From 1d73eb7604b8b57d87f72d664355c2078b9c38cd Mon Sep 17 00:00:00 2001 From: Marino Faggiana <8616947+marinofaggiana@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:37:01 +0100 Subject: [PATCH 001/135] https://github.com/nextcloud/ios/issues/2390 Signed-off-by: Marino Faggiana <8616947+marinofaggiana@users.noreply.github.com> --- Sources/NextcloudKit/NextcloudKit.swift | 20 +++++++++---------- .../NextcloudKit/NextcloudKitBackground.swift | 18 +++++++---------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 6cb34a57..241a2c73 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -366,17 +366,14 @@ import SwiftyJSON let fileNameLocalPathUrl = URL(fileURLWithPath: fileNameLocalPath) var headers = self.nkCommonInstance.getStandardHeaders(options: options) - if dateCreationFile != nil { - var iDate: TimeInterval = dateCreationFile?.timeIntervalSince1970 ?? 0 - // Epoch of linux do not permitted negativ value - if iDate < 0 { iDate = 0 } - headers.update(name: "X-OC-CTime", value: "\(iDate)") + + // Epoch of linux do not permitted negativ value + if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { + headers.update(name: "X-OC-CTime", value: "\(dateCreationFile.timeIntervalSince1970)") } - if dateModificationFile != nil { - var iDate: TimeInterval = dateModificationFile?.timeIntervalSince1970 ?? 0 - // Epoch of linux do not permitted negativ value - if iDate < 0 { iDate = 0 } - headers.update(name: "X-OC-MTime", value: "\(iDate)") + // Epoch of linux do not permitted negativ value + if let dateModificationFile, dateModificationFile.timeIntervalSince1970 > 0 { + headers.update(name: "X-OC-MTime", value: "\(dateModificationFile.timeIntervalSince1970)") } let request = sessionManager.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in @@ -571,10 +568,11 @@ import SwiftyJSON // Assemble the chunks let serverUrlFileNameSource = serverUrlChunkFolder + "/.file" - + // Epoch of linux do not permitted negativ value if let creationDate, creationDate.timeIntervalSince1970 > 0 { options.customHeader?["X-OC-CTime"] = "\(creationDate.timeIntervalSince1970)" } + // Epoch of linux do not permitted negativ value if let date, date.timeIntervalSince1970 > 0 { options.customHeader?["X-OC-MTime"] = "\(date.timeIntervalSince1970)" } diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 9ad88c53..420432f1 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -104,17 +104,13 @@ import Foundation request.httpMethod = "PUT" request.setValue(self.nkCommonInstance.userAgent, forHTTPHeaderField: "User-Agent") request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") - if dateCreationFile != nil { - var iDate: TimeInterval = dateCreationFile?.timeIntervalSince1970 ?? 0 - // Epoch of linux do not permitted negativ value - if iDate < 0 { iDate = 0 } - request.setValue("\(iDate)", forHTTPHeaderField: "X-OC-CTime") - } - if dateModificationFile != nil { - var iDate: TimeInterval = dateModificationFile?.timeIntervalSince1970 ?? 0 - // Epoch of linux do not permitted negativ value - if iDate < 0 { iDate = 0 } - request.setValue("\(iDate)", forHTTPHeaderField: "X-OC-MTime") + // Epoch of linux do not permitted negativ value + if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { + request.setValue("\(dateCreationFile.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-CTime") + } + // Epoch of linux do not permitted negativ value + if let dateModificationFile, dateModificationFile.timeIntervalSince1970 > 0 { + request.setValue("\(dateModificationFile.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-MTime") } let task = session.uploadTask(with: request, fromFile: URL(fileURLWithPath: fileNameLocalPath)) From cbd0274545069ecae82e3fce077ac587445a6882 Mon Sep 17 00:00:00 2001 From: Marino Faggiana <8616947+marinofaggiana@users.noreply.github.com> Date: Sat, 25 Nov 2023 11:41:21 +0100 Subject: [PATCH 002/135] add async/await searchMedia Signed-off-by: Marino Faggiana <8616947+marinofaggiana@users.noreply.github.com> --- .../WebDAV/NextcloudKit+WebDAV_Async.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV_Async.swift b/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV_Async.swift index 8f0aa7b9..7f1015e8 100644 --- a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV_Async.swift +++ b/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV_Async.swift @@ -82,4 +82,20 @@ extension NextcloudKit { } }) } + + public func searchMedia(path: String = "", + lessDate: Any, + greaterDate: Any, + elementDate: String, + limit: Int, + showHiddenFiles: Bool, + includeHiddenFiles: [String] = [], + options: NKRequestOptions = NKRequestOptions()) async -> (account: String, files: [NKFile], data: Data?, error: NKError) { + + await withUnsafeContinuation({ continuation in + searchMedia(path: path, lessDate: lessDate, greaterDate: greaterDate, elementDate: elementDate, limit: limit, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { account, files, data, error in + continuation.resume(returning: (account, files, data, error)) + } + }) + } } From 31ec1134ad04cfa8f38f995845b5f9c757bc00a2 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 28 Nov 2023 17:52:10 +0100 Subject: [PATCH 003/135] Livephoto detect (#51) * Update NKModel.swift --- Package.resolved | 4 ++-- Sources/NextcloudKit/NKModel.swift | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Package.resolved b/Package.resolved index 95debd57..dd082463 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/Alamofire/Alamofire", "state": { "branch": null, - "revision": "bc268c28fb170f494de9e9927c371b8342979ece", - "version": "5.7.1" + "revision": "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", + "version": "5.8.1" } }, { diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 734606d6..8251c62d 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -162,6 +162,7 @@ import SwiftyJSON @objc public var height: Int = 0 @objc public var width: Int = 0 @objc public var livePhotoFile = "" + @objc public var livePhotoServer = false @objc public var hidden = false } @@ -912,6 +913,7 @@ class NKDataFileXML: NSObject { if let livePhotoFile = propstat["d:prop", "nc:metadata-files-live-photo"].text { file.livePhotoFile = livePhotoFile + file.livePhotoServer = true } if let hidden = propstat["d:prop", "nc:hidden"].text { @@ -931,6 +933,23 @@ class NKDataFileXML: NSObject { files.append(file) } + // Live photo detect + files = files.sorted { + return ($0.serverUrl, ($0.fileName as NSString).deletingPathExtension, $0.classFile) < ($1.serverUrl, ($1.fileName as NSString).deletingPathExtension, $1.classFile) + } + for index in files.indices { + if !files[index].livePhotoFile.isEmpty || files[index].directory { + continue + } + if index < files.count - 1, + (files[index].fileName as NSString).deletingPathExtension == (files[index + 1].fileName as NSString) .deletingPathExtension, + files[index].classFile == NKCommon.TypeClassFile.image.rawValue, + files[index + 1].classFile == NKCommon.TypeClassFile.video.rawValue { + files[index].livePhotoFile = files[index + 1].fileName + files[index + 1].livePhotoFile = files[index].fileName + } + } + return files } From 544d8ad8f6eb78f76dbbeff5866c244e139c65c6 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 30 Nov 2023 10:01:28 +0100 Subject: [PATCH 004/135] cleaning Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 8251c62d..9aa797a5 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -162,7 +162,7 @@ import SwiftyJSON @objc public var height: Int = 0 @objc public var width: Int = 0 @objc public var livePhotoFile = "" - @objc public var livePhotoServer = false + @objc public var isFlaggedAsLivePhotoByServer = false // A flag indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side, then send it back to server. (for compatibility NC < 28) @objc public var hidden = false } @@ -913,7 +913,7 @@ class NKDataFileXML: NSObject { if let livePhotoFile = propstat["d:prop", "nc:metadata-files-live-photo"].text { file.livePhotoFile = livePhotoFile - file.livePhotoServer = true + file.isFlaggedAsLivePhotoByServer = true } if let hidden = propstat["d:prop", "nc:hidden"].text { From b4df6c9257d5291292efcb11d4659597a14f6c74 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Mon, 4 Dec 2023 18:01:24 +0800 Subject: [PATCH 005/135] Fix build on macOS broken by freeDisk change (#53) Signed-off-by: Claudio Cambra --- Sources/NextcloudKit/NextcloudKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 241a2c73..17337c61 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -474,7 +474,7 @@ import SwiftyJSON } catch { return completion(account, nil, nil, nil, NKError(errorCode: NKError.chunkNoEnoughMemory)) } - let freeDisk = (fsAttributes[FileAttributeKey.systemFreeSize] ?? 0) as? Int64 + let freeDisk = ((fsAttributes[FileAttributeKey.systemFreeSize] ?? 0) as? Int64) ?? 0 #else var freeDisk: Int64 = 0 let fileURL = URL(fileURLWithPath: directory as String) From 47b230f2da47da1e937391a0ca9860ff9d1a43a8 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 7 Dec 2023 11:07:54 +0100 Subject: [PATCH 006/135] livePhotoFile -> fileId Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 9aa797a5..4db1af43 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -945,8 +945,8 @@ class NKDataFileXML: NSObject { (files[index].fileName as NSString).deletingPathExtension == (files[index + 1].fileName as NSString) .deletingPathExtension, files[index].classFile == NKCommon.TypeClassFile.image.rawValue, files[index + 1].classFile == NKCommon.TypeClassFile.video.rawValue { - files[index].livePhotoFile = files[index + 1].fileName - files[index + 1].livePhotoFile = files[index].fileName + files[index].livePhotoFile = files[index + 1].fileId + files[index + 1].livePhotoFile = files[index].fileId } } From e3b2ae86439ee9f14f96238c817647d15a144e91 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 7 Dec 2023 11:37:32 +0100 Subject: [PATCH 007/135] rollback Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 4db1af43..9aa797a5 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -945,8 +945,8 @@ class NKDataFileXML: NSObject { (files[index].fileName as NSString).deletingPathExtension == (files[index + 1].fileName as NSString) .deletingPathExtension, files[index].classFile == NKCommon.TypeClassFile.image.rawValue, files[index + 1].classFile == NKCommon.TypeClassFile.video.rawValue { - files[index].livePhotoFile = files[index + 1].fileId - files[index + 1].livePhotoFile = files[index].fileId + files[index].livePhotoFile = files[index + 1].fileName + files[index + 1].livePhotoFile = files[index].fileName } } From b5f351d8f2374ff909be363e1721faa8ecc30601 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 9 Dec 2023 15:08:48 +0100 Subject: [PATCH 008/135] rollback Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 9aa797a5..4db1af43 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -945,8 +945,8 @@ class NKDataFileXML: NSObject { (files[index].fileName as NSString).deletingPathExtension == (files[index + 1].fileName as NSString) .deletingPathExtension, files[index].classFile == NKCommon.TypeClassFile.image.rawValue, files[index + 1].classFile == NKCommon.TypeClassFile.video.rawValue { - files[index].livePhotoFile = files[index + 1].fileName - files[index + 1].livePhotoFile = files[index].fileName + files[index].livePhotoFile = files[index + 1].fileId + files[index + 1].livePhotoFile = files[index].fileId } } From e77c2dccc0ce19e79f0ce8af6c5db8ff1dd04670 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 13 Dec 2023 12:27:29 +0100 Subject: [PATCH 009/135] Fix error description chunk Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKError.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index 845b7b66..a821a68d 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -77,6 +77,16 @@ public class NKError: NSObject { private static func getErrorDescription(for code: Int) -> String? { switch code { + case chunkNoEnoughMemory: + return NSLocalizedString("_chunk_enough_memory_", value: "It seems there is not enough space to send the file", comment: "") + case chunkMoveFile: + return NSLocalizedString("_chunk_move_", value: "The sent file could not be reassembled, please check the server log", comment: "") + case chunkCreateFolder: + return NSLocalizedString("_chunk_create_folder_", value: "The file could not be sent, please check the server log", comment: "") + case chunkFilesNull: + return NSLocalizedString("_chunk_files_null_", value: "The file for sending could not be created", comment: "") + case chunkFileNull: + return NSLocalizedString("_chunk_file_null_", value: "The file could not be sent", comment: "") case -9999: return NSLocalizedString("_internal_server_", value: "Internal error", comment: "") case -1001: From 396f18be198e045c0cdf914244b827139dfa2a29 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 13 Dec 2023 14:53:45 +0100 Subject: [PATCH 010/135] chunk error fix Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKError.swift | 10 ---------- Sources/NextcloudKit/NextcloudKit.swift | 9 ++++++--- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index a821a68d..845b7b66 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -77,16 +77,6 @@ public class NKError: NSObject { private static func getErrorDescription(for code: Int) -> String? { switch code { - case chunkNoEnoughMemory: - return NSLocalizedString("_chunk_enough_memory_", value: "It seems there is not enough space to send the file", comment: "") - case chunkMoveFile: - return NSLocalizedString("_chunk_move_", value: "The sent file could not be reassembled, please check the server log", comment: "") - case chunkCreateFolder: - return NSLocalizedString("_chunk_create_folder_", value: "The file could not be sent, please check the server log", comment: "") - case chunkFilesNull: - return NSLocalizedString("_chunk_files_null_", value: "The file for sending could not be created", comment: "") - case chunkFileNull: - return NSLocalizedString("_chunk_file_null_", value: "The file could not be sent", comment: "") case -9999: return NSLocalizedString("_internal_server_", value: "Internal error", comment: "") case -1001: diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 17337c61..c357d05a 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -487,7 +487,8 @@ import SwiftyJSON #endif if freeDisk < fileNameLocalSize * 3 { - return completion(account, nil, nil, nil, NKError(errorCode: NKError.chunkNoEnoughMemory)) + let error = NKError(errorCode: NKError.chunkNoEnoughMemory, errorDescription: NSLocalizedString("_chunk_enough_memory_", value: "It seems there is not enough space to send the file", comment: "")) + return completion(account, nil, nil, nil, error) } func createFolder(completion: @escaping (_ errorCode: NKError) -> Void) { @@ -520,7 +521,8 @@ import SwiftyJSON counterChunk(counter) } completion: { filesChunk in if filesChunk.isEmpty { - return completion(account, nil, nil, nil, NKError(errorCode: NKError.chunkFilesNull)) + let error = NKError(errorCode: NKError.chunkFilesNull, errorDescription: NSLocalizedString("_chunk_files_null_", value: "The file for sending could not be created", comment: "")) + return completion(account, nil, nil, nil, error) } var filesChunkOutput = filesChunk @@ -533,7 +535,8 @@ import SwiftyJSON let fileSize = self.nkCommonInstance.getFileSize(filePath: fileNameLocalPath) if fileSize == 0 { - return completion(account, nil, nil, .explicitlyCancelled, NKError(errorCode: NKError.chunkFileNull)) + let error = NKError(errorCode: NKError.chunkFileNull, errorDescription: NSLocalizedString("_chunk_file_null_", value: "The file could not be sent", comment: "")) + return completion(account, nil, nil, .explicitlyCancelled, error) } let semaphore = DispatchSemaphore(value: 0) From 4604eb4e6e342899e9c258f425d81611ec8f3b45 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 14 Dec 2023 09:22:27 +0100 Subject: [PATCH 011/135] errorDescription Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index c357d05a..9faaa00e 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -486,8 +486,9 @@ import SwiftyJSON } catch { } #endif - if freeDisk < fileNameLocalSize * 3 { - let error = NKError(errorCode: NKError.chunkNoEnoughMemory, errorDescription: NSLocalizedString("_chunk_enough_memory_", value: "It seems there is not enough space to send the file", comment: "")) + if freeDisk < fileNameLocalSize * 4 { + // It seems there is not enough space to send the file + let error = NKError(errorCode: NKError.chunkNoEnoughMemory, errorDescription: "_chunk_enough_memory_") return completion(account, nil, nil, nil, error) } @@ -521,7 +522,8 @@ import SwiftyJSON counterChunk(counter) } completion: { filesChunk in if filesChunk.isEmpty { - let error = NKError(errorCode: NKError.chunkFilesNull, errorDescription: NSLocalizedString("_chunk_files_null_", value: "The file for sending could not be created", comment: "")) + // The file for sending could not be created + let error = NKError(errorCode: NKError.chunkFilesNull, errorDescription: "_chunk_files_null_") return completion(account, nil, nil, nil, error) } @@ -535,7 +537,8 @@ import SwiftyJSON let fileSize = self.nkCommonInstance.getFileSize(filePath: fileNameLocalPath) if fileSize == 0 { - let error = NKError(errorCode: NKError.chunkFileNull, errorDescription: NSLocalizedString("_chunk_file_null_", value: "The file could not be sent", comment: "")) + // The file could not be sent + let error = NKError(errorCode: NKError.chunkFileNull, errorDescription: "_chunk_file_null_") return completion(account, nil, nil, .explicitlyCancelled, error) } From 20f6e08d3796fb6b5314a5d4de6234d2c712b18f Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 15 Dec 2023 10:36:04 +0100 Subject: [PATCH 012/135] added description Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 4db1af43..f6f225b6 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -161,8 +161,8 @@ import SwiftyJSON @objc public var longitude: Double = 0 @objc public var height: Int = 0 @objc public var width: Int = 0 - @objc public var livePhotoFile = "" - @objc public var isFlaggedAsLivePhotoByServer = false // A flag indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side, then send it back to server. (for compatibility NC < 28) + @objc public var livePhotoFile = "" // If this is not empty, the media is a live photo. New media gets this straight from server, but old media needs to be detected as live photo (look isFlaggedAsLivePhotoByServer) + @objc public var isFlaggedAsLivePhotoByServer = false // Indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side @objc public var hidden = false } From 94bbdab3dade066c284ac9cd855a38408d542794 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Fri, 15 Dec 2023 16:35:29 +0100 Subject: [PATCH 013/135] Change to markdown Signed-off-by: Milen Pivchev --- Sources/NextcloudKit/NKModel.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index f6f225b6..3bceb471 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -161,8 +161,13 @@ import SwiftyJSON @objc public var longitude: Double = 0 @objc public var height: Int = 0 @objc public var width: Int = 0 - @objc public var livePhotoFile = "" // If this is not empty, the media is a live photo. New media gets this straight from server, but old media needs to be detected as live photo (look isFlaggedAsLivePhotoByServer) - @objc public var isFlaggedAsLivePhotoByServer = false // Indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side + + /// If this is not empty, the media is a live photo. New media gets this straight from server, but old media needs to be detected as live photo (look isFlaggedAsLivePhotoByServer) + @objc public var livePhotoFile = "" + + /// Indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side + @objc public var isFlaggedAsLivePhotoByServer = false + @objc public var hidden = false } From f7f618d7234f38efd185cdae9d9c1360200154e4 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 18 Jan 2024 14:33:53 +0100 Subject: [PATCH 014/135] Added e2ee options versionApi Signed-off-by: Marino Faggiana --- .../NextcloudKit/E2EE/NextcloudKit+E2EE.swift | 68 +++++++++++++++---- Sources/NextcloudKit/NKRequestOptions.swift | 12 +++- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift index 1ed932d1..7de101a4 100644 --- a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift @@ -35,7 +35,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/encrypted/\(fileId)" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/encrypted/\(fileId)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } @@ -76,7 +80,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/lock/\(fileId)" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/lock/\(fileId)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } @@ -125,7 +133,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/meta-data/\(fileId)" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } @@ -172,7 +184,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/meta-data/\(fileId)" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } @@ -225,15 +241,19 @@ extension NextcloudKit { let urlBase = self.nkCommonInstance.urlBase let userId = self.nkCommonInstance.userId + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } var endpoint = "" if let user = user { guard let users = ("[\"" + user + "\"]").urlEncoded else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/public-key?users=" + users + endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key?users=" + users } else { - endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/public-key" + endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" } guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -275,7 +295,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/private-key" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } @@ -311,7 +335,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/server-key" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/server-key" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } @@ -348,7 +376,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/public-key" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } @@ -388,7 +420,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/private-key" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } @@ -426,7 +462,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/public-key" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } @@ -455,7 +495,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/v1/private-key" + var version = "v1" + if let optionsVesion = options.version { + version = optionsVesion + } + let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } diff --git a/Sources/NextcloudKit/NKRequestOptions.swift b/Sources/NextcloudKit/NKRequestOptions.swift index 4d3ddc5b..7f6ccc32 100644 --- a/Sources/NextcloudKit/NKRequestOptions.swift +++ b/Sources/NextcloudKit/NKRequestOptions.swift @@ -27,6 +27,7 @@ import Foundation public class NKRequestOptions: NSObject { var endpoint: String? + var version: String? var customHeader: [String: String]? var customUserAgent: String? var contentType: String? @@ -34,8 +35,17 @@ public class NKRequestOptions: NSObject { var timeout: TimeInterval var queue: DispatchQueue - public init(endpoint: String? = nil, customHeader: [String: String]? = nil, customUserAgent: String? = nil, contentType: String? = nil, e2eToken: String? = nil, timeout: TimeInterval = 60, queue: DispatchQueue = .main) { + public init(endpoint: String? = nil, + version: String? = nil, + customHeader: [String: String]? = nil, + customUserAgent: String? = nil, + contentType: String? = nil, + e2eToken: String? = nil, + timeout: TimeInterval = 60, + queue: DispatchQueue = .main) { + self.endpoint = endpoint + self.version = version self.customHeader = customHeader self.customUserAgent = customUserAgent self.contentType = contentType From 77dffd843f7d4b560d7442accf295c21649cac1c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 22 Jan 2024 17:58:05 +0100 Subject: [PATCH 015/135] change variable name Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/API/NextcloudKit+API.swift | 4 ++-- Sources/NextcloudKit/API/NextcloudKit+API_Async.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/API/NextcloudKit+API.swift index 20cd40e5..465edc67 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API.swift @@ -871,7 +871,7 @@ extension NextcloudKit { // MARK: - - public func sendClientDiagnosticsRemoteOperation(problems: Data, + public func sendClientDiagnosticsRemoteOperation(data: Data, options: NKRequestOptions = NKRequestOptions(), completion: @escaping (_ account: String, _ error: NKError) -> Void) { @@ -889,7 +889,7 @@ extension NextcloudKit { var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: .put, headers: headers) - urlRequest.httpBody = problems + urlRequest.httpBody = data } catch { return options.queue.async { completion(account, NKError(error: error)) } } diff --git a/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift b/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift index 7b369aae..20e8b413 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift @@ -78,11 +78,11 @@ extension NextcloudKit { }) } - public func sendClientDiagnosticsRemoteOperation(problems: Data, + public func sendClientDiagnosticsRemoteOperation(data: Data, options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { await withUnsafeContinuation({ continuation in - sendClientDiagnosticsRemoteOperation(problems: problems, options: options) { account, error in + sendClientDiagnosticsRemoteOperation(data: data, options: options) { account, error in continuation.resume(returning: (account: account, error: error)) } }) From 28eede93ae7ea3a01c6f75aeaee8286f881ff276 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 23 Jan 2024 16:05:16 +0100 Subject: [PATCH 016/135] endpoint update Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/API/NextcloudKit+API.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/API/NextcloudKit+API.swift index 465edc67..78b151f1 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API.swift @@ -878,7 +878,7 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/settings/diagnostics" + let endpoint = "ocs/v2.php/apps/security_guard/diagnostics" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } From 14c622f438b1905a9ef2971896e83238c0be9863 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 29 Jan 2024 16:35:07 +0100 Subject: [PATCH 017/135] Fix chunk S3 (#56) * cod Signed-off-by: Marino Faggiana * fix Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 9faaa00e..920c5516 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -465,6 +465,7 @@ import SwiftyJSON options.customHeader = [:] } options.customHeader?["Destination"] = serverUrlFileName + options.customHeader?["OC-Total-Length"] = String(fileNameLocalSize) // check space #if os(macOS) @@ -494,11 +495,11 @@ import SwiftyJSON func createFolder(completion: @escaping (_ errorCode: NKError) -> Void) { - readFileOrFolder(serverUrlFileName: serverUrlChunkFolder, depth: "0", options: NKRequestOptions(queue: self.nkCommonInstance.backgroundQueue)) { _, _, _, error in + readFileOrFolder(serverUrlFileName: serverUrlChunkFolder, depth: "0", options: options) { _, _, _, error in if error == .success { completion(NKError()) } else if error.errorCode == 404 { - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, options: NKRequestOptions(queue: self.nkCommonInstance.backgroundQueue)) { _, _, _, error in + NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, options: options) { _, _, _, error in completion(error) } } else { From 41475e7e9b3c7e121766499d31621949332cbfee Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 5 Feb 2024 16:02:15 +0100 Subject: [PATCH 018/135] public func Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 576e9f1e..8ac43abe 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -530,7 +530,7 @@ import MobileCoreServices return serverUrl.asUrl } - func convertDate(_ dateString: String, format: String) -> NSDate? { + public func convertDate(_ dateString: String, format: String) -> NSDate? { if dateString.isEmpty { return nil } let dateFormatter = DateFormatter() From ec87ccc1db380bbfa6e5904bdedfdaef1f30f488 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 5 Feb 2024 16:04:04 +0100 Subject: [PATCH 019/135] lint Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 8ac43abe..88e97fa9 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -531,6 +531,7 @@ import MobileCoreServices } public func convertDate(_ dateString: String, format: String) -> NSDate? { + if dateString.isEmpty { return nil } let dateFormatter = DateFormatter() From d231a48c227c7737ad02f705439903bdc897582b Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 7 Feb 2024 08:28:10 +0100 Subject: [PATCH 020/135] remove description Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 4 +-- .../NextcloudKit/NextcloudKitBackground.swift | 28 ++++--------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 88e97fa9..f7172431 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -37,8 +37,8 @@ import MobileCoreServices @objc optional func downloadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) @objc optional func uploadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) - @objc optional func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: NSDate?, dateLastModified: NSDate?, length: Int64, description: String?, task: URLSessionTask, error: NKError) - @objc optional func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: NSDate?, size: Int64, description: String?, task: URLSessionTask, error: NKError) + @objc optional func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: NSDate?, dateLastModified: NSDate?, length: Int64, task: URLSessionTask, error: NKError) + @objc optional func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: NSDate?, size: Int64, task: URLSessionTask, error: NKError) } @objc public class NKCommon: NSObject { diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 420432f1..85654cca 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -35,7 +35,6 @@ import Foundation @objc public func download(serverUrlFileName: Any, fileNameLocalPath: String, - description: String?, session: URLSession) -> URLSessionDownloadTask? { var url: URL? @@ -59,15 +58,8 @@ import Foundation request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") let task = session.downloadTask(with: request) - - if description == nil { - task.taskDescription = fileNameLocalPath - } else { - task.taskDescription = fileNameLocalPath + "|" + description! - } - + task.taskDescription = fileNameLocalPath task.resume() - self.nkCommonInstance.writeLog("Network start download file: \(serverUrlFileName)") return task @@ -79,7 +71,6 @@ import Foundation fileNameLocalPath: String, dateCreationFile: Date?, dateModificationFile: Date?, - description: String?, session: URLSession) -> URLSessionUploadTask? { var url: URL? @@ -114,10 +105,8 @@ import Foundation } let task = session.uploadTask(with: request, fromFile: URL(fileURLWithPath: fileNameLocalPath)) - - task.taskDescription = description + task.taskDescription = fileNameLocalPath task.resume() - self.nkCommonInstance.writeLog("Network start upload file: \(serverUrlFileName)") return task @@ -211,16 +200,9 @@ import Foundation } if task is URLSessionDownloadTask { - var description = task.taskDescription - let parameter = task.taskDescription?.components(separatedBy: "|") - if parameter?.count == 2 { - description = parameter![1] - } - self.nkCommonInstance.delegate?.downloadComplete?(fileName: fileName, serverUrl: serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, description: description, task: task, error: nkError) - } - if task is URLSessionUploadTask { - - self.nkCommonInstance.delegate?.uploadComplete?(fileName: fileName, serverUrl: serverUrl, ocId: ocId, etag: etag, date: date, size: task.countOfBytesExpectedToSend, description: task.taskDescription, task: task, error: nkError) + self.nkCommonInstance.delegate?.downloadComplete?(fileName: fileName, serverUrl: serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, task: task, error: nkError) + } else if task is URLSessionUploadTask { + self.nkCommonInstance.delegate?.uploadComplete?(fileName: fileName, serverUrl: serverUrl, ocId: ocId, etag: etag, date: date, size: task.countOfBytesExpectedToSend, task: task, error: nkError) } if nkError.errorCode == 0 { From c2e2d83b2f44826291bfd93a21eb19fcdcd1155a Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 7 Feb 2024 08:57:57 +0100 Subject: [PATCH 021/135] fix Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKitBackground.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 85654cca..e0a43a05 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -129,9 +129,7 @@ import Foundation if let httpResponse = (downloadTask.response as? HTTPURLResponse) { if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 { - let parameter = downloadTask.taskDescription?.components(separatedBy: "|") - if parameter?.count ?? 0 >= 1 { - let destinationFilePath = parameter![0] + if let destinationFilePath = downloadTask.taskDescription { let destinationUrl = NSURL.fileURL(withPath: destinationFilePath) do { try FileManager.default.removeItem(at: destinationUrl) From 23b355533ac257c06324f4e2b378790c871393d3 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 7 Feb 2024 09:04:20 +0100 Subject: [PATCH 022/135] fix Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 4 ++-- Sources/NextcloudKit/NextcloudKitBackground.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index f7172431..70e51bd1 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -37,8 +37,8 @@ import MobileCoreServices @objc optional func downloadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) @objc optional func uploadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) - @objc optional func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: NSDate?, dateLastModified: NSDate?, length: Int64, task: URLSessionTask, error: NKError) - @objc optional func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: NSDate?, size: Int64, task: URLSessionTask, error: NKError) + @objc optional func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: NSDate?, dateLastModified: NSDate?, length: Int64, fileNameLocalPath: String?, task: URLSessionTask, error: NKError) + @objc optional func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: NSDate?, size: Int64, fileNameLocalPath: String?, task: URLSessionTask, error: NKError) } @objc public class NKCommon: NSObject { diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index e0a43a05..67575ef5 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -198,9 +198,9 @@ import Foundation } if task is URLSessionDownloadTask { - self.nkCommonInstance.delegate?.downloadComplete?(fileName: fileName, serverUrl: serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, task: task, error: nkError) + self.nkCommonInstance.delegate?.downloadComplete?(fileName: fileName, serverUrl: serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, fileNameLocalPath: task.taskDescription, task: task, error: nkError) } else if task is URLSessionUploadTask { - self.nkCommonInstance.delegate?.uploadComplete?(fileName: fileName, serverUrl: serverUrl, ocId: ocId, etag: etag, date: date, size: task.countOfBytesExpectedToSend, task: task, error: nkError) + self.nkCommonInstance.delegate?.uploadComplete?(fileName: fileName, serverUrl: serverUrl, ocId: ocId, etag: etag, date: date, size: task.countOfBytesExpectedToSend, fileNameLocalPath: task.taskDescription, task: task, error: nkError) } if nkError.errorCode == 0 { From 7d61d2826dc93a69fe27bfc5a7dce237317e1416 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 10 Feb 2024 10:11:14 +0100 Subject: [PATCH 023/135] fix log Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKitBackground.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 67575ef5..f202317b 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -204,9 +204,9 @@ import Foundation } if nkError.errorCode == 0 { - self.nkCommonInstance.writeLog("Network completed upload file: \(serverUrl)/\(fileName)") + self.nkCommonInstance.writeLog("Network completed file: \(serverUrl)/\(fileName)") } else { - self.nkCommonInstance.writeLog("Network completed upload file: \(serverUrl)/\(fileName) with error code \(nkError.errorCode) and error description " + nkError.errorDescription) + self.nkCommonInstance.writeLog("Network completed file: \(serverUrl)/\(fileName) with error code \(nkError.errorCode) and error description " + nkError.errorDescription) } } From 3c34cc55eacae0654af5ce5a1761fdfab7411376 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 28 Feb 2024 12:27:50 +0100 Subject: [PATCH 024/135] Resolution - GPS (#59) * fix resolution-gps Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 43 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 3bceb471..04d0a4d0 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -159,6 +159,7 @@ import SwiftyJSON @objc public var userId = "" @objc public var latitude: Double = 0 @objc public var longitude: Double = 0 + @objc public var altitude: Double = 0 @objc public var height: Int = 0 @objc public var width: Int = 0 @@ -877,43 +878,41 @@ class NKDataFileXML: NSObject { } // NC27 ----- - if let gps = propstat["d:prop", "nc:file-metadata-gps"].text, - let data = gps.data(using: .utf8), - let jsonDict = try? JSONSerialization.jsonObject(with: data) as? [String: Double], - let latitude = jsonDict["latitude"], - let longitude = jsonDict["longitude"] { + if let latitude = propstat["d:prop", "nc:file-metadata-gps", "latitude"].double { file.latitude = latitude + } + if let longitude = propstat["d:prop", "nc:file-metadata-gps", "longitude"].double { file.longitude = longitude } + if let altitude = propstat["d:prop", "nc:file-metadata-gps", "altitude"].double { + file.altitude = altitude + } - if let resolution = propstat["d:prop", "nc:file-metadata-size"].text, - let data = resolution.data(using: .utf8), - let jsonDict = try? JSONSerialization.jsonObject(with: data) as? [String: Int], - let height = jsonDict["height"], - let width = jsonDict["width"] { - file.height = height + if let width = propstat["d:prop", "nc:file-metadata-size", "width"].int { file.width = width } + if let height = propstat["d:prop", "nc:file-metadata-size", "height"].int { + file.height = height + } // ---------- // ----- NC28 - if let gps = propstat["d:prop", "nc:metadata-photos-gps"].text, - let data = gps.data(using: .utf8), - let jsonDict = try? JSONSerialization.jsonObject(with: data) as? [String: Double], - let latitude = jsonDict["latitude"], - let longitude = jsonDict["longitude"] { + if let latitude = propstat["d:prop", "nc:metadata-photos-gps", "latitude"].double { file.latitude = latitude + } + if let longitude = propstat["d:prop", "nc:metadata-photos-gps", "longitude"].double { file.longitude = longitude } + if let altitude = propstat["d:prop", "nc:metadata-photos-gps", "altitude"].double { + file.altitude = altitude + } - if let resolution = propstat["d:prop", "nc:metadata-photos-size"].text, - let data = resolution.data(using: .utf8), - let jsonDict = try? JSONSerialization.jsonObject(with: data) as? [String: Int], - let height = jsonDict["height"], - let width = jsonDict["width"] { - file.height = height + if let width = propstat["d:prop", "nc:metadata-photos-size", "width"].int { file.width = width } + if let height = propstat["d:prop", "nc:metadata-photos-size", "height"].int { + file.height = height + } // ---------- if let livePhotoFile = propstat["d:prop", "nc:metadata-files-live-photo"].text { From ff4d3d1c7ee9bb2d6ef8d2c073d757bcfec7df86 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 28 Feb 2024 14:40:33 +0100 Subject: [PATCH 025/135] set h, w to Double Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 04d0a4d0..18735ed0 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -160,8 +160,8 @@ import SwiftyJSON @objc public var latitude: Double = 0 @objc public var longitude: Double = 0 @objc public var altitude: Double = 0 - @objc public var height: Int = 0 - @objc public var width: Int = 0 + @objc public var height: Double = 0 + @objc public var width: Double = 0 /// If this is not empty, the media is a live photo. New media gets this straight from server, but old media needs to be detected as live photo (look isFlaggedAsLivePhotoByServer) @objc public var livePhotoFile = "" @@ -888,10 +888,10 @@ class NKDataFileXML: NSObject { file.altitude = altitude } - if let width = propstat["d:prop", "nc:file-metadata-size", "width"].int { + if let width = propstat["d:prop", "nc:file-metadata-size", "width"].double { file.width = width } - if let height = propstat["d:prop", "nc:file-metadata-size", "height"].int { + if let height = propstat["d:prop", "nc:file-metadata-size", "height"].double { file.height = height } // ---------- @@ -907,10 +907,10 @@ class NKDataFileXML: NSObject { file.altitude = altitude } - if let width = propstat["d:prop", "nc:metadata-photos-size", "width"].int { + if let width = propstat["d:prop", "nc:metadata-photos-size", "width"].double { file.width = width } - if let height = propstat["d:prop", "nc:metadata-photos-size", "height"].int { + if let height = propstat["d:prop", "nc:metadata-photos-size", "height"].double { file.height = height } // ---------- From 4a33e6ca0d4b1154d5cb10f94efcf110cff5bbc9 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 6 Mar 2024 15:02:36 +0100 Subject: [PATCH 026/135] coding (#61) Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Login.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 568e3030..65750d3d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -75,6 +75,47 @@ extension NextcloudKit { } } + @objc public func deleteAppPassword(serverUrl: String, + username: String, + password: String, + userAgent: String? = nil, + queue: DispatchQueue = .main, + completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { + + let endpoint = "ocs/v2.php/core/apppassword" + + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + return queue.async { completion(nil, .urlError) } + } + + var headers: HTTPHeaders = [.authorization(username: username, password: password)] + if let userAgent = userAgent { + headers.update(.userAgent(userAgent)) + } + headers.update(name: "OCS-APIRequest", value: "true") + + var urlRequest: URLRequest + do { + try urlRequest = URLRequest(url: url, method: HTTPMethod(rawValue: "DELETE"), headers: headers) + } catch { + return queue.async { completion(nil, NKError(error: error)) } + } + + sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + queue.async { completion(nil, error) } + case .success(let xmlData): + queue.async { completion(xmlData, .success) } + } + } + } + // MARK: - Login Flow V2 @objc public func getLoginFlowV2(serverUrl: String, From 25e878827430ba7a5178e4a5e903985ef446088c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 8 Mar 2024 11:06:45 +0100 Subject: [PATCH 027/135] normalized taskHandler taskHandler(task) }. Signed-off-by: Marino Faggiana --- .../NextcloudKit/API/NextcloudKit+API.swift | 85 +++++++++++++++---- .../NextcloudKit/E2EE/NextcloudKit+E2EE.swift | 55 +++++++++--- .../NextcloudKit/NextcloudKit+Comments.swift | 25 ++++-- .../NextcloudKit/NextcloudKit+Dashboard.swift | 10 ++- .../NextcloudKit/NextcloudKit+FilesLock.swift | 5 +- .../NextcloudKit+Groupfolders.swift | 5 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 5 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 5 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 20 ++++- .../NextcloudKit/NextcloudKit+NCText.swift | 20 ++++- .../NextcloudKit+PushNotification.swift | 20 ++++- .../NextcloudKit+Richdocuments.swift | 20 ++++- .../NextcloudKit/NextcloudKit+Search.swift | 10 ++- Sources/NextcloudKit/NextcloudKit+Share.swift | 39 +++++++-- .../NextcloudKit+UserStatus.swift | 35 ++++++-- .../WebDAV/NextcloudKit+WebDAV.swift | 65 +++++++++++--- 16 files changed, 340 insertions(+), 84 deletions(-) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/API/NextcloudKit+API.swift index 78b151f1..9ef8b4b9 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API.swift @@ -34,13 +34,16 @@ extension NextcloudKit { @objc public func checkServer(serverUrl: String, queue: DispatchQueue = .main, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ error: NKError) -> Void) { guard let url = serverUrl.asUrl else { return queue.async { completion(.urlError) } } - sessionManager.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -60,6 +63,7 @@ extension NextcloudKit { @objc public func generalWithEndpoint(_ endpoint: String, method: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -73,7 +77,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -91,6 +97,7 @@ extension NextcloudKit { // MARK: - @objc public func getExternalSite(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -105,7 +112,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -156,6 +165,7 @@ extension NextcloudKit { public func getServerStatus(serverUrl: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (ServerInfoResult) -> Void) { let endpoint = "status.php" @@ -166,7 +176,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -210,13 +222,16 @@ extension NextcloudKit { @objc public func getPreview(url: URL, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -245,6 +260,7 @@ extension NextcloudKit { endpointTrashbin: Bool = false, useInternalEndpoint: Bool = true, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -280,7 +296,9 @@ extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -321,6 +339,7 @@ extension NextcloudKit { avatarSizeRounded: Int = 0, etag: String?, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -338,7 +357,9 @@ extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -403,6 +424,7 @@ extension NextcloudKit { @objc public func downloadContent(serverUrl: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -413,7 +435,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -435,6 +459,7 @@ extension NextcloudKit { // MARK: - @objc public func getUserProfile(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -448,7 +473,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -508,6 +535,7 @@ extension NextcloudKit { } @objc public func getCapabilities(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -521,7 +549,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -545,6 +575,7 @@ extension NextcloudKit { @objc public func getRemoteWipeStatus(serverUrl: String, token: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ wipe: Bool, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -559,7 +590,9 @@ extension NextcloudKit { return options.queue.async { completion(account, false, nil, .urlError) } } - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -579,6 +612,7 @@ extension NextcloudKit { @objc public func setRemoteWipeCompletition(serverUrl: String, token: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -593,7 +627,9 @@ extension NextcloudKit { return options.queue.async { completion(account, .urlError) } } - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -616,6 +652,7 @@ extension NextcloudKit { objectType: String?, previews: Bool, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -650,7 +687,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -714,6 +753,7 @@ extension NextcloudKit { // MARK: - @objc public func getNotifications(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -729,7 +769,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -794,6 +836,7 @@ extension NextcloudKit { idNotification: Int, method: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -814,7 +857,9 @@ extension NextcloudKit { let method = HTTPMethod(rawValue: method) let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -833,6 +878,7 @@ extension NextcloudKit { @objc public func getDirectDownload(fileId: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -851,7 +897,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -873,6 +921,7 @@ extension NextcloudKit { public func sendClientDiagnosticsRemoteOperation(data: Data, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -894,7 +943,9 @@ extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift index 7de101a4..75470434 100644 --- a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift @@ -30,6 +30,7 @@ extension NextcloudKit { @objc public func markE2EEFolder(fileId: String, delete: Bool, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -49,7 +50,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -75,6 +78,7 @@ extension NextcloudKit { e2eCounter: String?, method: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eToken: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -103,7 +107,9 @@ extension NextcloudKit { headers.update(name: "X-NC-E2EE-COUNTER", value: e2eCounter) } - sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -128,6 +134,7 @@ extension NextcloudKit { @objc public func getE2EEMetadata(fileId: String, e2eToken: String?, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eMetadata: String?, _ signature: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -150,7 +157,9 @@ extension NextcloudKit { parameters["e2e-token"] = e2eToken } - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -179,6 +188,7 @@ extension NextcloudKit { signature: String?, method: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ metadata: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -209,7 +219,9 @@ extension NextcloudKit { headers.update(name: "X-NC-E2EE-SIGNATURE", value: signature) } - sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -235,6 +247,7 @@ extension NextcloudKit { @objc public func getE2EECertificate(user: String? = nil, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ certificateUser: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -262,7 +275,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -290,6 +305,7 @@ extension NextcloudKit { } @objc public func getE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -307,7 +323,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -330,6 +348,7 @@ extension NextcloudKit { } @objc public func getE2EEPublicKey(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -347,7 +366,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -371,6 +392,7 @@ extension NextcloudKit { @objc public func signE2EECertificate(certificate: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -390,7 +412,9 @@ extension NextcloudKit { let parameters = ["csr": certificate] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -415,6 +439,7 @@ extension NextcloudKit { @objc public func storeE2EEPrivateKey(privateKey: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -434,7 +459,9 @@ extension NextcloudKit { let parameters = ["privateKey": privateKey] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -457,6 +484,7 @@ extension NextcloudKit { } @objc public func deleteE2EECertificate(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -474,7 +502,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -490,6 +520,7 @@ extension NextcloudKit { } @objc public func deleteE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -507,7 +538,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index e8831367..fedd6639 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -28,6 +28,7 @@ extension NextcloudKit { @objc public func getComments(fileId: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ items: [NKComments]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -50,7 +51,9 @@ extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -73,6 +76,7 @@ extension NextcloudKit { @objc public func putComments(fileId: String, message: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -95,7 +99,9 @@ extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -114,6 +120,7 @@ extension NextcloudKit { messageId: String, message: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -137,7 +144,9 @@ extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -155,6 +164,7 @@ extension NextcloudKit { @objc public func deleteComments(fileId: String, messageId: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -168,7 +178,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -185,6 +197,7 @@ extension NextcloudKit { @objc public func markAsReadComments(fileId: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -208,7 +221,9 @@ extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index ba401808..4b1ed83c 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -29,6 +29,7 @@ extension NextcloudKit { public func getDashboardWidget(options: NKRequestOptions = NKRequestOptions(), request: @escaping (DataRequest?) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardWidgets: [NCCDashboardWidget]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -48,7 +49,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -75,6 +78,7 @@ extension NextcloudKit { public func getDashboardWidgetsApplication(_ items: String, options: NKRequestOptions = NKRequestOptions(), request: @escaping (DataRequest?) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardApplications: [NCCDashboardApplication]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -94,7 +98,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index a4e02244..e91b5583 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -32,6 +32,7 @@ extension NextcloudKit { @objc public func lockUnlockFile(serverUrlFileName: String, shouldLock: Bool, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -46,7 +47,9 @@ extension NextcloudKit { var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "X-User-Lock", value: "1") - sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 15438f70..6ebe9ca8 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -28,6 +28,7 @@ import SwiftyJSON extension NextcloudKit { @objc public func getGroupfolders(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ results: [NKGroupfolders]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -42,7 +43,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index 352885b6..27b4600d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -30,6 +30,7 @@ extension NextcloudKit { @objc public func getHovercard(for userId: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ result: NKHovercard?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -44,7 +45,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index b5ca7ca9..4f862051 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -29,6 +29,7 @@ extension NextcloudKit { public func setLivephoto(serverUrlfileNamePath: String, livePhotoFile: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -49,7 +50,9 @@ extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 65750d3d..f590db9e 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -34,6 +34,7 @@ extension NextcloudKit { password: String, userAgent: String? = nil, queue: DispatchQueue = .main, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/getapppassword" @@ -55,7 +56,9 @@ extension NextcloudKit { return queue.async { completion(nil, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -80,6 +83,7 @@ extension NextcloudKit { password: String, userAgent: String? = nil, queue: DispatchQueue = .main, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/apppassword" @@ -101,7 +105,9 @@ extension NextcloudKit { return queue.async { completion(nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -121,6 +127,7 @@ extension NextcloudKit { @objc public func getLoginFlowV2(serverUrl: String, userAgent: String? = nil, queue: DispatchQueue = .main, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ endpoint: String?, _ login: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "index.php/login/v2" @@ -134,7 +141,9 @@ extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -159,6 +168,7 @@ extension NextcloudKit { endpoint: String, userAgent: String? = nil, queue: DispatchQueue = .main, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ server: String?, _ loginName: String?, _ appPassword: String?, _ data: Data?, _ error: NKError) -> Void) { let serverUrl = endpoint + "?token=" + token @@ -172,7 +182,9 @@ extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index cb77d7af..d6684371 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -28,6 +28,7 @@ import SwiftyJSON extension NextcloudKit { @objc public func NCTextObtainEditorDetails(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -44,7 +45,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -97,6 +100,7 @@ extension NextcloudKit { fileId: String? = nil, editor: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -117,7 +121,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -135,6 +141,7 @@ extension NextcloudKit { } @objc public func NCTextGetListOfTemplates(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ templates: [NKEditorTemplates], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -150,7 +157,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -184,6 +193,7 @@ extension NextcloudKit { creatorId: String, templateId: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -207,7 +217,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index 4e51169c..5307bf00 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -35,6 +35,7 @@ extension NextcloudKit { devicePublicKey: String, proxyServerUrl: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" @@ -51,7 +52,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -80,6 +83,7 @@ extension NextcloudKit { user: String, password: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" @@ -90,7 +94,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -111,6 +117,7 @@ extension NextcloudKit { signature: String, publicKey: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ error: NKError) -> Void) { let endpoint = "devices?format=json" @@ -129,7 +136,9 @@ extension NextcloudKit { let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -149,6 +158,7 @@ extension NextcloudKit { signature: String, publicKey: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ error: NKError) -> Void) { let endpoint = "devices" @@ -166,7 +176,9 @@ extension NextcloudKit { let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - sessionManager.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index 05b0e68d..a9119889 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -29,6 +29,7 @@ extension NextcloudKit { @objc public func createUrlRichdocuments(fileID: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -44,7 +45,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -67,6 +70,7 @@ extension NextcloudKit { @objc public func getTemplatesRichdocuments(typeTemplate: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ templates: [NKRichdocumentsTemplate]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -80,7 +84,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -117,6 +123,7 @@ extension NextcloudKit { @objc public func createRichdocuments(path: String, templateId: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -132,7 +139,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -155,6 +164,7 @@ extension NextcloudKit { @objc public func createAssetRichdocuments(path: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -170,7 +180,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index a4414f01..5fca06fe 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -48,6 +48,7 @@ extension NextcloudKit { timeoutProvider: TimeInterval = 60, 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, _ data: Data?, _ error: NKError) -> Void) { @@ -63,7 +64,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let requestUnifiedSearch = sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let requestUnifiedSearch = sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -124,6 +127,7 @@ extension NextcloudKit { cursor: Int? = nil, options: NKRequestOptions = NKRequestOptions(), timeout: TimeInterval = 60, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ accoun: String, NKSearchResult?, _ data: Data?, _ error: NKError) -> Void) -> DataRequest? { let urlBase = self.nkCommonInstance.urlBase @@ -162,7 +166,9 @@ extension NextcloudKit { return nil } - let requestSearchProvider = sessionManager.request(urlRequest).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let requestSearchProvider = sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 7d7ee36a..d792266d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -81,6 +81,7 @@ extension NextcloudKit { @objc public func readShares(parameters: NKShareParameter, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ shares: [NKShare]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -93,7 +94,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -134,6 +137,7 @@ extension NextcloudKit { itemType: String = "file", lookup: Bool = false, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ sharees: [NKSharee]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -160,7 +164,9 @@ extension NextcloudKit { "lookup": lookupString ] - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -255,9 +261,14 @@ extension NextcloudKit { password: String? = nil, permissions: Int = 1, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - createShare(path: path, shareType: 3, shareWith: nil, publicUpload: publicUpload, hideDownload: hideDownload, password: password, permissions: permissions, options: options, completion: completion) + createShare(path: path, shareType: 3, shareWith: nil, publicUpload: publicUpload, hideDownload: hideDownload, password: password, permissions: permissions, options: options) { task in + taskHandler(task) + } completion: { account, share, data, error in + completion(account, share, data, error) + } } @objc public func createShare(path: String, @@ -268,9 +279,14 @@ extension NextcloudKit { permissions: Int = 1, options: NKRequestOptions = NKRequestOptions(), attributes: String? = nil, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - createShare(path: path, shareType: shareType, shareWith: shareWith, publicUpload: false, note: note, hideDownload: false, password: password, permissions: permissions, attributes: attributes, options: options, completion: completion) + createShare(path: path, shareType: shareType, shareWith: shareWith, publicUpload: false, note: note, hideDownload: false, password: password, permissions: permissions, attributes: attributes, options: options) { task in + taskHandler(task) + } completion: { account, share, data, error in + completion(account, share, data, error) + } } private func createShare(path: String, @@ -283,6 +299,7 @@ extension NextcloudKit { permissions: Int = 1, attributes: String? = nil, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -320,7 +337,9 @@ extension NextcloudKit { parameters["attributes"] = attributes } - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -371,6 +390,7 @@ extension NextcloudKit { hideDownload: Bool, attributes: String? = nil, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -407,7 +427,9 @@ extension NextcloudKit { parameters["attributes"] = "[]" } - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -434,6 +456,7 @@ extension NextcloudKit { @objc public func deleteShare(idShare: Int, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -447,7 +470,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index d4525b14..b4e3f14b 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -29,6 +29,7 @@ extension NextcloudKit { @objc public func getUserStatus(userId: String? = nil, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ clearAt: NSDate?, _ icon: String?, _ message: String?, _ messageId: String?, _ messageIsPredefined: Bool, _ status: String?, _ statusIsUserDefined: Bool, _ userId: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -45,7 +46,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -81,6 +84,7 @@ extension NextcloudKit { @objc public func setUserStatus(status: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -98,7 +102,9 @@ extension NextcloudKit { "statusType": String(status) ] - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -122,6 +128,7 @@ extension NextcloudKit { @objc public func setCustomMessagePredefined(messageId: String, clearAt: Double, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -142,7 +149,9 @@ extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -168,6 +177,7 @@ extension NextcloudKit { message: String, clearAt: Double, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -191,7 +201,9 @@ extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -214,6 +226,7 @@ extension NextcloudKit { } @objc public func clearMessage(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -227,7 +240,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -250,6 +265,7 @@ extension NextcloudKit { } @objc public func getUserStatusPredefinedStatuses(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -264,7 +280,9 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -307,6 +325,7 @@ extension NextcloudKit { @objc public func getUserStatusRetrieveStatuses(limit: Int, offset: Int, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -326,7 +345,9 @@ extension NextcloudKit { "offset": String(offset) ] - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } diff --git a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift index 1c5a7886..0872a5fe 100644 --- a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift @@ -29,6 +29,7 @@ extension NextcloudKit { @objc public func createFolder(serverUrlFileName: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ ocId: String?, _ date: NSDate?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -49,7 +50,9 @@ extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -75,6 +78,7 @@ extension NextcloudKit { @objc public func deleteFileOrFolder(serverUrlFileName: String, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -93,7 +97,9 @@ extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -112,6 +118,7 @@ extension NextcloudKit { serverUrlFileNameDestination: String, overwrite: Bool, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -138,7 +145,9 @@ extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -157,6 +166,7 @@ extension NextcloudKit { serverUrlFileNameDestination: String, overwrite: Bool, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -183,7 +193,9 @@ extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -204,6 +216,7 @@ extension NextcloudKit { includeHiddenFiles: [String] = [], requestBody: Data? = nil, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -242,7 +255,9 @@ extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -265,6 +280,7 @@ extension NextcloudKit { @objc public func getFileFromFileId(fileId: String? = nil, link: String? = nil, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ file: NKFile?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -284,7 +300,9 @@ extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], options: options) { account, files, data, error in + search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], options: options) { task in + taskHandler(task) + } completion: { account, files, data, error in options.queue.async { completion(account, files.first, data, error) } } } @@ -294,11 +312,14 @@ extension NextcloudKit { showHiddenFiles: Bool, includeHiddenFiles: [String] = [], options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let httpBody = requestBody.data(using: .utf8)! - search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { account, files, data, error in + search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in + taskHandler(task) + } completion: { account, files, data, error in options.queue.async { completion(account, files, data, error) } } } @@ -309,6 +330,7 @@ extension NextcloudKit { showHiddenFiles: Bool, includeHiddenFiles: [String] = [], options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -321,7 +343,9 @@ extension NextcloudKit { let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodySearchFileName, href, depth, "%" + literal + "%") let httpBody = requestBody.data(using: .utf8)! - search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { account, files, data, error in + search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in + taskHandler(task) + } completion: { account, files, data, error in options.queue.async { completion(account, files, data, error) } } } @@ -334,6 +358,7 @@ extension NextcloudKit { showHiddenFiles: Bool, includeHiddenFiles: [String] = [], options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -368,7 +393,9 @@ extension NextcloudKit { let httpBody = requestBody.data(using: .utf8)! - search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { account, files, data, error in + search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in + taskHandler(task) + } completion: { account, files, data, error in options.queue.async { completion(account, files, data, error) } } } @@ -378,6 +405,7 @@ extension NextcloudKit { showHiddenFiles: Bool, includeHiddenFiles: [String], options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -404,7 +432,9 @@ extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -427,6 +457,7 @@ extension NextcloudKit { @objc public func setFavorite(fileName: String, favorite: Bool, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -453,7 +484,9 @@ extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).response(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -471,6 +504,7 @@ extension NextcloudKit { @objc public func listingFavorites(showHiddenFiles: Bool, includeHiddenFiles: [String] = [], options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -498,7 +532,9 @@ extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } @@ -520,6 +556,7 @@ extension NextcloudKit { @objc public func listingTrash(showHiddenFiles: Bool, options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ items: [NKTrash], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account @@ -547,7 +584,9 @@ extension NextcloudKit { return options.queue.async { completion(account, items, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } From 81e13181c8d7d42a8d5425caf5315bddc2c9b1de Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 21 Mar 2024 10:01:14 +0100 Subject: [PATCH 028/135] add filename in trash (#63) * add filename Signed-off-by: Marino Faggiana * disable Build and test Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- .github/workflows/{xcode.yml => xcode.xxx} | 0 Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) rename .github/workflows/{xcode.yml => xcode.xxx} (100%) diff --git a/.github/workflows/xcode.yml b/.github/workflows/xcode.xxx similarity index 100% rename from .github/workflows/xcode.yml rename to .github/workflows/xcode.xxx diff --git a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift index 0872a5fe..a1b332ec 100644 --- a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift @@ -554,7 +554,8 @@ extension NextcloudKit { } } - @objc public func listingTrash(showHiddenFiles: Bool, + @objc public func listingTrash(filename: String? = nil, + showHiddenFiles: Bool, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ items: [NKTrash], _ data: Data?, _ error: NKError) -> Void) { @@ -563,7 +564,10 @@ extension NextcloudKit { let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav - let serverUrlFileName = urlBase + "/" + dav + "/trashbin/" + userId + "/trash/" + var serverUrlFileName = urlBase + "/" + dav + "/trashbin/" + userId + "/trash/" + if let filename { + serverUrlFileName = serverUrlFileName + filename + } var items: [NKTrash] = [] guard let url = serverUrlFileName.encodedToUrl else { From 1e224f004cbd6d547480101e98af6a73f6c2c5d1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 18 Apr 2024 12:31:15 +0200 Subject: [PATCH 029/135] Nextcloud Assistant (#66) * getTextProcessingTaskTypes Signed-off-by: Marino Faggiana * getTextProcessingTaskTypes Signed-off-by: Marino Faggiana * NKTextProcessingTaskTypes Signed-off-by: Marino Faggiana * coding Signed-off-by: Marino Faggiana * coding Signed-off-by: Marino Faggiana * coding Signed-off-by: Marino Faggiana * coding Signed-off-by: Marino Faggiana * fix Signed-off-by: Marino Faggiana * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * Add init Signed-off-by: Milen Pivchev * Change model Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev --------- Signed-off-by: Marino Faggiana Signed-off-by: Milen Pivchev Co-authored-by: Milen Pivchev --- .../NextcloudKit/NextcloudKit+Assistant.swift | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 Sources/NextcloudKit/NextcloudKit+Assistant.swift diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift new file mode 100644 index 00000000..b76fe2d3 --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -0,0 +1,305 @@ +// +// NextcloudKit+Assistant.swift +// NextcloudKit +// +// Created by Marino Faggiana on 26/03/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation +import Alamofire +import SwiftyJSON + +extension NextcloudKit { + public func textProcessingGetTypes(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ types: [NKTextProcessingTaskType]?, _ data: Data?, _ error: NKError) -> Void) { + + let account = self.nkCommonInstance.account + let urlBase = self.nkCommonInstance.urlBase + + let endpoint = "ocs/v2.php/textprocessing/tasktypes" + + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let headers = self.nkCommonInstance.getStandardHeaders(options: options) + + sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, nil, nil, error) } + case .success(let jsonData): + let json = JSON(jsonData) + let data = json["ocs"]["data"]["types"] + let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError + if 200..<300 ~= statusCode { + let results = NKTextProcessingTaskType.factory(data: data) + options.queue.async { completion(account, results, jsonData, .success) } + } else { + options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + } + } + } + } + + public func textProcessingSchedule(input: String, + typeId: String, + appId: String = "assistant", + identifier: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { + + let account = self.nkCommonInstance.account + let urlBase = self.nkCommonInstance.urlBase + + let endpoint = "/ocs/v2.php/textprocessing/schedule" + + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let headers = self.nkCommonInstance.getStandardHeaders(options: options) + let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] + + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, nil, nil, error) } + case .success(let jsonData): + let json = JSON(jsonData) + let data = json["ocs"]["data"]["task"] + let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError + if 200..<300 ~= statusCode { + let result = NKTextProcessingTask.factory(data: data) + options.queue.async { completion(account, result, jsonData, .success) } + } else { + options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + } + } + } + } + + public func textProcessingGetTask(taskId: Int, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { + + let account = self.nkCommonInstance.account + let urlBase = self.nkCommonInstance.urlBase + + let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" + + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let headers = self.nkCommonInstance.getStandardHeaders(options: options) + + sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, nil, nil, error) } + case .success(let jsonData): + let json = JSON(jsonData) + let data = json["ocs"]["data"]["task"] + let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError + if 200..<300 ~= statusCode { + let result = NKTextProcessingTask.factory(data: data) + options.queue.async { completion(account, result, jsonData, .success) } + } else { + options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + } + } + } + } + + public func textProcessingDeleteTask(taskId: Int, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { + + let account = self.nkCommonInstance.account + let urlBase = self.nkCommonInstance.urlBase + + let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" + + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let headers = self.nkCommonInstance.getStandardHeaders(options: options) + + sessionManager.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, nil, nil, error) } + case .success(let jsonData): + let json = JSON(jsonData) + let data = json["ocs"]["data"]["task"] + let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError + if 200..<300 ~= statusCode { + let result = NKTextProcessingTask.factory(data: data) + options.queue.async { completion(account, result, jsonData, .success) } + } else { + options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + } + } + } + } + + public func textProcessingTaskList(appId: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ task: [NKTextProcessingTask]?, _ data: Data?, _ error: NKError) -> Void) { + + let account = self.nkCommonInstance.account + let urlBase = self.nkCommonInstance.urlBase + + let endpoint = "/ocs/v2.php/textprocessing/tasks/app/\(appId)" + + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let headers = self.nkCommonInstance.getStandardHeaders(options: options) + + sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, nil, nil, error) } + case .success(let jsonData): + let json = JSON(jsonData) + let data = json["ocs"]["data"]["tasks"] + let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError + if 200..<300 ~= statusCode { + let results = NKTextProcessingTask.factories(data: data) + options.queue.async { completion(account, results, jsonData, .success) } + } else { + options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + } + } + } + } +} + +public class NKTextProcessingTaskType { + public var id: String? + public var name: String? + public var description: String? + + public init(id: String? = nil, name: String? = nil, description: String? = nil) { + self.id = id + self.name = name + self.description = description + } + + init?(json: JSON) { + self.id = json["id"].string + self.name = json["name"].string + self.description = json["description"].string + } + + static func factory(data: JSON) -> [NKTextProcessingTaskType]? { + guard let allResults = data.array else { return nil } + return allResults.compactMap(NKTextProcessingTaskType.init) + } +} + +public class NKTextProcessingTask { + public var id: Int? + public var type: String? + public var status: Int? + public var userId: String? + public var appId: String? + public var input: String? + public var output: String? + public var identifier: String? + public var completionExpectedAt: Double? + + public init(id: Int? = nil, type: String? = nil, status: Int? = nil, userId: String? = nil, appId: String? = nil, input: String? = nil, output: String? = nil, identifier: String? = nil, completionExpectedAt: Double? = nil) { + self.id = id + self.type = type + self.status = status + self.userId = userId + self.appId = appId + self.input = input + self.output = output + self.identifier = identifier + self.completionExpectedAt = completionExpectedAt + } + + init?(json: JSON) { + self.id = json["id"].int + self.type = json["type"].string + self.status = json["status"].int + self.userId = json["userId"].string + self.appId = json["appId"].string + self.input = json["input"].string + self.output = json["output"].string + self.identifier = json["identifier"].string + self.completionExpectedAt = json["completionExpectedAt"].double + } + + static func factories(data: JSON) -> [NKTextProcessingTask]? { + guard let allResults = data.array else { return nil } + return allResults.compactMap(NKTextProcessingTask.init) + } + + static func factory(data: JSON) -> NKTextProcessingTask? { + NKTextProcessingTask.init(json: data) + } +} + From 73d245f3006be0471ffc94900abc96af09d5505e Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Mon, 22 Apr 2024 15:48:21 +0800 Subject: [PATCH 030/135] Add support for visionOS, fix tvOS and watchOS support (#67) * Add CoreServices import for visionOS Signed-off-by: Claudio Cambra * Fix screen scaling calculation when UIScreen is not available (for visionOS) Signed-off-by: Claudio Cambra * Fix availability of UIImage.resizeImage on non iOS platforms Signed-off-by: Claudio Cambra * Add visionOS target support to NextcloudKit Signed-off-by: Claudio Cambra * Bump watchOS version up, per dependency requirements Signed-off-by: Claudio Cambra * Remove reachability observation on watchOS, as this is unsupported in Alamofire Signed-off-by: Claudio Cambra * Remove free disk calculation on watchOS which cannot be done Signed-off-by: Claudio Cambra * Bump up tvOS version per dependencies Signed-off-by: Claudio Cambra * Also restrict free space calculation on tvOS, as unavailable Signed-off-by: Claudio Cambra --------- Signed-off-by: Claudio Cambra --- Package.swift | 7 +-- .../NextcloudKit/API/NextcloudKit+API.swift | 10 +++- .../Extensions/Image+Extension.swift | 53 +++++++++---------- Sources/NextcloudKit/NKCommon.swift | 2 + Sources/NextcloudKit/NextcloudKit.swift | 12 ++++- 5 files changed, 51 insertions(+), 33 deletions(-) diff --git a/Package.swift b/Package.swift index 4a2cb18f..a763f2c5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -8,8 +8,9 @@ let package = Package( platforms: [ .macOS(.v10_15), .iOS(.v10), - .tvOS(.v10), - .watchOS(.v3) + .tvOS(.v13), + .watchOS(.v6), + .visionOS(.v1) ], products: [ .library( diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/API/NextcloudKit+API.swift index 9ef8b4b9..7c824f36 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API.swift @@ -395,8 +395,14 @@ extension NextcloudKit { let contextRef = CGContext(data: nil, width: Int(rect.width), height: Int(rect.height), bitsPerComponent: image.cgImage!.bitsPerComponent, bytesPerRow: image.cgImage!.bytesPerRow, space: image.cgImage!.colorSpace!, bitmapInfo: image.cgImage!.bitmapInfo.rawValue) layerToMask.render(in: contextRef!) #else - let rect = CGRect(x: 0, y: 0, width: avatarSizeRounded / Int(UIScreen.main.scale), height: avatarSizeRounded / Int(UIScreen.main.scale)) - UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.main.scale) + + #if os(iOS) + let screenScale = UIScreen.main.scale + #else + let screenScale = 1.0 + #endif + let rect = CGRect(x: 0, y: 0, width: avatarSizeRounded / Int(screenScale), height: avatarSizeRounded / Int(screenScale)) + UIGraphicsBeginImageContextWithOptions(rect.size, false, screenScale) UIBezierPath(roundedRect: rect, cornerRadius: rect.size.height).addClip() imageAvatar?.draw(in: rect) imageAvatar = UIGraphicsGetImageFromCurrentImageContext() ?? image diff --git a/Sources/NextcloudKit/Extensions/Image+Extension.swift b/Sources/NextcloudKit/Extensions/Image+Extension.swift index a474d533..0d743573 100644 --- a/Sources/NextcloudKit/Extensions/Image+Extension.swift +++ b/Sources/NextcloudKit/Extensions/Image+Extension.swift @@ -23,33 +23,6 @@ // along with this program. If not, see . // -#if os(iOS) -import UIKit - -extension UIImage { - internal func resizeImage(size: CGSize, isAspectRation: Bool) -> UIImage? { - let originRatio = self.size.width / self.size.height - let newRatio = size.width / size.height - var newSize = size - - if isAspectRation { - if originRatio < newRatio { - newSize.height = size.height - newSize.width = size.height * originRatio - } else { - newSize.width = size.width - newSize.height = size.width / originRatio - } - } - - UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) - self.draw(in: CGRect(origin: .zero, size: newSize)) - defer { UIGraphicsEndImageContext() } - return UIGraphicsGetImageFromCurrentImageContext() - } -} -#endif - #if os(macOS) import Foundation import AppKit @@ -101,4 +74,30 @@ public extension NSImage { return nil } } +#else +import UIKit + +extension UIImage { + internal func resizeImage(size: CGSize, isAspectRation: Bool) -> UIImage? { + let originRatio = self.size.width / self.size.height + let newRatio = size.width / size.height + var newSize = size + + if isAspectRation { + if originRatio < newRatio { + newSize.height = size.height + newSize.width = size.height * originRatio + } else { + newSize.width = size.width + newSize.height = size.width / originRatio + } + } + + UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) + self.draw(in: CGRect(origin: .zero, size: newSize)) + defer { UIGraphicsEndImageContext() } + return UIGraphicsGetImageFromCurrentImageContext() + } +} #endif + diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 70e51bd1..6ac9b6de 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -26,6 +26,8 @@ import Alamofire #if os(iOS) import MobileCoreServices +#else +import CoreServices #endif @objc public protocol NKCommonDelegate { diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 920c5516..9f936b82 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -54,18 +54,24 @@ import SwiftyJSON return internalSessionManager } + #if !os(watchOS) private let reachabilityManager = Alamofire.NetworkReachabilityManager() + #endif // private var cookies: [String:[HTTPCookie]] = [:] @objc public let nkCommonInstance = NKCommon() override public init(fileManager: FileManager = .default) { super.init(fileManager: fileManager) + #if !os(watchOS) startNetworkReachabilityObserver() + #endif } deinit { + #if !os(watchOS) stopNetworkReachabilityObserver() + #endif } // MARK: - Setup @@ -161,6 +167,7 @@ import SwiftyJSON // MARK: - Reachability + #if !os(watchOS) @objc public func isNetworkReachable() -> Bool { return reachabilityManager?.isReachable ?? false } @@ -189,6 +196,7 @@ import SwiftyJSON reachabilityManager?.stopListening() } + #endif // MARK: - Session utility @@ -476,7 +484,7 @@ import SwiftyJSON return completion(account, nil, nil, nil, NKError(errorCode: NKError.chunkNoEnoughMemory)) } let freeDisk = ((fsAttributes[FileAttributeKey.systemFreeSize] ?? 0) as? Int64) ?? 0 - #else + #elseif os(visionOS) || os(iOS) var freeDisk: Int64 = 0 let fileURL = URL(fileURLWithPath: directory as String) do { @@ -487,11 +495,13 @@ import SwiftyJSON } catch { } #endif + #if os(visionOS) || os(iOS) if freeDisk < fileNameLocalSize * 4 { // It seems there is not enough space to send the file let error = NKError(errorCode: NKError.chunkNoEnoughMemory, errorDescription: "_chunk_enough_memory_") return completion(account, nil, nil, nil, error) } + #endif func createFolder(completion: @escaping (_ errorCode: NKError) -> Void) { From fdbe0dc8647f42190882790333ee03ae856a6884 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 7 May 2024 11:36:32 +0200 Subject: [PATCH 031/135] Task description (#70) * coding Signed-off-by: Marino Faggiana * task.taskDescription Signed-off-by: Marino Faggiana * DownloadingFinish Signed-off-by: Marino Faggiana * fix Signed-off-by: Marino Faggiana * improvements Signed-off-by: Marino Faggiana * fix Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- .../NextcloudKit/API/NextcloudKit+API.swift | 25 +++++++++-- .../NextcloudKit/E2EE/NextcloudKit+E2EE.swift | 11 +++++ Sources/NextcloudKit/NKCommon.swift | 7 +++- Sources/NextcloudKit/NKRequestOptions.swift | 3 ++ .../NextcloudKit/NextcloudKit+Assistant.swift | 5 +++ .../NextcloudKit/NextcloudKit+Comments.swift | 5 +++ .../NextcloudKit/NextcloudKit+Dashboard.swift | 2 + .../NextcloudKit/NextcloudKit+FilesLock.swift | 1 + .../NextcloudKit+Groupfolders.swift | 1 + .../NextcloudKit/NextcloudKit+Hovercard.swift | 1 + .../NextcloudKit/NextcloudKit+Livephoto.swift | 1 + Sources/NextcloudKit/NextcloudKit+Login.swift | 42 ++++++++++--------- .../NextcloudKit/NextcloudKit+NCText.swift | 4 ++ .../NextcloudKit+PushNotification.swift | 4 ++ .../NextcloudKit+Richdocuments.swift | 4 ++ .../NextcloudKit/NextcloudKit+Search.swift | 2 + Sources/NextcloudKit/NextcloudKit+Share.swift | 7 ++++ .../NextcloudKit+UserStatus.swift | 7 ++++ Sources/NextcloudKit/NextcloudKit.swift | 2 + .../NextcloudKit/NextcloudKitBackground.swift | 23 ++++------ .../WebDAV/NextcloudKit+WebDAV.swift | 10 +++++ 21 files changed, 126 insertions(+), 41 deletions(-) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/API/NextcloudKit+API.swift index 7c824f36..36378405 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API.swift @@ -33,15 +33,16 @@ import SwiftyJSON extension NextcloudKit { @objc public func checkServer(serverUrl: String, - queue: DispatchQueue = .main, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ error: NKError) -> Void) { guard let url = serverUrl.asUrl else { - return queue.async { completion(.urlError) } + return options.queue.async { completion(.urlError) } } sessionManager.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -51,9 +52,9 @@ extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - queue.async { completion(error) } + options.queue.async { completion(error) } case .success: - queue.async { completion(.success) } + options.queue.async { completion(.success) } } } } @@ -78,6 +79,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -113,6 +115,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -177,6 +180,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -230,6 +234,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -297,6 +302,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -358,6 +364,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -442,6 +449,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -480,6 +488,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -556,6 +565,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -597,6 +607,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -634,6 +645,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -694,6 +706,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -776,6 +789,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -864,6 +878,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -904,6 +919,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -950,6 +966,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift index 75470434..333f53ff 100644 --- a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift @@ -51,6 +51,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -108,6 +109,7 @@ extension NextcloudKit { } sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -158,6 +160,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -220,6 +223,7 @@ extension NextcloudKit { } sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -276,6 +280,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -324,6 +329,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -367,6 +373,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -413,6 +420,7 @@ extension NextcloudKit { let parameters = ["csr": certificate] sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -460,6 +468,7 @@ extension NextcloudKit { let parameters = ["privateKey": privateKey] sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -503,6 +512,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -539,6 +549,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 6ac9b6de..c8550e83 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -39,8 +39,11 @@ import CoreServices @objc optional func downloadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) @objc optional func uploadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) - @objc optional func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: NSDate?, dateLastModified: NSDate?, length: Int64, fileNameLocalPath: String?, task: URLSessionTask, error: NKError) - @objc optional func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: NSDate?, size: Int64, fileNameLocalPath: String?, task: URLSessionTask, error: NKError) + + @objc optional func downloadingFinish(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) + + @objc optional func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: NSDate?, dateLastModified: NSDate?, length: Int64, task: URLSessionTask, error: NKError) + @objc optional func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: NSDate?, size: Int64, task: URLSessionTask, error: NKError) } @objc public class NKCommon: NSObject { diff --git a/Sources/NextcloudKit/NKRequestOptions.swift b/Sources/NextcloudKit/NKRequestOptions.swift index 7f6ccc32..961829d7 100644 --- a/Sources/NextcloudKit/NKRequestOptions.swift +++ b/Sources/NextcloudKit/NKRequestOptions.swift @@ -33,6 +33,7 @@ public class NKRequestOptions: NSObject { var contentType: String? var e2eToken: String? var timeout: TimeInterval + var taskDescription: String? var queue: DispatchQueue public init(endpoint: String? = nil, @@ -42,6 +43,7 @@ public class NKRequestOptions: NSObject { contentType: String? = nil, e2eToken: String? = nil, timeout: TimeInterval = 60, + taskDescription: String? = nil, queue: DispatchQueue = .main) { self.endpoint = endpoint @@ -51,6 +53,7 @@ public class NKRequestOptions: NSObject { self.contentType = contentType self.e2eToken = e2eToken self.timeout = timeout + self.taskDescription = taskDescription self.queue = queue } } diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index b76fe2d3..900e4fc1 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -42,6 +42,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -87,6 +88,7 @@ extension NextcloudKit { let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -128,6 +130,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -169,6 +172,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -210,6 +214,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index fedd6639..447e22e1 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -52,6 +52,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -100,6 +101,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -145,6 +147,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -179,6 +182,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -222,6 +226,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 4b1ed83c..205f6f13 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -50,6 +50,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -99,6 +100,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index e91b5583..f3faa608 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -48,6 +48,7 @@ extension NextcloudKit { headers.update(name: "X-User-Lock", value: "1") sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 6ebe9ca8..8ebb27ed 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -44,6 +44,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index 27b4600d..9b858c16 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -46,6 +46,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 4f862051..1c79250a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -51,6 +51,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index f590db9e..52ef2468 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -33,14 +33,14 @@ extension NextcloudKit { username: String, password: String, userAgent: String? = nil, - queue: DispatchQueue = .main, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/getapppassword" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { - return queue.async { completion(nil, nil, .urlError) } + return options.queue.async { completion(nil, nil, .urlError) } } var headers: HTTPHeaders = [.authorization(username: username, password: password)] @@ -53,10 +53,11 @@ extension NextcloudKit { do { try urlRequest = URLRequest(url: url, method: HTTPMethod(rawValue: "GET"), headers: headers) } catch { - return queue.async { completion(nil, nil, NKError(error: error)) } + return options.queue.async { completion(nil, nil, NKError(error: error)) } } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -66,13 +67,13 @@ extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - queue.async { completion(nil, nil, error) } + options.queue.async { completion(nil, nil, error) } case .success(let xmlData): if let data = xmlData { let apppassword = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataAppPassword(data: data) - queue.async { completion(apppassword, xmlData, .success) } + options.queue.async { completion(apppassword, xmlData, .success) } } else { - queue.async { completion(nil, nil, .xmlError) } + options.queue.async { completion(nil, nil, .xmlError) } } } } @@ -82,14 +83,14 @@ extension NextcloudKit { username: String, password: String, userAgent: String? = nil, - queue: DispatchQueue = .main, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/apppassword" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { - return queue.async { completion(nil, .urlError) } + return options.queue.async { completion(nil, .urlError) } } var headers: HTTPHeaders = [.authorization(username: username, password: password)] @@ -102,10 +103,11 @@ extension NextcloudKit { do { try urlRequest = URLRequest(url: url, method: HTTPMethod(rawValue: "DELETE"), headers: headers) } catch { - return queue.async { completion(nil, NKError(error: error)) } + return options.queue.async { completion(nil, NKError(error: error)) } } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -115,9 +117,9 @@ extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - queue.async { completion(nil, error) } + options.queue.async { completion(nil, error) } case .success(let xmlData): - queue.async { completion(xmlData, .success) } + options.queue.async { completion(xmlData, .success) } } } } @@ -126,14 +128,14 @@ extension NextcloudKit { @objc public func getLoginFlowV2(serverUrl: String, userAgent: String? = nil, - queue: DispatchQueue = .main, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ endpoint: String?, _ login: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "index.php/login/v2" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { - return queue.async { completion(nil, nil, nil, nil, .urlError) } + return options.queue.async { completion(nil, nil, nil, nil, .urlError) } } var headers: HTTPHeaders? @@ -142,6 +144,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -151,7 +154,7 @@ extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - queue.async { completion(nil, nil, nil, nil, error) } + options.queue.async { completion(nil, nil, nil, nil, error) } case .success(let jsonData): let json = JSON(jsonData) @@ -159,7 +162,7 @@ extension NextcloudKit { let endpoint = json["poll"]["endpoint"].string let login = json["login"].string - queue.async { completion(token, endpoint, login, jsonData, .success) } + options.queue.async { completion(token, endpoint, login, jsonData, .success) } } } } @@ -167,14 +170,14 @@ extension NextcloudKit { @objc public func getLoginFlowV2Poll(token: String, endpoint: String, userAgent: String? = nil, - queue: DispatchQueue = .main, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ server: String?, _ loginName: String?, _ appPassword: String?, _ data: Data?, _ error: NKError) -> Void) { let serverUrl = endpoint + "?token=" + token guard let url = serverUrl.asUrl else { - return queue.async { completion(nil, nil, nil, nil, .urlError) } + return options.queue.async { completion(nil, nil, nil, nil, .urlError) } } var headers: HTTPHeaders? @@ -183,6 +186,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -192,7 +196,7 @@ extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - queue.async { completion(nil, nil, nil, nil, error) } + options.queue.async { completion(nil, nil, nil, nil, error) } case .success(let jsonData): let json = JSON(jsonData) @@ -200,7 +204,7 @@ extension NextcloudKit { let loginName = json["loginName"].string let appPassword = json["appPassword"].string - queue.async { completion(server, loginName, appPassword, jsonData, .success) } + options.queue.async { completion(server, loginName, appPassword, jsonData, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index d6684371..c7f5b096 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -46,6 +46,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -122,6 +123,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -158,6 +160,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -218,6 +221,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index 5307bf00..134e9b2c 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -53,6 +53,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -95,6 +96,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -137,6 +139,7 @@ extension NextcloudKit { let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -177,6 +180,7 @@ extension NextcloudKit { let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) sessionManager.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index a9119889..d2574b06 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -46,6 +46,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -85,6 +86,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -140,6 +142,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -181,6 +184,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index 5fca06fe..fda46024 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -65,6 +65,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) let requestUnifiedSearch = sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -167,6 +168,7 @@ extension NextcloudKit { } let requestSearchProvider = sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index d792266d..f52a1d43 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -95,6 +95,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -165,6 +166,7 @@ extension NextcloudKit { ] sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -265,6 +267,7 @@ extension NextcloudKit { completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { createShare(path: path, shareType: 3, shareWith: nil, publicUpload: publicUpload, hideDownload: hideDownload, password: password, permissions: permissions, options: options) { task in + task.taskDescription = options.taskDescription taskHandler(task) } completion: { account, share, data, error in completion(account, share, data, error) @@ -283,6 +286,7 @@ extension NextcloudKit { completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { createShare(path: path, shareType: shareType, shareWith: shareWith, publicUpload: false, note: note, hideDownload: false, password: password, permissions: permissions, attributes: attributes, options: options) { task in + task.taskDescription = options.taskDescription taskHandler(task) } completion: { account, share, data, error in completion(account, share, data, error) @@ -338,6 +342,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -428,6 +433,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -471,6 +477,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index b4e3f14b..3be34d80 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -47,6 +47,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -103,6 +104,7 @@ extension NextcloudKit { ] sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -150,6 +152,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -202,6 +205,7 @@ extension NextcloudKit { } sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -241,6 +245,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -281,6 +286,7 @@ extension NextcloudKit { let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -346,6 +352,7 @@ extension NextcloudKit { ] sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 9f936b82..ab4d4fb1 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -281,6 +281,7 @@ import SwiftyJSON let request = sessionManager.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } } .downloadProgress { progress in @@ -386,6 +387,7 @@ import SwiftyJSON let request = sessionManager.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in + task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } }) .uploadProgress { progress in diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index f202317b..e6993a22 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -35,6 +35,7 @@ import Foundation @objc public func download(serverUrlFileName: Any, fileNameLocalPath: String, + taskDescription: String? = nil, session: URLSession) -> URLSessionDownloadTask? { var url: URL? @@ -58,7 +59,7 @@ import Foundation request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") let task = session.downloadTask(with: request) - task.taskDescription = fileNameLocalPath + task.taskDescription = taskDescription task.resume() self.nkCommonInstance.writeLog("Network start download file: \(serverUrlFileName)") @@ -71,6 +72,7 @@ import Foundation fileNameLocalPath: String, dateCreationFile: Date?, dateModificationFile: Date?, + taskDescription: String? = nil, session: URLSession) -> URLSessionUploadTask? { var url: URL? @@ -105,7 +107,7 @@ import Foundation } let task = session.uploadTask(with: request, fromFile: URL(fileURLWithPath: fileNameLocalPath)) - task.taskDescription = fileNameLocalPath + task.taskDescription = taskDescription task.resume() self.nkCommonInstance.writeLog("Network start upload file: \(serverUrlFileName)") @@ -126,18 +128,7 @@ import Foundation } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { - - if let httpResponse = (downloadTask.response as? HTTPURLResponse) { - if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 { - if let destinationFilePath = downloadTask.taskDescription { - let destinationUrl = NSURL.fileURL(withPath: destinationFilePath) - do { - try FileManager.default.removeItem(at: destinationUrl) - try FileManager.default.copyItem(at: location, to: destinationUrl) - } catch { } - } - } - } + self.nkCommonInstance.delegate?.downloadingFinish?(session, downloadTask: downloadTask, didFinishDownloadingTo: location) } public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { @@ -198,9 +189,9 @@ import Foundation } if task is URLSessionDownloadTask { - self.nkCommonInstance.delegate?.downloadComplete?(fileName: fileName, serverUrl: serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, fileNameLocalPath: task.taskDescription, task: task, error: nkError) + self.nkCommonInstance.delegate?.downloadComplete?(fileName: fileName, serverUrl: serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, task: task, error: nkError) } else if task is URLSessionUploadTask { - self.nkCommonInstance.delegate?.uploadComplete?(fileName: fileName, serverUrl: serverUrl, ocId: ocId, etag: etag, date: date, size: task.countOfBytesExpectedToSend, fileNameLocalPath: task.taskDescription, task: task, error: nkError) + self.nkCommonInstance.delegate?.uploadComplete?(fileName: fileName, serverUrl: serverUrl, ocId: ocId, etag: etag, date: date, size: task.countOfBytesExpectedToSend, task: task, error: nkError) } if nkError.errorCode == 0 { diff --git a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift index a1b332ec..124ef509 100644 --- a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift @@ -51,6 +51,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -98,6 +99,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -146,6 +148,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -194,6 +197,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -256,6 +260,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -301,6 +306,7 @@ extension NextcloudKit { } search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], options: options) { task in + task.taskDescription = options.taskDescription taskHandler(task) } completion: { account, files, data, error in options.queue.async { completion(account, files.first, data, error) } @@ -433,6 +439,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -485,6 +492,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -533,6 +541,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -589,6 +598,7 @@ extension NextcloudKit { } sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { From 31c85f4283594113480e2433d8aa40528fd244e9 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 15 May 2024 15:21:36 +0200 Subject: [PATCH 032/135] change TypeIconFile (#71) Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index c8550e83..164be534 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -71,19 +71,19 @@ import CoreServices } public enum TypeIconFile: String { - case audio = "file_audio" - case code = "file_code" - case compress = "file_compress" + case audio = "audio" + case code = "code" + case compress = "compress" case directory = "directory" case document = "document" - case image = "file_photo" - case movie = "file_movie" - case pdf = "file_pdf" - case ppt = "file_ppt" - case txt = "file_txt" + case image = "image" + case movie = "movie" + case pdf = "pdf" + case ppt = "ppt" + case txt = "txt" case unknow = "file" case url = "url" - case xls = "file_xls" + case xls = "xls" } public struct UTTypeConformsToServer { From 99c5a273f2327c1763bee55d2a738d5052004faf Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 21 Jun 2024 11:16:12 +0200 Subject: [PATCH 033/135] Core Preview Improvements and use only fileId Signed-off-by: Marino Faggiana --- .../NextcloudKit/API/NextcloudKit+API.swift | 84 ++++++++++++------- .../API/NextcloudKit+API_Async.swift | 14 ++-- 2 files changed, 64 insertions(+), 34 deletions(-) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/API/NextcloudKit+API.swift index 36378405..f236580b 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API.swift @@ -255,40 +255,68 @@ extension NextcloudKit { } } - @objc public func downloadPreview(fileNamePathOrFileId: String, - fileNamePreviewLocalPath: String, - widthPreview: Int, - heightPreview: Int, - fileNameIconLocalPath: String? = nil, - sizeIcon: Int = 0, - etag: String? = nil, - endpointTrashbin: Bool = false, - useInternalEndpoint: Bool = true, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { + public func downloadPreview(fileId: String, + fileNamePreviewLocalPath: String, + fileNameIconLocalPath: String? = nil, + widthPreview: Int, + heightPreview: Int, + sizeIcon: Int, + etag: String? = nil, + crop: Int = 0, + cropMode: String = "fill", + forceIcon: Int = 1, + mimeFallback: Int = 0, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account - let urlBase = self.nkCommonInstance.urlBase - var endpoint = "" - var url: URLConvertible? + let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - if useInternalEndpoint { + downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, etag: etag, endpoint: endpoint, options: options) { task in + taskHandler(task) + } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in + completion(account, imagePreview, imageIcon, imageOriginal,etag,error) + } + } - if endpointTrashbin { - endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileNamePathOrFileId)&x=\(widthPreview)&y=\(heightPreview)" - } else { - guard let fileNamePath = fileNamePathOrFileId.urlEncoded else { - return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } - } - endpoint = "index.php/core/preview.png?file=\(fileNamePath)&x=\(widthPreview)&y=\(heightPreview)&a=1&mode=cover" - } + public func downloadTrashPreview(fileId: String, + fileNamePreviewLocalPath: String, + fileNameIconLocalPath: String, + widthPreview: Int, + heightPreview: Int, + sizeIcon: Int, + crop: Int = 0, + cropMode: String = "fill", + forceIcon: Int = 1, + mimeFallback: Int = 0, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - } else { + downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, etag: nil, endpoint: endpoint, options: options) { task in + taskHandler(task) + } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in + completion(account, imagePreview, imageIcon, imageOriginal,etag,error) + } + } - url = fileNamePathOrFileId.asUrl + private func downloadPreview(fileNamePreviewLocalPath: String, + fileNameIconLocalPath: String? = nil, + sizeIcon: Int = 0, + etag: String? = nil, + endpoint: String?, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { + + let account = self.nkCommonInstance.account + let urlBase = self.nkCommonInstance.urlBase + var url: URLConvertible? + + if let endpoint { + url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) } guard let urlRequest = url else { diff --git a/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift b/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift index 20e8b413..a591a23e 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift @@ -51,19 +51,21 @@ extension NextcloudKit { }) } - public func downloadPreview(fileNamePathOrFileId: String, + public func downloadPreview(fileId: String, fileNamePreviewLocalPath: String, + fileNameIconLocalPath: String? = nil, widthPreview: Int, heightPreview: Int, - fileNameIconLocalPath: String? = nil, - sizeIcon: Int = 0, + sizeIcon: Int, etag: String? = nil, - endpointTrashbin: Bool = false, - useInternalEndpoint: Bool = true, + crop: Int = 0, + cropMode: String = "fill", + forceIcon: Int = 1, + mimeFallback: Int = 0, options: NKRequestOptions = NKRequestOptions()) async -> (account: String, imagePreview: UIImage?, imageIcon: UIImage?, imageOriginal: UIImage?, etag: String?, error: NKError) { await withUnsafeContinuation({ continuation in - downloadPreview(fileNamePathOrFileId: fileNamePathOrFileId, fileNamePreviewLocalPath: fileNamePreviewLocalPath, widthPreview: widthPreview, heightPreview: heightPreview, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, etag: etag, options: options) { account, imagePreview, imageIcon, imageOriginal, etag, error in + downloadPreview(fileId: fileId, fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, widthPreview: widthPreview, heightPreview: heightPreview, sizeIcon: sizeIcon, etag: etag, crop: crop, cropMode: cropMode, forceIcon: forceIcon, mimeFallback: mimeFallback, options: options) { account, imagePreview, imageIcon, imageOriginal, etag, error in continuation.resume(returning: (account: account, imagePreview: imagePreview, imageIcon: imageIcon, imageOriginal: imageOriginal, etag: etag, error: error)) } }) From 6814775d24d11ad37a889346aad4cb4700bf73b7 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 21 Jun 2024 11:34:10 +0200 Subject: [PATCH 034/135] fix Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/API/NextcloudKit+API.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/API/NextcloudKit+API.swift index f236580b..1bea7e52 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API.swift @@ -270,7 +270,7 @@ extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, etag: etag, endpoint: endpoint, options: options) { task in taskHandler(task) @@ -293,7 +293,7 @@ extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, etag: nil, endpoint: endpoint, options: options) { task in taskHandler(task) From 28f155436fba4a266e4ea546ae34bc601bcf2f7d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 21 Jun 2024 11:48:05 +0200 Subject: [PATCH 035/135] improvements Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/API/NextcloudKit+API.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/API/NextcloudKit+API.swift index 1bea7e52..4545d2e8 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API.swift @@ -258,9 +258,9 @@ extension NextcloudKit { public func downloadPreview(fileId: String, fileNamePreviewLocalPath: String, fileNameIconLocalPath: String? = nil, - widthPreview: Int, - heightPreview: Int, - sizeIcon: Int, + widthPreview: Int = 512, + heightPreview: Int = 512, + sizeIcon: Int = 512, etag: String? = nil, crop: Int = 0, cropMode: String = "fill", @@ -282,9 +282,9 @@ extension NextcloudKit { public func downloadTrashPreview(fileId: String, fileNamePreviewLocalPath: String, fileNameIconLocalPath: String, - widthPreview: Int, - heightPreview: Int, - sizeIcon: Int, + widthPreview: Int = 512, + heightPreview: Int = 512, + sizeIcon: Int = 512, crop: Int = 0, cropMode: String = "fill", forceIcon: Int = 1, From 35d4460b0b868d988625ed6be2860e6e0a19b11f Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 21 Jun 2024 11:48:49 +0200 Subject: [PATCH 036/135] Improvements Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/API/NextcloudKit+API_Async.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift b/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift index a591a23e..c0d7f981 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift @@ -54,9 +54,9 @@ extension NextcloudKit { public func downloadPreview(fileId: String, fileNamePreviewLocalPath: String, fileNameIconLocalPath: String? = nil, - widthPreview: Int, - heightPreview: Int, - sizeIcon: Int, + widthPreview: Int = 512, + heightPreview: Int = 512, + sizeIcon: Int = 512, etag: String? = nil, crop: Int = 0, cropMode: String = "fill", From 3b3b1783d0658e482565e475c33555527a507b49 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 23 Jun 2024 11:39:40 +0200 Subject: [PATCH 037/135] added compressionQuality Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/API/NextcloudKit+API.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/API/NextcloudKit+API.swift index 4545d2e8..11bf82bb 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/API/NextcloudKit+API.swift @@ -261,6 +261,8 @@ extension NextcloudKit { widthPreview: Int = 512, heightPreview: Int = 512, sizeIcon: Int = 512, + compressionQualityPreview: CGFloat = 0.5, + compressionQualityIcon: CGFloat = 0.5, etag: String? = nil, crop: Int = 0, cropMode: String = "fill", @@ -272,7 +274,7 @@ extension NextcloudKit { let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, etag: etag, endpoint: endpoint, options: options) { task in + downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: etag, endpoint: endpoint, options: options) { task in taskHandler(task) } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in completion(account, imagePreview, imageIcon, imageOriginal,etag,error) @@ -285,6 +287,8 @@ extension NextcloudKit { widthPreview: Int = 512, heightPreview: Int = 512, sizeIcon: Int = 512, + compressionQualityPreview: CGFloat = 0.5, + compressionQualityIcon: CGFloat = 0.5, crop: Int = 0, cropMode: String = "fill", forceIcon: Int = 1, @@ -295,7 +299,7 @@ extension NextcloudKit { let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, etag: nil, endpoint: endpoint, options: options) { task in + downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: nil, endpoint: endpoint, options: options) { task in taskHandler(task) } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in completion(account, imagePreview, imageIcon, imageOriginal,etag,error) @@ -305,6 +309,8 @@ extension NextcloudKit { private func downloadPreview(fileNamePreviewLocalPath: String, fileNameIconLocalPath: String? = nil, sizeIcon: Int = 0, + compressionQualityPreview: CGFloat, + compressionQualityIcon: CGFloat, etag: String? = nil, endpoint: String?, options: NKRequestOptions = NKRequestOptions(), @@ -348,13 +354,13 @@ extension NextcloudKit { let etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields)?.replacingOccurrences(of: "\"", with: "") var imagePreview, imageIcon: UIImage? do { - if let data = imageOriginal.jpegData(compressionQuality: 0.5) { + if let data = imageOriginal.jpegData(compressionQuality: compressionQualityPreview) { try data.write(to: URL(fileURLWithPath: fileNamePreviewLocalPath), options: .atomic) imagePreview = UIImage(data: data) } if fileNameIconLocalPath != nil && sizeIcon > 0 { imageIcon = imageOriginal.resizeImage(size: CGSize(width: sizeIcon, height: sizeIcon), isAspectRation: true) - if let data = imageIcon?.jpegData(compressionQuality: 0.5) { + if let data = imageIcon?.jpegData(compressionQuality: compressionQualityIcon) { try data.write(to: URL(fileURLWithPath: fileNameIconLocalPath!), options: .atomic) imageIcon = UIImage(data: data)! } From 064cf0561ba8a3eada3ffc8fd5e2d1ddf5142379 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 2 Jul 2024 10:09:42 +0200 Subject: [PATCH 038/135] Improvements (#76) Signed-off-by: Marino Faggiana --- AUTHORS | 2 + COPYING.iOS | 13 + LICENSE.txt | 674 ++++++++++++++++++ Package.resolved | 78 +- .../API/NextcloudKit+API_Async.swift | 92 --- .../E2EE/NextcloudKit+E2EE_Async.swift | 144 ---- .../Extensions/String+Extension.swift | 1 - Sources/NextcloudKit/NKCommon.swift | 92 +-- Sources/NextcloudKit/NKError.swift | 3 - Sources/NextcloudKit/NKModel.swift | 632 ++++++++-------- Sources/NextcloudKit/NKRequestOptions.swift | 5 +- Sources/NextcloudKit/NKShareAccounts.swift | 22 +- .../{API => }/NextcloudKit+API.swift | 328 ++++----- .../NextcloudKit/NextcloudKit+Assistant.swift | 75 +- .../NextcloudKit/NextcloudKit+Comments.swift | 74 +- .../NextcloudKit/NextcloudKit+Dashboard.swift | 68 +- .../{E2EE => }/NextcloudKit+E2EE.swift | 162 ++--- .../NextcloudKit/NextcloudKit+FilesLock.swift | 19 +- .../NextcloudKit+Groupfolders.swift | 33 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 42 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 31 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 77 +- .../NextcloudKit/NextcloudKit+NCText.swift | 68 +- .../NextcloudKit+PushNotification.swift | 86 +-- .../NextcloudKit+Richdocuments.swift | 61 +- .../NextcloudKit/NextcloudKit+Search.swift | 94 +-- Sources/NextcloudKit/NextcloudKit+Share.swift | 260 +++---- .../NextcloudKit+UserStatus.swift | 122 +--- .../{WebDAV => }/NextcloudKit+WebDAV.swift | 237 +++--- Sources/NextcloudKit/NextcloudKit.swift | 157 +--- .../NextcloudKit/NextcloudKitBackground.swift | 51 +- .../WebDAV/NextcloudKit+WebDAV_Async.swift | 101 --- 32 files changed, 1774 insertions(+), 2130 deletions(-) create mode 100644 AUTHORS create mode 100644 COPYING.iOS create mode 100644 LICENSE.txt delete mode 100644 Sources/NextcloudKit/API/NextcloudKit+API_Async.swift delete mode 100644 Sources/NextcloudKit/E2EE/NextcloudKit+E2EE_Async.swift rename Sources/NextcloudKit/{API => }/NextcloudKit+API.swift (82%) rename Sources/NextcloudKit/{E2EE => }/NextcloudKit+E2EE.swift (83%) rename Sources/NextcloudKit/{WebDAV => }/NextcloudKit+WebDAV.swift (77%) delete mode 100644 Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV_Async.swift diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..74656bbd --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Marino Faggiana +All contributors can be viewed at https://github.com/nextcloud/NextcloudKit/graphs/contributors diff --git a/COPYING.iOS b/COPYING.iOS new file mode 100644 index 00000000..534f7b9d --- /dev/null +++ b/COPYING.iOS @@ -0,0 +1,13 @@ +The NextcloudKit developers are aware that the terms of service that +apply to apps distributed via Apple's App Store services may conflict +with rights granted under the NextcloudKit license, the GNU General +Public License, version 3 or (at your option) any later version. The +copyright holders of the NextcloudKit do not wish this conflict +to prevent the otherwise-compliant distribution of derived apps via +the App Store. Therefore, we have committed not to pursue any license +violation that results solely from the conflict between the GNU GPLv3 +or any later version and the Apple App Store terms of service. In +other words, as long as you comply with the GPL in all other respects, +including its requirements to provide users with source code and the +text of the license, we will not object to your distribution of the +NextcloudKit through the App Store. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..9cecc1d4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Package.resolved b/Package.resolved index dd082463..bff4ffdc 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,43 +1,41 @@ { - "object": { - "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire", - "state": { - "branch": null, - "revision": "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", - "version": "5.8.1" - } - }, - { - "package": "Mocker", - "repositoryURL": "https://github.com/WeTransfer/Mocker.git", - "state": { - "branch": null, - "revision": "5d86f27a8f80d4ba388bc1a379a3c2289a1f3d18", - "version": "2.6.0" - } - }, - { - "package": "SwiftyJSON", - "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON", - "state": { - "branch": null, - "revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", - "version": "5.0.1" - } - }, - { - "package": "SwiftyXMLParser", - "repositoryURL": "https://github.com/yahoojapan/SwiftyXMLParser", - "state": { - "branch": null, - "revision": "d7a1d23f04c86c1cd2e8f19247dd15d74e0ea8be", - "version": "5.6.0" - } + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", + "version" : "5.9.1" } - ] - }, - "version": 1 + }, + { + "identity" : "mocker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WeTransfer/Mocker.git", + "state" : { + "revision" : "5d86f27a8f80d4ba388bc1a379a3c2289a1f3d18", + "version" : "2.6.0" + } + }, + { + "identity" : "swiftyjson", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftyJSON/SwiftyJSON", + "state" : { + "revision" : "af76cf3ef710b6ca5f8c05f3a31307d44a3c5828", + "version" : "5.0.2" + } + }, + { + "identity" : "swiftyxmlparser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/yahoojapan/SwiftyXMLParser", + "state" : { + "revision" : "d7a1d23f04c86c1cd2e8f19247dd15d74e0ea8be", + "version" : "5.6.0" + } + } + ], + "version" : 2 } diff --git a/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift b/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift deleted file mode 100644 index c0d7f981..00000000 --- a/Sources/NextcloudKit/API/NextcloudKit+API_Async.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// NextcloudKit+API_Async.swift -// NextcloudKit -// -// Created by Marino Faggiana on 19/10/22. -// -// Copyright © 2022 Marino Faggiana. All rights reserved. -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -#if os(macOS) -import Foundation -import AppKit -#else -import UIKit -#endif - -@available(iOS 13.0, *) -extension NextcloudKit { - - public func getServerStatus(serverUrl: String, - options: NKRequestOptions = NKRequestOptions()) async -> (ServerInfoResult) { - - await withUnsafeContinuation({ continuation in - getServerStatus(serverUrl: serverUrl) { serverInfoResult in - continuation.resume(returning: serverInfoResult) - } - }) - } - - public func getPreview(url: URL, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - getPreview(url: url, options: options) { account, data, error in - continuation.resume(returning: (account: account, data: data, error: error)) - } - }) - } - - public func downloadPreview(fileId: String, - fileNamePreviewLocalPath: String, - fileNameIconLocalPath: String? = nil, - widthPreview: Int = 512, - heightPreview: Int = 512, - sizeIcon: Int = 512, - etag: String? = nil, - crop: Int = 0, - cropMode: String = "fill", - forceIcon: Int = 1, - mimeFallback: Int = 0, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, imagePreview: UIImage?, imageIcon: UIImage?, imageOriginal: UIImage?, etag: String?, error: NKError) { - - await withUnsafeContinuation({ continuation in - downloadPreview(fileId: fileId, fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, widthPreview: widthPreview, heightPreview: heightPreview, sizeIcon: sizeIcon, etag: etag, crop: crop, cropMode: cropMode, forceIcon: forceIcon, mimeFallback: mimeFallback, options: options) { account, imagePreview, imageIcon, imageOriginal, etag, error in - continuation.resume(returning: (account: account, imagePreview: imagePreview, imageIcon: imageIcon, imageOriginal: imageOriginal, etag: etag, error: error)) - } - }) - } - - public func getUserProfile(options: NKRequestOptions = NKRequestOptions()) async -> (account: String, userProfile: NKUserProfile?, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - getUserProfile(options: options) { account, userProfile, data, error in - continuation.resume(returning: (account: account, userProfile: userProfile, data: data, error: error)) - } - }) - } - - public func sendClientDiagnosticsRemoteOperation(data: Data, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { - - await withUnsafeContinuation({ continuation in - sendClientDiagnosticsRemoteOperation(data: data, options: options) { account, error in - continuation.resume(returning: (account: account, error: error)) - } - }) - } -} diff --git a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE_Async.swift b/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE_Async.swift deleted file mode 100644 index 06e2a6ff..00000000 --- a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE_Async.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// NextcloudKit+E2EE_Async.swift -// NextcloudKit -// -// Created by Marino Faggiana on 19/10/22. -// -// Copyright © 2022 Marino Faggiana. All rights reserved. -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import Foundation - -@available(iOS 13.0, *) -extension NextcloudKit { - - public func markE2EEFolder(fileId: String, - delete: Bool, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { - - await withUnsafeContinuation({ continuation in - markE2EEFolder(fileId: fileId, delete: delete, options: options) { account, error in - - continuation.resume(returning: (account: account, error: error)) - } - }) - } - - @discardableResult - public func lockE2EEFolder(fileId: String, - e2eToken: String?, - e2eCounter: String?, - method: String, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, e2eToken: String?, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - lockE2EEFolder(fileId: fileId, e2eToken: e2eToken, e2eCounter: e2eCounter, method: method, options: options) { account, e2eToken, data, error in - continuation.resume(returning: (account: account, e2eToken: e2eToken, data: data, error: error)) - } - }) - } - - public func getE2EEMetadata(fileId: String, - e2eToken: String?, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, e2eMetadata: String?, signature: String?, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - getE2EEMetadata(fileId: fileId, e2eToken: e2eToken, options: options) { account, e2eMetadata, signature, data, error in - continuation.resume(returning: (account: account, e2eMetadata: e2eMetadata, signature: signature, data: data, error: error)) - } - }) - } - - public func putE2EEMetadata(fileId: String, - e2eToken: String, - e2eMetadata: String?, - signature: String?, - method: String, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, metadata: String?, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - putE2EEMetadata(fileId: fileId, e2eToken: e2eToken, e2eMetadata: e2eMetadata, signature: signature, method: method, options: options) { account, metadata, data, error in - continuation.resume(returning: (account: account, metadata: metadata, data: data, error: error)) - } - }) - } - - public func getE2EECertificate(user: String? = nil, options: NKRequestOptions = NKRequestOptions()) async -> (account: String, certificate: String?, certificateUser: String?, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - getE2EECertificate(user: user, options: options) { account, certificate, certificateUser, data, error in - continuation.resume(returning: (account: account, certificate: certificate, certificateUser: certificateUser, data: data, error: error)) - } - }) - } - - public func getE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions()) async -> (account: String, privateKey: String?, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - getE2EEPrivateKey(options: options) { account, privateKey, data, error in - continuation.resume(returning: (account: account, privateKey: privateKey, data: data, error: error)) - } - }) - } - - public func getE2EEPublicKey(options: NKRequestOptions = NKRequestOptions()) async -> (account: String, publicKey: String?, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - getE2EEPublicKey(options: options) { account, publicKey, data, error in - continuation.resume(returning: (account: account, publicKey: publicKey, data: data, error: error)) - } - }) - } - - public func signE2EECertificate(certificate: String, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, certificate: String?, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - signE2EECertificate(certificate: certificate, options: options) { account, certificate, data, error in - continuation.resume(returning: (account: account, certificate: certificate, data: data, error: error)) - } - }) - } - - public func storeE2EEPrivateKey(privateKey: String, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, privateKey: String?, data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - storeE2EEPrivateKey(privateKey: privateKey, options: options) { account, privateKey, data, error in - continuation.resume(returning: (account: account, privateKey: privateKey, data: data, error: error)) - } - }) - } - - public func deleteE2EECertificate(options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { - - await withUnsafeContinuation({ continuation in - deleteE2EECertificate(options: options) { account, error in - continuation.resume(returning: (account: account, error: error)) - } - }) - } - - public func deleteE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { - - await withUnsafeContinuation({ continuation in - deleteE2EEPrivateKey(options: options) { account, error in - continuation.resume(returning: (account: account, error: error)) - } - }) - } -} diff --git a/Sources/NextcloudKit/Extensions/String+Extension.swift b/Sources/NextcloudKit/Extensions/String+Extension.swift index 9f5e8c12..a205a436 100644 --- a/Sources/NextcloudKit/Extensions/String+Extension.swift +++ b/Sources/NextcloudKit/Extensions/String+Extension.swift @@ -24,7 +24,6 @@ import Foundation import Alamofire extension String { - public var urlEncoded: String? { // + for historical reason, most web servers treat + as a replacement of whitespace // ?, & mark query pararmeter which should not be part of a url string, but added seperately diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 164be534..a606b2b8 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -30,29 +30,27 @@ import MobileCoreServices import CoreServices #endif -@objc public protocol NKCommonDelegate { +public protocol NKCommonDelegate { + func authenticationChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) - @objc optional func authenticationChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) - @objc optional func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) + func networkReachabilityObserver(_ typeReachability: NKCommon.TypeReachability) - @objc optional func networkReachabilityObserver(_ typeReachability: NKCommon.TypeReachability) + func downloadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) + func uploadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) - @objc optional func downloadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) - @objc optional func uploadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) - - @objc optional func downloadingFinish(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) + func downloadingFinish(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) - @objc optional func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: NSDate?, dateLastModified: NSDate?, length: Int64, task: URLSessionTask, error: NKError) - @objc optional func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: NSDate?, size: Int64, task: URLSessionTask, error: NKError) + func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: Date?, dateLastModified: Date?, length: Int64, task: URLSessionTask, error: NKError) + func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: Date?, size: Int64, task: URLSessionTask, error: NKError) } -@objc public class NKCommon: NSObject { - - @objc public let dav: String = "remote.php/dav" - @objc public let sessionIdentifierDownload: String = "com.nextcloud.nextcloudkit.session.download" - @objc public let sessionIdentifierUpload: String = "com.nextcloud.nextcloudkit.session.upload" +public class NKCommon: NSObject { + public let dav: String = "remote.php/dav" + public let sessionIdentifierDownload: String = "com.nextcloud.nextcloudkit.session.download" + public let sessionIdentifierUpload: String = "com.nextcloud.nextcloudkit.session.upload" - @objc public enum TypeReachability: Int { + public enum TypeReachability: Int { case unknown = 0 case notReachable = 1 case reachableEthernetOrWiFi = 2 @@ -127,37 +125,37 @@ import CoreServices private var _copyLogToDocumentDirectory: Bool = false private let queueLog = DispatchQueue(label: "com.nextcloud.nextcloudkit.queuelog", attributes: .concurrent ) - @objc public var user: String { + public var user: String { return internalUser } - @objc public var userId: String { + public var userId: String { return internalUserId } - @objc public var password: String { + public var password: String { return internalPassword } - @objc public var account: String { + public var account: String { return internalAccount } - @objc public var urlBase: String { + public var urlBase: String { return internalUrlBase } - @objc public var userAgent: String? { + public var userAgent: String? { return internalUserAgent } - @objc public var nextcloudVersion: Int { + public var nextcloudVersion: Int { return internalNextcloudVersion } - @objc public let backgroundQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.backgroundqueue", qos: .background, attributes: .concurrent) + public let backgroundQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.backgroundqueue", qos: .background, attributes: .concurrent) - @objc public var filenameLog: String { + public var filenameLog: String { get { return _filenameLog } @@ -169,7 +167,7 @@ import CoreServices } } - @objc public var pathLog: String { + public var pathLog: String { get { return _pathLog } @@ -185,11 +183,11 @@ import CoreServices } } - @objc public var filenamePathLog: String { + public var filenamePathLog: String { return _filenamePathLog } - @objc public var levelLog: Int { + public var levelLog: Int { get { return _levelLog } @@ -198,7 +196,7 @@ import CoreServices } } - @objc public var printLog: Bool { + public var printLog: Bool { get { return _printLog } @@ -207,7 +205,7 @@ import CoreServices } } - @objc public var copyLogToDocumentDirectory: Bool { + public var copyLogToDocumentDirectory: Bool { get { return _copyLogToDocumentDirectory } @@ -239,27 +237,22 @@ import CoreServices return results } - @objc public func addInternalTypeIdentifier(typeIdentifier: String, classFile: String, editor: String, iconName: String, name: String) { - + public func addInternalTypeIdentifier(typeIdentifier: String, classFile: String, editor: String, iconName: String, name: String) { if !internalTypeIdentifiers.contains(where: { $0.typeIdentifier == typeIdentifier && $0.editor == editor}) { let newUTI = UTTypeConformsToServer(typeIdentifier: typeIdentifier, classFile: classFile, editor: editor, iconName: iconName, name: name) internalTypeIdentifiers.append(newUTI) } } - @objc public func objcGetInternalType(fileName: String, mimeType: String, directory: Bool) -> [String: String] { - + public func objcGetInternalType(fileName: String, mimeType: String, directory: Bool) -> [String: String] { let results = getInternalType(fileName: fileName, mimeType: mimeType, directory: directory) - return ["mimeType": results.mimeType, "classFile": results.classFile, "iconName": results.iconName, "typeIdentifier": results.typeIdentifier, "fileNameWithoutExt": results.fileNameWithoutExt, "ext": results.ext] } public func getInternalType(fileName: String, mimeType: String, directory: Bool) -> (mimeType: String, classFile: String, iconName: String, typeIdentifier: String, fileNameWithoutExt: String, ext: String) { - var ext = (fileName as NSString).pathExtension.lowercased() var mimeType = mimeType var classFile = "", iconName = "", typeIdentifier = "", fileNameWithoutExt = "" - var inUTI: CFString? if let cachedUTI = utiCache.object(forKey: ext as NSString) { @@ -315,7 +308,6 @@ import CoreServices } public func getFileProperties(inUTI: CFString) -> NKFileProperty { - let fileProperty = NKFileProperty() let typeIdentifier: String = inUTI as String @@ -497,7 +489,6 @@ import CoreServices } public func getStandardHeaders(user: String?, password: String?, appendHeaders: [String: String]?, customUserAgent: String?, contentType: String? = nil) -> HTTPHeaders { - var headers: HTTPHeaders = [] if let username = user, let password = password { @@ -526,30 +517,25 @@ import CoreServices } public func createStandardUrl(serverUrl: String, endpoint: String) -> URLConvertible? { - guard var serverUrl = serverUrl.urlEncoded else { return nil } if serverUrl.last != "/" { serverUrl = serverUrl + "/" } serverUrl = serverUrl + endpoint - return serverUrl.asUrl } - public func convertDate(_ dateString: String, format: String) -> NSDate? { - + public func convertDate(_ dateString: String, format: String) -> Date? { if dateString.isEmpty { return nil } - let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.dateFormat = format - guard let date = dateFormatter.date(from: dateString) as? NSDate else { return nil } + guard let date = dateFormatter.date(from: dateString) else { return nil } return date } func convertDate(_ date: Date, format: String) -> String? { - let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") @@ -559,7 +545,6 @@ import CoreServices } func findHeader(_ header: String, allHeaderFields: [AnyHashable: Any]?) -> String? { - guard let allHeaderFields = allHeaderFields else { return nil } let keyValues = allHeaderFields.map { (String(describing: $0.key).lowercased(), String(describing: $0.value)) } @@ -570,7 +555,6 @@ import CoreServices } func getHostName(urlString: String) -> String? { - if let url = URL(string: urlString) { guard let hostName = url.host else { return nil } guard let scheme = url.scheme else { return nil } @@ -583,7 +567,6 @@ import CoreServices } func getHostNameComponent(urlString: String) -> String? { - if let url = URL(string: urlString) { let components = url.pathComponents return components.joined(separator: "") @@ -592,7 +575,6 @@ import CoreServices } func getFileSize(filePath: String) -> Int64 { - do { let attributes = try FileManager.default.attributesOfItem(atPath: filePath) return attributes[FileAttributeKey.size] as? Int64 ?? 0 @@ -603,13 +585,11 @@ import CoreServices } public func returnPathfromServerUrl(_ serverUrl: String) -> String { - let home = urlBase + "/remote.php/dav/files/" + userId return serverUrl.replacingOccurrences(of: home, with: "") } public func getSessionErrorFromAFError(_ afError: AFError?) -> NSError? { - if let afError = afError?.asAFError { switch afError { case .sessionTaskFailed(let sessionError): @@ -622,8 +602,7 @@ import CoreServices // MARK: - Log - @objc public func clearFileLog() { - + public func clearFileLog() { FileManager.default.createFile(atPath: filenamePathLog, contents: nil, attributes: nil) if copyLogToDocumentDirectory { let filenameCopyToDocumentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/" + filenameLog @@ -632,8 +611,7 @@ import CoreServices } } - @objc public func writeLog(_ text: String?) { - + public func writeLog(_ text: String?) { guard let text = text else { return } guard let date = self.convertDate(Date(), format: "yyyy-MM-dd' 'HH:mm:ss") else { return } let textToWrite = "\(date) " + text + "\n" @@ -656,12 +634,12 @@ import CoreServices } private func writeLogToDisk(filename: String, text: String) { - guard let data = text.data(using: .utf8) else { return } if !FileManager.default.fileExists(atPath: filename) { FileManager.default.createFile(atPath: filename, contents: nil, attributes: nil) } + if let fileHandle = FileHandle(forWritingAtPath: filename) { fileHandle.seekToEndOfFile() fileHandle.write(data) diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index 845b7b66..579b3000 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -51,11 +51,8 @@ extension OCSPath { static var ocsXMLMsg: Self { ["d:error", "s:message"] } } -@objcMembers public class NKError: NSObject { - static let internalError = -9999 - // Chunk error public static let chunkNoEnoughMemory = -9998 public static let chunkMoveFile = -9997 diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 18735ed0..d69f68c7 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -33,260 +33,285 @@ import SwiftyJSON // MARK: - -@objc public class NKActivity: NSObject { - - @objc public var app = "" - @objc public var date = NSDate() - @objc public var idActivity: Int = 0 - @objc public var icon = "" - @objc public var link = "" - @objc public var message = "" - @objc public var messageRich: Data? - @objc public var objectId: Int = 0 - @objc public var objectName = "" - @objc public var objectType = "" - @objc public var previews: Data? - @objc public var subject = "" - @objc public var subjectRich: Data? - @objc public var type = "" - @objc public var user = "" -} +public enum NKProperties: String, CaseIterable { + case displayname = "" + case getlastmodified = "" + case getetag = "" + case getcontenttype = "" + case resourcetype = "" + case quotaavailablebytes = "" + case quotausedbytes = "" + case getcontentlength = "" + + case permissions = "" + case id = "" + case fileid = "" + case size = "" + case favorite = "" + case sharetypes = "" + case ownerid = "" + case ownerdisplayname = "" + case commentsunread = "" + case checksums = "" + case downloadURL = "" + case datafingerprint = "" + + case creationtime = "" + case uploadtime = "" + case isencrypted = "" + case haspreview = "" + case mounttype = "" + case richworkspace = "" + case note = "" + case lock = "" + case lockowner = "" + case lockownereditor = "" + case lockownerdisplayname = "" + case lockownertype = "" + case locktime = "" + case locktimeout = "" + case systemtags = "" + case filemetadatasize = "" + case filemetadatagps = "" + case metadataphotossize = "" + case metadataphotosgps = "" + case metadatafileslivephoto = "" + case hidden = "" + + case sharepermissionscollaboration = "" + case sharepermissionscloudmesh = "" + + static func properties(removeProperties: [String] = []) -> String { + var properties = allCases.map { $0.rawValue }.joined() + for property in removeProperties { + properties = properties.replacingOccurrences(of: property, with: "") + } + return properties + } -@objc public class NKComments: NSObject { - - @objc public var actorDisplayName = "" - @objc public var actorId = "" - @objc public var actorType = "" - @objc public var creationDateTime = NSDate() - @objc public var isUnread: Bool = false - @objc public var message = "" - @objc public var messageId = "" - @objc public var objectId = "" - @objc public var objectType = "" - @objc public var path = "" - @objc public var verb = "" + static func trashProperties() -> String { + let properties: [String] = [displayname.rawValue, getcontenttype.rawValue, resourcetype.rawValue, id.rawValue, fileid.rawValue, size.rawValue, haspreview.rawValue, "", "", ""] + return properties.joined() + } } -@objc public class NKEditorDetailsCreators: NSObject { - - @objc public var editor = "" - @objc public var ext = "" - @objc public var identifier = "" - @objc public var mimetype = "" - @objc public var name = "" - @objc public var templates: Int = 0 +public class NKActivity: NSObject { + public var app = "" + public var date = Date() + public var idActivity: Int = 0 + public var icon = "" + public var link = "" + public var message = "" + public var messageRich: Data? + public var objectId: Int = 0 + public var objectName = "" + public var objectType = "" + public var previews: Data? + public var subject = "" + public var subjectRich: Data? + public var type = "" + public var user = "" } -@objc public class NKEditorDetailsEditors: NSObject { - - @objc public var mimetypes: [String] = [] - @objc public var name = "" - @objc public var optionalMimetypes: [String] = [] - @objc public var secure: Int = 0 +public class NKComments: NSObject { + public var actorDisplayName = "" + public var actorId = "" + public var actorType = "" + public var creationDateTime = Date() + public var isUnread: Bool = false + public var message = "" + public var messageId = "" + public var objectId = "" + public var objectType = "" + public var path = "" + public var verb = "" } -@objc public class NKEditorTemplates: NSObject { - - @objc public var delete = "" - @objc public var ext = "" - @objc public var identifier = "" - @objc public var name = "" - @objc public var preview = "" - @objc public var type = "" +public class NKEditorDetailsCreators: NSObject { + public var editor = "" + public var ext = "" + public var identifier = "" + public var mimetype = "" + public var name = "" + public var templates: Int = 0 } -@objc public class NKExternalSite: NSObject { +public class NKEditorDetailsEditors: NSObject { + public var mimetypes: [String] = [] + public var name = "" + public var optionalMimetypes: [String] = [] + public var secure: Int = 0 +} - @objc public var icon = "" - @objc public var idExternalSite: Int = 0 - @objc public var lang = "" - @objc public var name = "" - @objc public var order: Int = 0 - @objc public var type = "" - @objc public var url = "" +public class NKEditorTemplates: NSObject { + public var delete = "" + public var ext = "" + public var identifier = "" + public var name = "" + public var preview = "" + public var type = "" } -@objc public class NKFile: NSObject { - - @objc public var account = "" - @objc public var classFile = "" - @objc public var commentsUnread: Bool = false - @objc public var contentType = "" - @objc public var checksums = "" - @objc public var creationDate: NSDate? - @objc public var dataFingerprint = "" - @objc public var date = NSDate() - @objc public var directory: Bool = false - @objc public var downloadURL = "" - @objc public var e2eEncrypted: Bool = false - @objc public var etag = "" - @objc public var favorite: Bool = false - @objc public var fileId = "" - @objc public var fileName = "" - @objc public var hasPreview: Bool = false - @objc public var iconName = "" - @objc public var mountType = "" - @objc public var name = "" - @objc public var note = "" - @objc public var ocId = "" - @objc public var ownerId = "" - @objc public var ownerDisplayName = "" - @objc public var lock = false - @objc public var lockOwner = "" - @objc public var lockOwnerEditor = "" - @objc public var lockOwnerType = 0 - @objc public var lockOwnerDisplayName = "" - @objc public var lockTime: Date? - @objc public var lockTimeOut: Date? - @objc public var path = "" - @objc public var permissions = "" - @objc public var quotaUsedBytes: Int64 = 0 - @objc public var quotaAvailableBytes: Int64 = 0 - @objc public var resourceType = "" - @objc public var richWorkspace: String? - @objc public var sharePermissionsCollaborationServices: Int = 0 - @objc public var sharePermissionsCloudMesh: [String] = [] - @objc public var shareType: [Int] = [] - @objc public var size: Int64 = 0 - @objc public var serverUrl = "" - @objc public var tags: [String] = [] - @objc public var trashbinFileName = "" - @objc public var trashbinOriginalLocation = "" - @objc public var trashbinDeletionTime = NSDate() - @objc public var uploadDate: NSDate? - @objc public var urlBase = "" - @objc public var user = "" - @objc public var userId = "" - @objc public var latitude: Double = 0 - @objc public var longitude: Double = 0 - @objc public var altitude: Double = 0 - @objc public var height: Double = 0 - @objc public var width: Double = 0 +public class NKExternalSite: NSObject { + public var icon = "" + public var idExternalSite: Int = 0 + public var lang = "" + public var name = "" + public var order: Int = 0 + public var type = "" + public var url = "" +} +public class NKFile: NSObject { + public var account = "" + public var classFile = "" + public var commentsUnread: Bool = false + public var contentType = "" + public var checksums = "" + public var creationDate: Date? + public var dataFingerprint = "" + public var date = Date() + public var directory: Bool = false + public var downloadURL = "" + public var e2eEncrypted: Bool = false + public var etag = "" + public var favorite: Bool = false + public var fileId = "" + public var fileName = "" + public var hasPreview: Bool = false + public var iconName = "" + public var mountType = "" + public var name = "" + public var note = "" + public var ocId = "" + public var ownerId = "" + public var ownerDisplayName = "" + public var lock = false + public var lockOwner = "" + public var lockOwnerEditor = "" + public var lockOwnerType = 0 + public var lockOwnerDisplayName = "" + public var lockTime: Date? + public var lockTimeOut: Date? + public var path = "" + public var permissions = "" + public var quotaUsedBytes: Int64 = 0 + public var quotaAvailableBytes: Int64 = 0 + public var resourceType = "" + public var richWorkspace: String? + public var sharePermissionsCollaborationServices: Int = 0 + public var sharePermissionsCloudMesh: [String] = [] + public var shareType: [Int] = [] + public var size: Int64 = 0 + public var serverUrl = "" + public var tags: [String] = [] + public var trashbinFileName = "" + public var trashbinOriginalLocation = "" + public var trashbinDeletionTime = Date() + public var uploadDate: Date? + public var urlBase = "" + public var user = "" + public var userId = "" + public var latitude: Double = 0 + public var longitude: Double = 0 + public var altitude: Double = 0 + public var height: Double = 0 + public var width: Double = 0 + public var hidden = false /// If this is not empty, the media is a live photo. New media gets this straight from server, but old media needs to be detected as live photo (look isFlaggedAsLivePhotoByServer) - @objc public var livePhotoFile = "" - + public var livePhotoFile = "" /// Indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side - @objc public var isFlaggedAsLivePhotoByServer = false - - @objc public var hidden = false - + public var isFlaggedAsLivePhotoByServer = false } -@objcMembers public class NKFileProperty: NSObject { - +public class NKFileProperty: NSObject { public var classFile: String = "" public var iconName: String = "" public var name: String = "" public var ext: String = "" } -@objc public class NKNotifications: NSObject { - - @objc public var actions: Data? - @objc public var app = "" - @objc public var date = NSDate() - @objc public var icon: String? - @objc public var idNotification: Int = 0 - @objc public var link = "" - @objc public var message = "" - @objc public var messageRich = "" - @objc public var messageRichParameters: Data? - @objc public var objectId = "" - @objc public var objectType = "" - @objc public var subject = "" - @objc public var subjectRich = "" - @objc public var subjectRichParameters: Data? - @objc public var user = "" -} - -@objc public class NKRichdocumentsTemplate: NSObject { - - @objc public var delete = "" - @objc public var ext = "" - @objc public var name = "" - @objc public var preview = "" - @objc public var templateId: Int = 0 - @objc public var type = "" +public class NKRichdocumentsTemplate: NSObject { + public var delete = "" + public var ext = "" + public var name = "" + public var preview = "" + public var templateId: Int = 0 + public var type = "" } -@objc public class NKSharee: NSObject { - - @objc public var circleInfo = "" - @objc public var circleOwner = "" - @objc public var label = "" - @objc public var name = "" - @objc public var shareType: Int = 0 - @objc public var shareWith = "" - @objc public var uuid = "" - @objc public var userClearAt: NSDate? - @objc public var userIcon = "" - @objc public var userMessage = "" - @objc public var userStatus = "" +public class NKSharee: NSObject { + public var circleInfo = "" + public var circleOwner = "" + public var label = "" + public var name = "" + public var shareType: Int = 0 + public var shareWith = "" + public var uuid = "" + public var userClearAt: Date? + public var userIcon = "" + public var userMessage = "" + public var userStatus = "" } -@objc public class NKTrash: NSObject { - - @objc public var contentType = "" - @objc public var date = NSDate() - @objc public var directory: Bool = false - @objc public var fileId = "" - @objc public var fileName = "" - @objc public var filePath = "" - @objc public var hasPreview: Bool = false - @objc public var iconName = "" - @objc public var size: Int64 = 0 - @objc public var classFile = "" - @objc public var trashbinFileName = "" - @objc public var trashbinOriginalLocation = "" - @objc public var trashbinDeletionTime = NSDate() +public class NKTrash: NSObject { + public var contentType = "" + public var date = Date() + public var directory: Bool = false + public var fileId = "" + public var fileName = "" + public var filePath = "" + public var hasPreview: Bool = false + public var iconName = "" + public var size: Int64 = 0 + public var classFile = "" + public var trashbinFileName = "" + public var trashbinOriginalLocation = "" + public var trashbinDeletionTime = Date() } -@objc public class NKUserProfile: NSObject { - - @objc public var address = "" - @objc public var backend = "" - @objc public var backendCapabilitiesSetDisplayName: Bool = false - @objc public var backendCapabilitiesSetPassword: Bool = false - @objc public var displayName = "" - @objc public var email = "" - @objc public var enabled: Bool = false - @objc public var groups: [String] = [] - @objc public var language = "" - @objc public var lastLogin: Int64 = 0 - @objc public var locale = "" - @objc public var organisation = "" - @objc public var phone = "" - @objc public var quota: Int64 = 0 - @objc public var quotaFree: Int64 = 0 - @objc public var quotaRelative: Double = 0 - @objc public var quotaTotal: Int64 = 0 - @objc public var quotaUsed: Int64 = 0 - @objc public var storageLocation = "" - @objc public var subadmin: [String] = [] - @objc public var twitter = "" - @objc public var userId = "" - @objc public var website = "" +public class NKUserProfile: NSObject { + public var address = "" + public var backend = "" + public var backendCapabilitiesSetDisplayName: Bool = false + public var backendCapabilitiesSetPassword: Bool = false + public var displayName = "" + public var email = "" + public var enabled: Bool = false + public var groups: [String] = [] + public var language = "" + public var lastLogin: Int64 = 0 + public var locale = "" + public var organisation = "" + public var phone = "" + public var quota: Int64 = 0 + public var quotaFree: Int64 = 0 + public var quotaRelative: Double = 0 + public var quotaTotal: Int64 = 0 + public var quotaUsed: Int64 = 0 + public var storageLocation = "" + public var subadmin: [String] = [] + public var twitter = "" + public var userId = "" + public var website = "" } -@objc public class NKUserStatus: NSObject { - - @objc public var clearAt: NSDate? - @objc public var clearAtTime: String? - @objc public var clearAtType: String? - @objc public var icon: String? - @objc public var id: String? - @objc public var message: String? - @objc public var predefined: Bool = false - @objc public var status: String? - @objc public var userId: String? +public class NKUserStatus: NSObject { + public var clearAt: Date? + public var clearAtTime: String? + public var clearAtType: String? + public var icon: String? + public var id: String? + public var message: String? + public var predefined: Bool = false + public var status: String? + public var userId: String? } // MARK: - Data File class NKDataFileXML: NSObject { let nkCommonInstance: NKCommon - let requestBodyComments = """ @@ -330,64 +355,17 @@ class NKDataFileXML: NSObject { """ - let propStandard = - """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - """ - - lazy var requestBodyFile: String = { - return """ + func getRequestBodyFile(removeProperties: [String] = []) -> String { + let request = """ - """ + propStandard + """ + """ + NKProperties.properties(removeProperties: removeProperties) + """ """ - }() + return request + } let requestBodyFileSetFavorite = """ @@ -401,28 +379,29 @@ class NKDataFileXML: NSObject { """ - lazy var requestBodyFileListingFavorites: String = { - return """ + func getRequestBodyFileListingFavorites(removeProperties: [String] = []) -> String { + let request = """ - """ + propStandard + """ + """ + NKProperties.properties(removeProperties: removeProperties) + """ 1 """ - }() + return request + } - lazy var requestBodySearchFileName: String = { - return """ + func getRequestBodySearchFileName(removeProperties: [String] = []) -> String { + let request = """ - """ + propStandard + """ + """ + NKProperties.properties(removeProperties: removeProperties) + """ @@ -440,16 +419,17 @@ class NKDataFileXML: NSObject { """ - }() + return request + } - lazy var requestBodySearchFileId: String = { - return """ + func getRequestBodySearchFileId(removeProperties: [String] = []) -> String { + let request = """ - """ + propStandard + """ + """ + NKProperties.properties(removeProperties: removeProperties) + """ @@ -467,46 +447,17 @@ class NKDataFileXML: NSObject { """ - }() - - lazy var requestBodySearchLessThan: String = { - return """ - - - - - - """ + propStandard + """ - - - - - %@ - infinity - - - - - - %@ - - - - %@ - - - - """ - }() + return request + } - lazy var requestBodySearchMedia: String = { - return """ + func getRequestBodySearchMedia(removeProperties: [String] = []) -> String { + let request = """ - """ + propStandard + """ + """ + NKProperties.properties(removeProperties: removeProperties) + """ @@ -554,16 +505,17 @@ class NKDataFileXML: NSObject { """ - }() + return request + } - lazy var requestBodySearchMediaWithLimit: String = { - return """ + func getRequestBodySearchMediaWithLimit(removeProperties: [String] = []) -> String { + let request = """ - """ + propStandard + """ + """ + NKProperties.properties(removeProperties: removeProperties) + """ @@ -614,37 +566,15 @@ class NKDataFileXML: NSObject { """ - }() + return request + } let requestBodyTrash = """ - - - - - - - - - - - - - - - - - - - - - - - - + """ + NKProperties.trashProperties() + """ """ @@ -667,21 +597,19 @@ class NKDataFileXML: NSObject { } func convertDataAppPassword(data: Data) -> String? { - let xml = XML.parse(data) return xml["ocs", "data", "apppassword"].text } func convertDataFile(xmlData: Data, dav: String, urlBase: String, user: String, userId: String, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] { - var files: [NKFile] = [] let rootFiles = "/" + dav + "/files/" guard let baseUrl = self.nkCommonInstance.getHostName(urlString: urlBase) else { return files } - let xml = XML.parse(xmlData) let elements = xml["d:multistatus", "d:response"] + for element in elements { let file = NKFile() if let href = element["d:href"].text { @@ -733,11 +661,11 @@ class NKDataFileXML: NSObject { } if let creationtime = propstat["d:prop", "nc:creation_time"].double, creationtime > 0 { - file.creationDate = NSDate(timeIntervalSince1970: creationtime) + file.creationDate = Date(timeIntervalSince1970: creationtime) } if let uploadtime = propstat["d:prop", "nc:upload_time"].double, uploadtime > 0 { - file.uploadDate = NSDate(timeIntervalSince1970: uploadtime) + file.uploadDate = Date(timeIntervalSince1970: uploadtime) } if let getetag = propstat["d:prop", "d:getetag"].text { @@ -958,15 +886,14 @@ class NKDataFileXML: NSObject { } func convertDataTrash(xmlData: Data, urlBase: String, showHiddenFiles: Bool) -> [NKTrash] { - var files: [NKTrash] = [] var first: Bool = true guard let baseUrl = self.nkCommonInstance.getHostName(urlString: urlBase) else { return files } - let xml = XML.parse(xmlData) let elements = xml["d:multistatus", "d:response"] + for element in elements { if first { first = false @@ -1027,7 +954,7 @@ class NKDataFileXML: NSObject { } if let trashbinDeletionTime = propstat["d:prop", "nc:trashbin-deletion-time"].text, let trashbinDeletionTimeDouble = Double(trashbinDeletionTime) { - file.trashbinDeletionTime = Date(timeIntervalSince1970: trashbinDeletionTimeDouble) as NSDate + file.trashbinDeletionTime = Date(timeIntervalSince1970: trashbinDeletionTimeDouble) } let results = self.nkCommonInstance.getInternalType(fileName: file.trashbinFileName, mimeType: file.contentType, directory: file.directory) @@ -1043,11 +970,10 @@ class NKDataFileXML: NSObject { } func convertDataComments(xmlData: Data) -> [NKComments] { - var items: [NKComments] = [] - let xml = XML.parse(xmlData) let elements = xml["d:multistatus", "d:response"] + for element in elements { let item = NKComments() diff --git a/Sources/NextcloudKit/NKRequestOptions.swift b/Sources/NextcloudKit/NKRequestOptions.swift index 961829d7..2bfaff3e 100644 --- a/Sources/NextcloudKit/NKRequestOptions.swift +++ b/Sources/NextcloudKit/NKRequestOptions.swift @@ -23,9 +23,7 @@ import Foundation -@objcMembers public class NKRequestOptions: NSObject { - var endpoint: String? var version: String? var customHeader: [String: String]? @@ -34,6 +32,7 @@ public class NKRequestOptions: NSObject { var e2eToken: String? var timeout: TimeInterval var taskDescription: String? + var removeProperties: [String] var queue: DispatchQueue public init(endpoint: String? = nil, @@ -44,6 +43,7 @@ public class NKRequestOptions: NSObject { e2eToken: String? = nil, timeout: TimeInterval = 60, taskDescription: String? = nil, + removeProperties: [String] = [], queue: DispatchQueue = .main) { self.endpoint = endpoint @@ -54,6 +54,7 @@ public class NKRequestOptions: NSObject { self.e2eToken = e2eToken self.timeout = timeout self.taskDescription = taskDescription + self.removeProperties = removeProperties self.queue = queue } } diff --git a/Sources/NextcloudKit/NKShareAccounts.swift b/Sources/NextcloudKit/NKShareAccounts.swift index 16143a4e..b9b082bb 100644 --- a/Sources/NextcloudKit/NKShareAccounts.swift +++ b/Sources/NextcloudKit/NKShareAccounts.swift @@ -25,16 +25,14 @@ import Foundation #if os(iOS) import UIKit -@objc public class NKShareAccounts: NSObject { - - @objc public class DataAccounts: NSObject { - - @objc public var url: String - @objc public var user: String - @objc public var name: String? - @objc public var image: UIImage? - - @objc public init(withUrl url: String, user: String, name: String? = nil, image: UIImage? = nil) { +public class NKShareAccounts: NSObject { + public class DataAccounts: NSObject { + public var url: String + public var user: String + public var name: String? + public var image: UIImage? + + public init(withUrl url: String, user: String, name: String? = nil, image: UIImage? = nil) { self.url = url self.user = user self.name = name @@ -59,7 +57,7 @@ import UIKit /// - directory: the group directory of share the accounts (group.com.nextcloud.apps), use the func containerURL(forSecurityApplicationGroupIdentifier groupIdentifier: String) -> URL? // Available for OS X in 10.8.3. /// - app: the name of app /// - dataAccounts: the accounts data - @objc public func putShareAccounts(at directory: URL, app: String, dataAccounts: [DataAccounts]) -> Error? { + public func putShareAccounts(at directory: URL, app: String, dataAccounts: [DataAccounts]) -> Error? { var apps: [String: [Account]] = [:] var accounts: [Account] = [] @@ -105,7 +103,7 @@ import UIKit /// - Parameters: /// - directory: the group directory of share the accounts (group.com.nextcloud.apps), use the func containerURL(forSecurityApplicationGroupIdentifier groupIdentifier: String) -> URL? // Available for OS X in 10.8.3. /// - application: the UIApplication used for verify if the app(s) is still installed - @objc public func getShareAccount(at directory: URL, application: UIApplication) -> [DataAccounts]? { + public func getShareAccount(at directory: URL, application: UIApplication) -> [DataAccounts]? { var dataAccounts: [DataAccounts] = [] let url = directory.appendingPathComponent(directoryAccounts + "/" + fileName) diff --git a/Sources/NextcloudKit/API/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift similarity index 82% rename from Sources/NextcloudKit/API/NextcloudKit+API.swift rename to Sources/NextcloudKit/NextcloudKit+API.swift index 11bf82bb..559d5243 100644 --- a/Sources/NextcloudKit/API/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -30,13 +30,29 @@ import UIKit import Alamofire import SwiftyJSON -extension NextcloudKit { - - @objc public func checkServer(serverUrl: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ error: NKError) -> Void) { +public class NKNotifications: NSObject { + public var actions: Data? + public var app = "" + public var date = Date() + public var icon: String? + public var idNotification: Int = 0 + public var link = "" + public var message = "" + public var messageRich = "" + public var messageRichParameters: Data? + public var objectId = "" + public var objectType = "" + public var subject = "" + public var subjectRich = "" + public var subjectRichParameters: Data? + public var user = "" +} +public extension NextcloudKit { + func checkServer(serverUrl: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ error: NKError) -> Void) { guard let url = serverUrl.asUrl else { return options.queue.async { completion(.urlError) } } @@ -48,7 +64,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -61,21 +76,17 @@ extension NextcloudKit { // MARK: - - @objc public func generalWithEndpoint(_ endpoint: String, - method: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - + func generalWithEndpoint(_ endpoint: String, + method: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, .urlError) } } - let method = HTTPMethod(rawValue: method.uppercased()) - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -85,7 +96,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -98,20 +108,16 @@ extension NextcloudKit { // MARK: - - @objc public func getExternalSite(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ data: Data?, _ error: NKError) -> Void) { - + func getExternalSite(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var externalSites: [NKExternalSite] = [] - let endpoint = "ocs/v2.php/apps/external/api/v1" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, externalSites, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -121,7 +127,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -131,14 +136,12 @@ extension NextcloudKit { let ocsdata = json["ocs"]["data"] for (_, subJson): (String, JSON) in ocsdata { let extrernalSite = NKExternalSite() - extrernalSite.icon = subJson["icon"].stringValue extrernalSite.idExternalSite = subJson["id"].intValue extrernalSite.lang = subJson["lang"].stringValue extrernalSite.name = subJson["name"].stringValue extrernalSite.type = subJson["type"].stringValue extrernalSite.url = subJson["url"].stringValue - externalSites.append(extrernalSite) } options.queue.async { completion(account, externalSites, jsonData, .success) } @@ -148,7 +151,7 @@ extension NextcloudKit { // MARK: - getServerStatus - public struct ServerInfo { + struct ServerInfo { public let installed: Bool public let maintenance: Bool public let needsDbUpgrade: Bool @@ -161,22 +164,19 @@ extension NextcloudKit { public let data: Data? } - public enum ServerInfoResult { + enum ServerInfoResult { case success(ServerInfo) case failure(NKError) } - public func getServerStatus(serverUrl: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (ServerInfoResult) -> Void) { - + func getServerStatus(serverUrl: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (ServerInfoResult) -> Void) { let endpoint = "status.php" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(ServerInfoResult.failure(.urlError)) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -186,7 +186,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -216,7 +215,6 @@ extension NextcloudKit { versionMinor: versionMinor, versionMicro: versionMicro, data: jsonData) - options.queue.async { completion(ServerInfoResult.success(serverInfo)) } } } @@ -224,13 +222,11 @@ extension NextcloudKit { // MARK: - - @objc public func getPreview(url: URL, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - + func getPreview(url: URL, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -240,7 +236,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -255,23 +250,22 @@ extension NextcloudKit { } } - public func downloadPreview(fileId: String, - fileNamePreviewLocalPath: String, - fileNameIconLocalPath: String? = nil, - widthPreview: Int = 512, - heightPreview: Int = 512, - sizeIcon: Int = 512, - compressionQualityPreview: CGFloat = 0.5, - compressionQualityIcon: CGFloat = 0.5, - etag: String? = nil, - crop: Int = 0, - cropMode: String = "fill", - forceIcon: Int = 1, - mimeFallback: Int = 0, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - + func downloadPreview(fileId: String, + fileNamePreviewLocalPath: String, + fileNameIconLocalPath: String? = nil, + widthPreview: Int = 512, + heightPreview: Int = 512, + sizeIcon: Int = 512, + compressionQualityPreview: CGFloat = 0.5, + compressionQualityIcon: CGFloat = 0.5, + etag: String? = nil, + crop: Int = 0, + cropMode: String = "fill", + forceIcon: Int = 1, + mimeFallback: Int = 0, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: etag, endpoint: endpoint, options: options) { task in @@ -281,22 +275,21 @@ extension NextcloudKit { } } - public func downloadTrashPreview(fileId: String, - fileNamePreviewLocalPath: String, - fileNameIconLocalPath: String, - widthPreview: Int = 512, - heightPreview: Int = 512, - sizeIcon: Int = 512, - compressionQualityPreview: CGFloat = 0.5, - compressionQualityIcon: CGFloat = 0.5, - crop: Int = 0, - cropMode: String = "fill", - forceIcon: Int = 1, - mimeFallback: Int = 0, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - + func downloadTrashPreview(fileId: String, + fileNamePreviewLocalPath: String, + fileNameIconLocalPath: String, + widthPreview: Int = 512, + heightPreview: Int = 512, + sizeIcon: Int = 512, + compressionQualityPreview: CGFloat = 0.5, + compressionQualityIcon: CGFloat = 0.5, + crop: Int = 0, + cropMode: String = "fill", + forceIcon: Int = 1, + mimeFallback: Int = 0, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: nil, endpoint: endpoint, options: options) { task in @@ -316,19 +309,15 @@ extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var url: URLConvertible? - if let endpoint { url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) } - guard let urlRequest = url else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } - var headers = self.nkCommonInstance.getStandardHeaders(options: options) if var etag = etag { etag = "\"" + etag + "\"" @@ -342,7 +331,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -373,24 +361,20 @@ extension NextcloudKit { } } - @objc public func downloadAvatar(user: String, - fileNameLocalPath: String, - sizeImage: Int, - avatarSizeRounded: Int = 0, - etag: String?, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - + func downloadAvatar(user: String, + fileNameLocalPath: String, + sizeImage: Int, + avatarSizeRounded: Int = 0, + etag: String?, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "index.php/avatar/\(user)/\(sizeImage)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - var headers = self.nkCommonInstance.getStandardHeaders(options: options) if var etag = etag { etag = "\"" + etag + "\"" @@ -404,7 +388,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -469,17 +452,14 @@ extension NextcloudKit { } } - @objc public func downloadContent(serverUrl: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - + func downloadContent(serverUrl: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - guard let url = serverUrl.asUrl else { return options.queue.async { completion(account, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -489,7 +469,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -506,19 +485,15 @@ extension NextcloudKit { // MARK: - - @objc public func getUserProfile(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { - + func getUserProfile(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/cloud/user" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -528,7 +503,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -539,9 +513,7 @@ extension NextcloudKit { let data = ocs["data"] if json["ocs"]["meta"]["statuscode"].int == 200 { - let userProfile = NKUserProfile() - userProfile.address = data["address"].stringValue userProfile.backend = data["backend"].stringValue userProfile.backendCapabilitiesSetDisplayName = data["backendCapabilities"]["setDisplayName"].boolValue @@ -575,7 +547,6 @@ extension NextcloudKit { userProfile.website = data["website"].stringValue options.queue.async { completion(account, userProfile, jsonData, .success) } - } else { options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } @@ -583,19 +554,15 @@ extension NextcloudKit { } } - @objc public func getCapabilities(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - + func getCapabilities(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v1.php/cloud/capabilities" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -605,7 +572,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -622,20 +588,15 @@ extension NextcloudKit { // MARK: - - @objc public func getRemoteWipeStatus(serverUrl: String, - token: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ wipe: Bool, _ data: Data?, _ error: NKError) -> Void) { - + func getRemoteWipeStatus(serverUrl: String, + token: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ wipe: Bool, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - let endpoint = "index.php/core/wipe/check" - let parameters: [String: Any] = ["token": token] - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(account, false, nil, .urlError) } } @@ -647,7 +608,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -660,20 +620,15 @@ extension NextcloudKit { } } - @objc public func setRemoteWipeCompletition(serverUrl: String, - token: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func setRemoteWipeCompletition(serverUrl: String, + token: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - let endpoint = "index.php/core/wipe/success" - let parameters: [String: Any] = ["token": token] - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } @@ -685,7 +640,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -698,23 +652,20 @@ extension NextcloudKit { // MARK: - - @objc public func getActivity(since: Int, - limit: Int, - objectId: String?, - objectType: String?, - previews: Bool, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ data: Data?, _ error: NKError) -> Void) { - + func getActivity(since: Int, + limit: Int, + objectId: String?, + objectType: String?, + previews: Bool, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var activities: [NKActivity] = [] var activityFirstKnown = 0 var activityLastGiven = 0 - var endpoint = "ocs/v2.php/apps/activity/api/v2/activity/" - var parameters: [String: Any] = [ "format": "json", "since": String(since), @@ -736,7 +687,6 @@ extension NextcloudKit { guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -746,7 +696,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -805,21 +754,16 @@ extension NextcloudKit { // MARK: - - @objc public func getNotifications(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ data: Data?, _ error: NKError) -> Void) { - + func getNotifications(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications" - var notifications: [NKNotifications] = [] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -829,7 +773,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -849,7 +792,7 @@ extension NextcloudKit { notification.app = subJson["app"].stringValue if let datetime = subJson["datetime"].string { if let date = self.nkCommonInstance.convertDate(datetime, format: "yyyy-MM-dd'T'HH:mm:ssZZZZZ") { - notification.date = date + notification.date = date as Date } } notification.icon = subJson["icon"].string @@ -872,42 +815,34 @@ extension NextcloudKit { } catch {} } notification.user = subJson["user"].stringValue - notifications.append(notification) } - options.queue.async { completion(account, notifications, jsonData, .success) } - } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } } - @objc public func setNotification(serverUrl: String?, - idNotification: Int, - method: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func setNotification(serverUrl: String?, + idNotification: Int, + method: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var url: URLConvertible? - if serverUrl == nil { let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications/\(idNotification)" url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) } else { url = serverUrl!.asUrl } - guard let urlRequest = url else { return options.queue.async { completion(account, .urlError) } } - let method = HTTPMethod(rawValue: method) let headers = self.nkCommonInstance.getStandardHeaders(options: options) @@ -918,7 +853,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -931,25 +865,20 @@ extension NextcloudKit { // MARK: - - @objc public func getDirectDownload(fileId: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - + func getDirectDownload(fileId: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/dav/api/v1/direct" - let parameters: [String: Any] = [ "fileId": fileId, "format": "json" ] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -959,7 +888,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -975,22 +903,17 @@ extension NextcloudKit { // MARK: - - public func sendClientDiagnosticsRemoteOperation(data: Data, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func sendClientDiagnosticsRemoteOperation(data: Data, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/security_guard/diagnostics" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") - var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: .put, headers: headers) @@ -1006,7 +929,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 900e4fc1..7f889c55 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -25,20 +25,16 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - public func textProcessingGetTypes(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ types: [NKTextProcessingTaskType]?, _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func textProcessingGetTypes(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ types: [NKTextProcessingTaskType]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/textprocessing/tasktypes" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -48,7 +44,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -67,23 +62,19 @@ extension NextcloudKit { } } - public func textProcessingSchedule(input: String, - typeId: String, - appId: String = "assistant", - identifier: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - + func textProcessingSchedule(input: String, + typeId: String, + appId: String = "assistant", + identifier: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "/ocs/v2.php/textprocessing/schedule" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] @@ -94,7 +85,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -113,20 +103,16 @@ extension NextcloudKit { } } - public func textProcessingGetTask(taskId: Int, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - + func textProcessingGetTask(taskId: Int, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -136,7 +122,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -155,20 +140,16 @@ extension NextcloudKit { } } - public func textProcessingDeleteTask(taskId: Int, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - + func textProcessingDeleteTask(taskId: Int, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -178,7 +159,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -197,20 +177,16 @@ extension NextcloudKit { } } - public func textProcessingTaskList(appId: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ task: [NKTextProcessingTask]?, _ data: Data?, _ error: NKError) -> Void) { - + func textProcessingTaskList(appId: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ task: [NKTextProcessingTask]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "/ocs/v2.php/textprocessing/tasks/app/\(appId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -220,7 +196,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -251,7 +226,7 @@ public class NKTextProcessingTaskType { self.description = description } - init?(json: JSON) { + public init?(json: JSON) { self.id = json["id"].string self.name = json["name"].string self.description = json["description"].string @@ -286,7 +261,7 @@ public class NKTextProcessingTask { self.completionExpectedAt = completionExpectedAt } - init?(json: JSON) { + public init?(json: JSON) { self.id = json["id"].int self.type = json["type"].string self.status = json["status"].int diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 447e22e1..d0257196 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -24,26 +24,22 @@ import Foundation import Alamofire -extension NextcloudKit { - - @objc public func getComments(fileId: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ items: [NKComments]?, _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func getComments(fileId: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ items: [NKComments]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" - guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let method = HTTPMethod(rawValue: "PROPFIND") let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyComments.data(using: .utf8) @@ -74,24 +70,21 @@ extension NextcloudKit { } } - @objc public func putComments(fileId: String, - message: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func putComments(fileId: String, + message: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" - guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: .post, headers: headers) let parameters = "{\"actorType\":\"users\",\"verb\":\"comment\",\"message\":\"" + message + "\"}" @@ -118,26 +111,23 @@ extension NextcloudKit { } } - @objc public func updateComments(fileId: String, - messageId: String, - message: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func updateComments(fileId: String, + messageId: String, + message: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" - guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let method = HTTPMethod(rawValue: "PROPPATCH") let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) let parameters = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyCommentsUpdate, message) @@ -164,21 +154,18 @@ extension NextcloudKit { } } - @objc public func deleteComments(fileId: String, - messageId: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func deleteComments(fileId: String, + messageId: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" - guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -199,24 +186,21 @@ extension NextcloudKit { } } - @objc public func markAsReadComments(fileId: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func markAsReadComments(fileId: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" - guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let method = HTTPMethod(rawValue: "PROPPATCH") let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) let parameters = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyCommentsMarkAsRead) diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 205f6f13..280f17ce 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -25,28 +25,23 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - public func getDashboardWidget(options: NKRequestOptions = NKRequestOptions(), - request: @escaping (DataRequest?) -> Void = { _ in }, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ dashboardWidgets: [NCCDashboardWidget]?, _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func getDashboardWidget(options: NKRequestOptions = NKRequestOptions(), + request: @escaping (DataRequest?) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ dashboardWidgets: [NCCDashboardWidget]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var url: URLConvertible? - if let endpoint = options.endpoint { url = URL(string: endpoint) } else { let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widgets" url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) } - guard let url = url else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -56,7 +51,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .success(let jsonData): let json = JSON(jsonData) @@ -76,27 +70,23 @@ extension NextcloudKit { options.queue.async { request(dashboardRequest) } } - public func getDashboardWidgetsApplication(_ items: String, - options: NKRequestOptions = NKRequestOptions(), - request: @escaping (DataRequest?) -> Void = { _ in }, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ dashboardApplications: [NCCDashboardApplication]?, _ data: Data?, _ error: NKError) -> Void) { - + func getDashboardWidgetsApplication(_ items: String, + options: NKRequestOptions = NKRequestOptions(), + request: @escaping (DataRequest?) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ dashboardApplications: [NCCDashboardApplication]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var url: URLConvertible? - if let endpoint = options.endpoint { url = URL(string: endpoint) } else { let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widget-items?widgets[]=\(items)" url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) } - guard let url = url else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -106,7 +96,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .success(let jsonData): let json = JSON(jsonData) @@ -127,10 +116,10 @@ extension NextcloudKit { } } -@objc public class NCCDashboardApplication: NSObject { +public class NCCDashboardApplication: NSObject { - @objc public var application: String? - @objc public var items: [NCCDashboardItem]? + public var application: String? + public var items: [NCCDashboardItem]? init?(application: String, data: JSON) { self.application = application @@ -148,13 +137,12 @@ extension NextcloudKit { } } -@objc public class NCCDashboardItem: NSObject { - - @objc public let title: String? - @objc public let subtitle: String? - @objc public let link: String? - @objc public let iconUrl: String? - @objc public let sinceId: Int +public class NCCDashboardItem: NSObject { + public let title: String? + public let subtitle: String? + public let link: String? + public let iconUrl: String? + public let sinceId: Int init?(json: JSON) { self.title = json["title"].string @@ -170,13 +158,12 @@ extension NextcloudKit { } } -@objc public class NCCDashboardWidget: NSObject { - - @objc public var id, title: String - @objc public let order: Int - @objc public let iconClass, iconUrl, widgetUrl: String? - @objc public let itemIconsRound: Bool - @objc public let button: [NCCDashboardWidgetButton]? +public class NCCDashboardWidget: NSObject { + public var id, title: String + public let order: Int + public let iconClass, iconUrl, widgetUrl: String? + public let itemIconsRound: Bool + public let button: [NCCDashboardWidgetButton]? init?(application: String, data: JSON) { guard let id = data["id"].string, @@ -204,9 +191,8 @@ extension NextcloudKit { } } -@objc public class NCCDashboardWidgetButton: NSObject { - - @objc public let type, text, link: String +public class NCCDashboardWidgetButton: NSObject { + public let type, text, link: String init?(data: JSON) { guard let type = data["type"].string, diff --git a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift similarity index 83% rename from Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift rename to Sources/NextcloudKit/NextcloudKit+E2EE.swift index 333f53ff..f4711fca 100644 --- a/Sources/NextcloudKit/E2EE/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -25,29 +25,23 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - @objc public func markE2EEFolder(fileId: String, - delete: Bool, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func markE2EEFolder(fileId: String, + delete: Bool, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/encrypted/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let method: HTTPMethod = delete ? .delete : .put - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -57,7 +51,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -74,29 +67,24 @@ extension NextcloudKit { } } - @objc public func lockE2EEFolder(fileId: String, - e2eToken: String?, - e2eCounter: String?, - method: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ e2eToken: String?, _ data: Data?, _ error: NKError) -> Void) { - + func lockE2EEFolder(fileId: String, + e2eToken: String?, + e2eCounter: String?, + method: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ e2eToken: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/lock/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let method = HTTPMethod(rawValue: method) - var headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] @@ -115,7 +103,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -133,25 +120,21 @@ extension NextcloudKit { } } - @objc public func getE2EEMetadata(fileId: String, - e2eToken: String?, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ e2eMetadata: String?, _ signature: String?, _ data: Data?, _ error: NKError) -> Void) { - + func getE2EEMetadata(fileId: String, + e2eToken: String?, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ e2eMetadata: String?, _ signature: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] @@ -166,7 +149,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -185,30 +167,25 @@ extension NextcloudKit { } } - @objc public func putE2EEMetadata(fileId: String, - e2eToken: String, - e2eMetadata: String?, - signature: String?, - method: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ metadata: String?, _ data: Data?, _ error: NKError) -> Void) { - + func putE2EEMetadata(fileId: String, + e2eToken: String, + e2eMetadata: String?, + signature: String?, + method: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ metadata: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let method = HTTPMethod(rawValue: method) - var headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] @@ -229,7 +206,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -249,21 +225,18 @@ extension NextcloudKit { // MARK: - - @objc public func getE2EECertificate(user: String? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ certificate: String?, _ certificateUser: String?, _ data: Data?, _ error: NKError) -> Void) { - + func getE2EECertificate(user: String? = nil, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ certificate: String?, _ certificateUser: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let userId = self.nkCommonInstance.userId - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } var endpoint = "" - if let user = user { guard let users = ("[\"" + user + "\"]").urlEncoded else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } @@ -272,11 +245,9 @@ extension NextcloudKit { } else { endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -286,7 +257,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -309,23 +279,19 @@ extension NextcloudKit { } } - @objc public func getE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { - + func getE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -335,7 +301,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -353,23 +318,19 @@ extension NextcloudKit { } } - @objc public func getE2EEPublicKey(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { - + func getE2EEPublicKey(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/server-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -379,7 +340,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -397,26 +357,21 @@ extension NextcloudKit { } } - @objc public func signE2EECertificate(certificate: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ certificate: String?, _ data: Data?, _ error: NKError) -> Void) { - + func signE2EECertificate(certificate: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ certificate: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let parameters = ["csr": certificate] sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -426,7 +381,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -445,26 +399,21 @@ extension NextcloudKit { } } - @objc public func storeE2EEPrivateKey(privateKey: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { - + func storeE2EEPrivateKey(privateKey: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let parameters = ["privateKey": privateKey] sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -474,7 +423,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -492,23 +440,19 @@ extension NextcloudKit { } } - @objc public func deleteE2EECertificate(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func deleteE2EECertificate(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -518,7 +462,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -529,23 +472,19 @@ extension NextcloudKit { } } - @objc public func deleteE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func deleteE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -555,7 +494,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index f3faa608..d4b7f0d2 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -26,24 +26,18 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - // available in NC >= 24 - @objc public func lockUnlockFile(serverUrlFileName: String, - shouldLock: Bool, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func lockUnlockFile(serverUrlFileName: String, + shouldLock: Bool, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let method = HTTPMethod(rawValue: shouldLock ? "LOCK" : "UNLOCK") - var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "X-User-Lock", value: "1") @@ -54,7 +48,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 8ebb27ed..68b63269 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -25,22 +25,17 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - @objc public func getGroupfolders(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ results: [NKGroupfolders]?, _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func getGroupfolders(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ results: [NKGroupfolders]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "index.php/apps/groupfolders/folders?applicable=1" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -50,7 +45,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -76,17 +70,16 @@ extension NextcloudKit { } } -@objc public class NKGroupfolders: NSObject { - - @objc public let id: Int - @objc public let mountPoint: String - @objc public let acl: Bool - @objc public let size: Int - @objc public let quota: Int - @objc public let manage: Data? - @objc public let groups: [String: Any]? +public class NKGroupfolders: NSObject { + public let id: Int + public let mountPoint: String + public let acl: Bool + public let size: Int + public let quota: Int + public let manage: Data? + public let groups: [String: Any]? - internal init?(json: JSON) { + init?(json: JSON) { guard let id = json["id"].int, let mountPoint = json["mount_point"].string, let acl = json["acl"].bool, diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index 9b858c16..9509295c 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -26,23 +26,18 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - @objc public func getHovercard(for userId: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ result: NKHovercard?, _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func getHovercard(for userId: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ result: NKHovercard?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/hovercard/v1/\(userId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -52,7 +47,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -73,8 +67,11 @@ extension NextcloudKit { } } -@objc public class NKHovercard: NSObject { - internal init?(jsonData: JSON) { +public class NKHovercard: NSObject { + public let userId, displayName: String + public let actions: [Action] + + init?(jsonData: JSON) { guard let userId = jsonData["userId"].string, let displayName = jsonData["displayName"].string, let actions = jsonData["actions"].array?.compactMap(Action.init) @@ -86,8 +83,14 @@ extension NextcloudKit { self.actions = actions } - @objc public class Action: NSObject { - internal init?(jsonData: JSON) { + public class Action: NSObject { + public let title: String + public let icon: String + public let hyperlink: String + public var hyperlinkUrl: URL? { URL(string: hyperlink) } + public let appId: String + + init?(jsonData: JSON) { guard let title = jsonData["title"].string, let icon = jsonData["icon"].string, let hyperlink = jsonData["hyperlink"].string, @@ -100,14 +103,5 @@ extension NextcloudKit { self.hyperlink = hyperlink self.appId = appId } - - @objc public let title: String - @objc public let icon: String - @objc public let hyperlink: String - @objc public var hyperlinkUrl: URL? { URL(string: hyperlink) } - @objc public let appId: String } - - @objc public let userId, displayName: String - @objc public let actions: [Action] } diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 1c79250a..6f589252 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -24,24 +24,20 @@ import Foundation import Alamofire -extension NextcloudKit { - - public func setLivephoto(serverUrlfileNamePath: String, - livePhotoFile: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func setLivephoto(serverUrlfileNamePath: String, + livePhotoFile: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - guard let url = serverUrlfileNamePath.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let method = HTTPMethod(rawValue: "PROPPATCH") let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) let parameters = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyLivephoto, livePhotoFile) @@ -57,7 +53,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -67,16 +62,4 @@ extension NextcloudKit { } } } - - @available(iOS 13.0, *) - public func setLivephoto(serverUrlfileNamePath: String, - livePhotoFile: String, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { - - await withUnsafeContinuation({ continuation in - setLivephoto(serverUrlfileNamePath: serverUrlfileNamePath, livePhotoFile: livePhotoFile, options: options) { account, error in - continuation.resume(returning: (account: account, error: error)) - } - }) - } } diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 52ef2468..e67c712d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -25,31 +25,26 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - +public extension NextcloudKit { // MARK: - App Password - - @objc public func getAppPassword(serverUrl: String, - username: String, - password: String, - userAgent: String? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ token: String?, _ data: Data?, _ error: NKError) -> Void) { - + func getAppPassword(serverUrl: String, + username: String, + password: String, + userAgent: String? = nil, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ token: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/getapppassword" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(nil, nil, .urlError) } } - var headers: HTTPHeaders = [.authorization(username: username, password: password)] if let userAgent = userAgent { headers.update(.userAgent(userAgent)) } headers.update(name: "OCS-APIRequest", value: "true") - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: HTTPMethod(rawValue: "GET"), headers: headers) } catch { @@ -63,7 +58,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -79,27 +73,24 @@ extension NextcloudKit { } } - @objc public func deleteAppPassword(serverUrl: String, - username: String, - password: String, - userAgent: String? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { - + func deleteAppPassword(serverUrl: String, + username: String, + password: String, + userAgent: String? = nil, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/apppassword" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(nil, .urlError) } } - var headers: HTTPHeaders = [.authorization(username: username, password: password)] if let userAgent = userAgent { headers.update(.userAgent(userAgent)) } headers.update(name: "OCS-APIRequest", value: "true") - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: HTTPMethod(rawValue: "DELETE"), headers: headers) } catch { @@ -113,7 +104,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -126,20 +116,16 @@ extension NextcloudKit { // MARK: - Login Flow V2 - @objc public func getLoginFlowV2(serverUrl: String, - userAgent: String? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ token: String?, _ endpoint: String?, _ login: String?, _ data: Data?, _ error: NKError) -> Void) { - + func getLoginFlowV2(serverUrl: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ token: String?, _ endpoint: String?, _ login: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "index.php/login/v2" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(nil, nil, nil, nil, .urlError) } } - var headers: HTTPHeaders? - if let userAgent = userAgent { + if let userAgent = options.customUserAgent { headers = [HTTPHeader.userAgent(userAgent)] } @@ -150,7 +136,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -167,21 +152,17 @@ extension NextcloudKit { } } - @objc public func getLoginFlowV2Poll(token: String, - endpoint: String, - userAgent: String? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ server: String?, _ loginName: String?, _ appPassword: String?, _ data: Data?, _ error: NKError) -> Void) { - + func getLoginFlowV2Poll(token: String, + endpoint: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ server: String?, _ loginName: String?, _ appPassword: String?, _ data: Data?, _ error: NKError) -> Void) { let serverUrl = endpoint + "?token=" + token - guard let url = serverUrl.asUrl else { return options.queue.async { completion(nil, nil, nil, nil, .urlError) } } - var headers: HTTPHeaders? - if let userAgent = userAgent { + if let userAgent = options.customUserAgent { headers = [HTTPHeader.userAgent(userAgent)] } @@ -192,14 +173,12 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) options.queue.async { completion(nil, nil, nil, nil, error) } case .success(let jsonData): let json = JSON(jsonData) - let server = json["server"].string let loginName = json["loginName"].string let appPassword = json["appPassword"].string diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index c7f5b096..7639ac2a 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -25,24 +25,18 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - @objc public func NCTextObtainEditorDetails(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func NCTextObtainEditorDetails(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing" - var editors: [NKEditorDetailsEditors] = [] var creators: [NKEditorDetailsCreators] = [] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, editors, creators, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -52,7 +46,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -88,7 +81,6 @@ extension NextcloudKit { creator.mimetype = subJson["mimetype"].stringValue creator.name = subJson["name"].stringValue creator.templates = subJson["templates"].intValue - creators.append(creator) } @@ -97,29 +89,24 @@ extension NextcloudKit { } } - @objc public func NCTextOpenFile(fileNamePath: String, - fileId: String? = nil, - editor: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - + func NCTextOpenFile(fileNamePath: String, + fileId: String? = nil, + editor: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } - var endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/open?path=/\(fileNamePath)&editorId=\(editor)" if let fileId = fileId { endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/open?path=/\(fileNamePath)&fileId=\(fileId)&editorId=\(editor)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -129,7 +116,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -142,21 +128,16 @@ extension NextcloudKit { } } - @objc public func NCTextGetListOfTemplates(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ templates: [NKEditorTemplates], _ data: Data?, _ error: NKError) -> Void) { - + func NCTextGetListOfTemplates(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ templates: [NKEditorTemplates], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/templates/text/textdocumenttemplate" - var templates: [NKEditorTemplates] = [] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, templates, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -166,7 +147,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -182,7 +162,6 @@ extension NextcloudKit { template.identifier = subJson["id"].stringValue template.name = subJson["name"].stringValue template.preview = subJson["preview"].stringValue - templates.append(template) } @@ -191,33 +170,27 @@ extension NextcloudKit { } } - @objc public func NCTextCreateFile(fileNamePath: String, - editorId: String, - creatorId: String, - templateId: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - + func NCTextCreateFile(fileNamePath: String, + editorId: String, + creatorId: String, + templateId: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } - var endpoint = "" - if templateId.isEmpty { endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/create?path=/\(fileNamePath)&editorId=\(editorId)&creatorId=\(creatorId)" } else { endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/create?path=/\(fileNamePath)&editorId=\(editorId)&creatorId=\(creatorId)&templateId=\(templateId)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -227,7 +200,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index 134e9b2c..003f063a 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -25,31 +25,26 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - @objc public func subscribingPushNotification(serverUrl: String, - account: String, - user: String, - password: String, - pushTokenHash: String, - devicePublicKey: String, - proxyServerUrl: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func subscribingPushNotification(serverUrl: String, + account: String, + user: String, + password: String, + pushTokenHash: String, + devicePublicKey: String, + proxyServerUrl: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } - let parameters = [ "pushTokenHash": pushTokenHash, "devicePublicKey": devicePublicKey, "proxyServer": proxyServerUrl ] - let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -59,7 +54,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -79,20 +73,17 @@ extension NextcloudKit { } } - @objc public func unsubscribingPushNotification(serverUrl: String, - account: String, - user: String, - password: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func unsubscribingPushNotification(serverUrl: String, + account: String, + user: String, + password: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -102,7 +93,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -113,29 +103,25 @@ extension NextcloudKit { } } - @objc public func subscribingPushProxy(proxyServerUrl: String, - pushToken: String, - deviceIdentifier: String, - signature: String, - publicKey: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ error: NKError) -> Void) { - + func subscribingPushProxy(proxyServerUrl: String, + pushToken: String, + deviceIdentifier: String, + signature: String, + publicKey: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ error: NKError) -> Void) { let endpoint = "devices?format=json" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), let userAgent = options.customUserAgent else { return options.queue.async { completion(.urlError) } } - let parameters = [ "pushToken": pushToken, "deviceIdentifier": deviceIdentifier, "deviceIdentifierSignature": signature, "userPublicKey": publicKey ] - let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -145,7 +131,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -156,27 +141,23 @@ extension NextcloudKit { } } - @objc public func unsubscribingPushProxy(proxyServerUrl: String, - deviceIdentifier: String, - signature: String, - publicKey: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ error: NKError) -> Void) { - + func unsubscribingPushProxy(proxyServerUrl: String, + deviceIdentifier: String, + signature: String, + publicKey: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ error: NKError) -> Void) { let endpoint = "devices" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), let userAgent = options.customUserAgent else { return options.queue.async { completion(.urlError) } } - let parameters = [ "deviceIdentifier": deviceIdentifier, "deviceIdentifierSignature": signature, "userPublicKey": publicKey ] - let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) sessionManager.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -186,7 +167,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index d2574b06..215193e5 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -25,24 +25,18 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - @objc public func createUrlRichdocuments(fileID: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func createUrlRichdocuments(fileID: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/document" - let parameters: [String: Any] = ["fileId": fileID] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -52,7 +46,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -69,20 +62,16 @@ extension NextcloudKit { } } - @objc public func getTemplatesRichdocuments(typeTemplate: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ templates: [NKRichdocumentsTemplate]?, _ data: Data?, _ error: NKError) -> Void) { - + func getTemplatesRichdocuments(typeTemplate: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ templates: [NKRichdocumentsTemplate]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/\(typeTemplate)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -92,7 +81,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -111,7 +99,6 @@ extension NextcloudKit { template.name = templateJSON["name"].stringValue template.preview = templateJSON["preview"].stringValue template.type = templateJSON["type"].stringValue - templates.append(template) } options.queue.async { completion(account, templates, jsonData, .success) } @@ -122,23 +109,18 @@ extension NextcloudKit { } } - @objc public func createRichdocuments(path: String, - templateId: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - + func createRichdocuments(path: String, + templateId: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/new" - let parameters: [String: Any] = ["path": path, "template": templateId] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -148,7 +130,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -165,22 +146,17 @@ extension NextcloudKit { } } - @objc public func createAssetRichdocuments(path: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - + func createAssetRichdocuments(path: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "index.php/apps/richdocuments/assets" - let parameters: [String: Any] = ["path": path] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -190,7 +166,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index fda46024..165ad45e 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -26,8 +26,7 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - +public extension NextcloudKit { /// Available NC >= 20 /// Search many different datasources in the cloud and combine them into one result. /// @@ -42,26 +41,22 @@ extension NextcloudKit { /// - filter: Filter search provider that should be searched. Default is all available provider.. /// - update: Callback, notifying that a search provider return its result. Does not include previous results. /// - completion: Callback, notifying that all search providers have been searched. The search is done. Includes all search results. - public func unifiedSearch(term: String, - options: NKRequestOptions = NKRequestOptions(), - timeout: TimeInterval = 30, - timeoutProvider: TimeInterval = 60, - 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, _ data: Data?, _ error: NKError) -> Void) { - + func unifiedSearch(term: String, + timeout: TimeInterval = 30, + timeoutProvider: TimeInterval = 60, + 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, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/search/providers" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return completion(account, nil, .urlError) } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let requestUnifiedSearch = sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -71,7 +66,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .success(let jsonData): let json = JSON(jsonData) @@ -79,7 +73,6 @@ extension NextcloudKit { guard let allProvider = NKSearchProvider.factory(jsonArray: providerData) else { return completion(account, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } - providers(account, allProvider) let filteredProviders = allProvider.filter(filter) @@ -120,34 +113,27 @@ extension NextcloudKit { /// - timeout: Filter search provider that should be searched. Default is all available provider.. /// - update: Callback, notifying that a search provider return its result. Does not include previous results. /// - completion: Callback, notifying that all search results. - @discardableResult - public func searchProvider(_ id: String, - account: String, - term: String, - limit: Int? = nil, - cursor: Int? = nil, - options: NKRequestOptions = NKRequestOptions(), - timeout: TimeInterval = 60, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ accoun: String, NKSearchResult?, _ data: Data?, _ error: NKError) -> Void) -> DataRequest? { - + func searchProvider(_ id: String, + account: String, + term: String, + limit: Int? = nil, + cursor: Int? = nil, + options: NKRequestOptions = NKRequestOptions(), + timeout: TimeInterval = 60, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ accoun: String, NKSearchResult?, _ data: Data?, _ error: NKError) -> Void) -> DataRequest? { let urlBase = self.nkCommonInstance.urlBase - guard let term = term.urlEncoded else { completion(account, nil, nil, .urlError) return nil } - var endpoint = "ocs/v2.php/search/providers/\(id)/search?term=\(term)" - if let limit = limit { endpoint += "&limit=\(limit)" } - if let cursor = cursor { endpoint += "&cursor=\(cursor)" } - guard let url = self.nkCommonInstance.createStandardUrl( serverUrl: urlBase, endpoint: endpoint) @@ -155,10 +141,9 @@ extension NextcloudKit { completion(account, nil, nil, .urlError) return nil } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: .get, headers: headers) urlRequest.timeoutInterval = timeout @@ -192,12 +177,11 @@ extension NextcloudKit { } } -@objc public class NKSearchResult: NSObject { - - @objc public let id: String - @objc public let name: String - @objc public let isPaginated: Bool - @objc public let entries: [NKSearchEntry] +public class NKSearchResult: NSObject { + public let id: String + public let name: String + public let isPaginated: Bool + public let entries: [NKSearchEntry] public let cursor: Int? init?(json: JSON, id: String) { @@ -213,21 +197,18 @@ extension NextcloudKit { } } -@objc public class NKSearchEntry: NSObject { - - @objc public let thumbnailURL: String - @objc public let title, subline: String - @objc public let resourceURL: String - @objc public let icon: String - @objc public let rounded: Bool - @objc public let attributes: [String: Any]? - +public class NKSearchEntry: NSObject { + public let thumbnailURL: String + public let title, subline: String + public let resourceURL: String + public let icon: String + public let rounded: Bool + public let attributes: [String: Any]? public var fileId: Int? { guard let fileAttribute = attributes?["fileId"] as? String else { return nil } return Int(fileAttribute) } - - @objc public var filePath: String? { + public var filePath: String? { attributes?["path"] as? String } @@ -255,7 +236,9 @@ extension NextcloudKit { } } -@objc public class NKSearchProvider: NSObject { +public class NKSearchProvider: NSObject { + public let id, name: String + public let order: Int init?(json: JSON) { guard let id = json["id"].string, @@ -267,9 +250,6 @@ extension NextcloudKit { self.order = order } - @objc public let id, name: String - @objc public let order: Int - static func factory(jsonArray: JSON) -> [NKSearchProvider]? { guard let allProvider = jsonArray.array else { return nil } return allProvider.compactMap(NKSearchProvider.init) diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index f52a1d43..27d08118 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -25,14 +25,34 @@ import Foundation import Alamofire import SwiftyJSON -@objc public class NKShareParameter: NSObject { +public class NKShareParameter: NSObject { + let path: String? + let idShare: Int + let reshares: Bool + let subfiles: Bool + let sharedWithMe: Bool + internal var endpoint: String { + guard idShare > 0 else { + return "ocs/v2.php/apps/files_sharing/api/v1/shares" + } + return "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" + } + internal var queryParameters: [String: String] { + var parameters = [ + "reshares": reshares ? "true" : "false", + "subfiles": subfiles ? "true" : "false", + "shared_with_me": sharedWithMe ? "true" : "false" + ] + parameters["path"] = path + return parameters + } /// - Parameters: /// - path: Path to file or folder /// - reshares: If set to false (default), only shares owned by the current user are returned. If set to true, shares owned by any user from the given file are returned. /// - subfiles: If set to false (default), lists only the folder being shared. If set to true, all shared files within the folder are returned. /// - sharedWithMe: (?) retrieve all shares, if set to true - @objc public init(path: String? = nil, reshares: Bool = false, subfiles: Bool = false, sharedWithMe: Bool = false) { + public init(path: String? = nil, reshares: Bool = false, subfiles: Bool = false, sharedWithMe: Bool = false) { self.path = path self.idShare = 0 self.reshares = reshares @@ -45,53 +65,26 @@ import SwiftyJSON /// - reshares: If set to false (default), only shares owned by the current user are returned. If set to true, shares owned by any user from the given file are returned. /// - subfiles: If set to false (default), lists only the folder being shared. If set to true, all shared files within the folder are returned. /// - sharedWithMe: (?) retrieve all shares, if set to true - @objc public init(idShare: Int, reshares: Bool = false, subfiles: Bool = false, sharedWithMe: Bool = false) { + public init(idShare: Int, reshares: Bool = false, subfiles: Bool = false, sharedWithMe: Bool = false) { self.path = nil self.idShare = idShare self.reshares = reshares self.subfiles = subfiles self.sharedWithMe = sharedWithMe } - - let path: String? - let idShare: Int - let reshares: Bool - let subfiles: Bool - let sharedWithMe: Bool - - internal var endpoint: String { - guard idShare > 0 else { - return "ocs/v2.php/apps/files_sharing/api/v1/shares" - } - return "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" - } - - internal var queryParameters: [String: String] { - var parameters = [ - "reshares": reshares ? "true" : "false", - "subfiles": subfiles ? "true" : "false", - "shared_with_me": sharedWithMe ? "true" : "false" - ] - parameters["path"] = path - return parameters - } } -extension NextcloudKit { - - @objc public func readShares(parameters: NKShareParameter, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ shares: [NKShare]?, _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func readShares(parameters: NKShareParameter, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ shares: [NKShare]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: parameters.endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -101,7 +94,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -132,31 +124,24 @@ extension NextcloudKit { * @param perPage The number of items per page (default 200) * @param lookup Default false, for global search use true */ - - @objc public func searchSharees(search: String = "", - page: Int = 1, perPage: Int = 200, - itemType: String = "file", - lookup: Bool = false, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ sharees: [NKSharee]?, _ data: Data?, _ error: NKError) -> Void) { - + func searchSharees(search: String = "", + page: Int = 1, perPage: Int = 200, + itemType: String = "file", + lookup: Bool = false, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ sharees: [NKSharee]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/sharees" - var lookupString = "false" if lookup { lookupString = "true" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let parameters = [ "search": search, "page": String(page), @@ -172,7 +157,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -197,7 +181,7 @@ extension NextcloudKit { sharee.circleOwner = subJson["value"]["circleOwner"].stringValue if let clearAt = subJson["status"]["clearAt"].double { - let date = Date(timeIntervalSince1970: clearAt) as NSDate + let date = Date(timeIntervalSince1970: clearAt) sharee.userClearAt = date } sharee.userIcon = subJson["status"]["icon"].stringValue @@ -220,7 +204,7 @@ extension NextcloudKit { sharee.circleOwner = subJson["value"]["circleOwner"].stringValue if let clearAt = subJson["status"]["clearAt"].double { - let date = Date(timeIntervalSince1970: clearAt) as NSDate + let date = Date(timeIntervalSince1970: clearAt) sharee.userClearAt = date } sharee.userIcon = subJson["status"]["icon"].stringValue @@ -257,15 +241,14 @@ extension NextcloudKit { * @param attributes There is currently only one share attribute “download” from the scope “permissions”. This attribute is only valid for user and group shares, not for public link shares. */ - @objc public func createShareLink(path: String, - hideDownload: Bool = false, - publicUpload: Bool = false, - password: String? = nil, - permissions: Int = 1, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - + func createShareLink(path: String, + hideDownload: Bool = false, + publicUpload: Bool = false, + password: String? = nil, + permissions: Int = 1, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { createShare(path: path, shareType: 3, shareWith: nil, publicUpload: publicUpload, hideDownload: hideDownload, password: password, permissions: permissions, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) @@ -274,17 +257,16 @@ extension NextcloudKit { } } - @objc public func createShare(path: String, - shareType: Int, - shareWith: String, - password: String? = nil, - note: String? = nil, - permissions: Int = 1, - options: NKRequestOptions = NKRequestOptions(), - attributes: String? = nil, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - + func createShare(path: String, + shareType: Int, + shareWith: String, + password: String? = nil, + note: String? = nil, + permissions: Int = 1, + attributes: String? = nil, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { createShare(path: path, shareType: shareType, shareWith: shareWith, publicUpload: false, note: note, hideDownload: false, password: password, permissions: permissions, attributes: attributes, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) @@ -308,15 +290,11 @@ extension NextcloudKit { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - var parameters = [ "path": path, "shareType": String(shareType), @@ -348,7 +326,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -385,30 +362,25 @@ extension NextcloudKit { * @param attributes There is currently only one share attribute “download” from the scope “permissions”. This attribute is only valid for user and group shares, not for public link shares. */ - @objc public func updateShare(idShare: Int, - password: String? = nil, - expireDate: String? = nil, - permissions: Int = 1, - publicUpload: Bool = false, - note: String? = nil, - label: String? = nil, - hideDownload: Bool, - attributes: String? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - + func updateShare(idShare: Int, + password: String? = nil, + expireDate: String? = nil, + permissions: Int = 1, + publicUpload: Bool = false, + note: String? = nil, + label: String? = nil, + hideDownload: Bool, + attributes: String? = nil, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - var parameters = [ "permissions": String(permissions) ] @@ -439,7 +411,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -459,21 +430,16 @@ extension NextcloudKit { /* * @param idShare Identifier of the share to update */ - - @objc public func deleteShare(idShare: Int, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func deleteShare(idShare: Int, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -483,7 +449,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -505,7 +470,7 @@ extension NextcloudKit { share.displaynameFileOwner = json["displayname_file_owner"].stringValue share.displaynameOwner = json["displayname_owner"].stringValue if let expiration = json["expiration"].string, let date = self.nkCommonInstance.convertDate(expiration, format: "YYYY-MM-dd HH:mm:ss") { - share.expirationDate = date + share.expirationDate = date as NSDate } share.fileParent = json["file_parent"].intValue share.fileSource = json["file_source"].intValue @@ -527,7 +492,7 @@ extension NextcloudKit { share.shareWith = json["share_with"].stringValue share.shareWithDisplayname = json["share_with_displayname"].stringValue if let stime = json["stime"].double { - let date = Date(timeIntervalSince1970: stime) as NSDate + let date = Date(timeIntervalSince1970: stime) share.date = date } share.storage = json["storage"].intValue @@ -537,7 +502,7 @@ extension NextcloudKit { share.uidOwner = json["uid_owner"].stringValue share.url = json["url"].stringValue if let clearAt = json["status"]["clearAt"].double { - let date = Date(timeIntervalSince1970: clearAt) as NSDate + let date = Date(timeIntervalSince1970: clearAt) share.userClearAt = date } share.userIcon = json["status"]["icon"].stringValue @@ -549,43 +514,42 @@ extension NextcloudKit { } } -@objc public class NKShare: NSObject { - - @objc public var account = "" - @objc public var canEdit: Bool = false - @objc public var canDelete: Bool = false - @objc public var date: NSDate? - @objc public var displaynameFileOwner = "" - @objc public var displaynameOwner = "" - @objc public var expirationDate: NSDate? - @objc public var fileParent: Int = 0 - @objc public var fileSource: Int = 0 - @objc public var fileTarget = "" - @objc public var hideDownload: Bool = false - @objc public var idShare: Int = 0 - @objc public var itemSource: Int = 0 - @objc public var itemType = "" - @objc public var label = "" - @objc public var mailSend: Bool = false - @objc public var mimeType = "" - @objc public var note = "" - @objc public var parent = "" - @objc public var password = "" - @objc public var path = "" - @objc public var permissions: Int = 0 - @objc public var sendPasswordByTalk: Bool = false - @objc public var shareType: Int = 0 - @objc public var shareWith = "" - @objc public var shareWithDisplayname = "" - @objc public var storage: Int = 0 - @objc public var storageId = "" - @objc public var token = "" - @objc public var uidFileOwner = "" - @objc public var uidOwner = "" - @objc public var url = "" - @objc public var userClearAt: NSDate? - @objc public var userIcon = "" - @objc public var userMessage = "" - @objc public var userStatus = "" - @objc public var attributes: String? +public class NKShare: NSObject { + public var account = "" + public var canEdit: Bool = false + public var canDelete: Bool = false + public var date: Date? + public var displaynameFileOwner = "" + public var displaynameOwner = "" + public var expirationDate: NSDate? + public var fileParent: Int = 0 + public var fileSource: Int = 0 + public var fileTarget = "" + public var hideDownload: Bool = false + public var idShare: Int = 0 + public var itemSource: Int = 0 + public var itemType = "" + public var label = "" + public var mailSend: Bool = false + public var mimeType = "" + public var note = "" + public var parent = "" + public var password = "" + public var path = "" + public var permissions: Int = 0 + public var sendPasswordByTalk: Bool = false + public var shareType: Int = 0 + public var shareWith = "" + public var shareWithDisplayname = "" + public var storage: Int = 0 + public var storageId = "" + public var token = "" + public var uidFileOwner = "" + public var uidOwner = "" + public var url = "" + public var userClearAt: Date? + public var userIcon = "" + public var userMessage = "" + public var userStatus = "" + public var attributes: String? } diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index 3be34d80..eaa58f94 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -25,25 +25,20 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - @objc public func getUserStatus(userId: String? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ clearAt: NSDate?, _ icon: String?, _ message: String?, _ messageId: String?, _ messageIsPredefined: Bool, _ status: String?, _ statusIsUserDefined: Bool, _ userId: String?, _ data: Data?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func getUserStatus(userId: String? = nil, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ clearAt: Date?, _ icon: String?, _ message: String?, _ messageId: String?, _ messageIsPredefined: Bool, _ status: String?, _ statusIsUserDefined: Bool, _ userId: String?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - var endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status" if let userId = userId { endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/\(userId)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -53,7 +48,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -63,9 +57,9 @@ extension NextcloudKit { let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - var clearAt: NSDate? + var clearAt: Date? if let clearAtDouble = json["ocs"]["data"]["clearAt"].double { - clearAt = Date(timeIntervalSince1970: clearAtDouble) as NSDate + clearAt = Date(timeIntervalSince1970: clearAtDouble) } let icon = json["ocs"]["data"]["icon"].string let message = json["ocs"]["data"]["message"].string @@ -83,22 +77,17 @@ extension NextcloudKit { } } - @objc public func setUserStatus(status: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func setUserStatus(status: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/status" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let parameters = [ "statusType": String(status) ] @@ -110,7 +99,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -127,23 +115,18 @@ extension NextcloudKit { } } - @objc public func setCustomMessagePredefined(messageId: String, - clearAt: Double, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func setCustomMessagePredefined(messageId: String, + clearAt: Double, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/predefined" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - var parameters = [ "messageId": String(messageId) ] @@ -158,7 +141,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -176,24 +158,19 @@ extension NextcloudKit { } } - @objc public func setCustomMessageUserDefined(statusIcon: String?, - message: String, - clearAt: Double, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func setCustomMessageUserDefined(statusIcon: String?, + message: String, + clearAt: Double, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/custom" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - var parameters = [ "message": String(message) ] @@ -211,15 +188,14 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) options.queue.async { completion(account, error) } case .success(let jsonData): let json = JSON(jsonData) - let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError + if statusCode == 200 { options.queue.async { completion(account, .success) } } else { @@ -229,19 +205,15 @@ extension NextcloudKit { } } - @objc public func clearMessage(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func clearMessage(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase - let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -251,15 +223,14 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) options.queue.async { completion(account, error) } case .success(let jsonData): let json = JSON(jsonData) - let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError + if statusCode == 200 { options.queue.async { completion(account, .success) } } else { @@ -269,20 +240,16 @@ extension NextcloudKit { } } - @objc public func getUserStatusPredefinedStatuses(options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { - + func getUserStatusPredefinedStatuses(options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var userStatuses: [NKUserStatus] = [] - let endpoint = "ocs/v2.php/apps/user_status/api/v1/predefined_statuses" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -292,7 +259,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -301,8 +267,8 @@ extension NextcloudKit { let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - let ocsdata = json["ocs"]["data"] + for (_, subJson): (String, JSON) in ocsdata { let userStatus = NKUserStatus() @@ -316,10 +282,8 @@ extension NextcloudKit { userStatus.id = subJson["id"].string userStatus.message = subJson["message"].string userStatus.predefined = true - userStatuses.append(userStatus) } - options.queue.async { completion(account, userStatuses, jsonData, .success) } } else { options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } @@ -328,24 +292,19 @@ extension NextcloudKit { } } - @objc public func getUserStatusRetrieveStatuses(limit: Int, - offset: Int, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { - + func getUserStatusRetrieveStatuses(limit: Int, + offset: Int, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var userStatuses: [NKUserStatus] = [] - let endpoint = "ocs/v2.php/apps/user_status/api/v1/statuses" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let parameters = [ "limit": String(limit), "offset": String(offset) @@ -358,7 +317,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -367,28 +325,24 @@ extension NextcloudKit { let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - let ocsdata = json["ocs"]["data"] + for (_, subJson): (String, JSON) in ocsdata { let userStatus = NKUserStatus() if let value = subJson["clearAt"].double { if value > 0 { - userStatus.clearAt = NSDate(timeIntervalSince1970: value) + userStatus.clearAt = Date(timeIntervalSince1970: value) } } userStatus.icon = subJson["icon"].string userStatus.message = subJson["message"].string userStatus.predefined = false userStatus.userId = subJson["userId"].string - userStatuses.append(userStatus) } - options.queue.async { completion(account, userStatuses, jsonData, .success) } - } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } diff --git a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift similarity index 77% rename from Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift rename to Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 124ef509..b7a48ab9 100644 --- a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -25,24 +25,19 @@ import Foundation import Alamofire import SwiftyJSON -extension NextcloudKit { - - @objc public func createFolder(serverUrlFileName: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ ocId: String?, _ date: NSDate?, _ error: NKError) -> Void) { - +public extension NextcloudKit { + func createFolder(serverUrlFileName: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let method = HTTPMethod(rawValue: "MKCOL") - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -57,7 +52,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -77,20 +71,17 @@ extension NextcloudKit { } } - @objc public func deleteFileOrFolder(serverUrlFileName: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func deleteFileOrFolder(serverUrlFileName: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: .delete, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -105,7 +96,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -116,21 +106,17 @@ extension NextcloudKit { } } - @objc public func moveFileOrFolder(serverUrlFileNameSource: String, - serverUrlFileNameDestination: String, - overwrite: Bool, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func moveFileOrFolder(serverUrlFileNameSource: String, + serverUrlFileNameDestination: String, + overwrite: Bool, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - guard let url = serverUrlFileNameSource.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let method = HTTPMethod(rawValue: "MOVE") - var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") if overwrite { @@ -138,8 +124,8 @@ extension NextcloudKit { } else { headers.update(name: "Overwrite", value: "F") } - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -154,7 +140,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -165,21 +150,17 @@ extension NextcloudKit { } } - @objc public func copyFileOrFolder(serverUrlFileNameSource: String, - serverUrlFileNameDestination: String, - overwrite: Bool, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func copyFileOrFolder(serverUrlFileNameSource: String, + serverUrlFileNameDestination: String, + overwrite: Bool, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - guard let url = serverUrlFileNameSource.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let method = HTTPMethod(rawValue: "COPY") - var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") if overwrite { @@ -187,8 +168,8 @@ extension NextcloudKit { } else { headers.update(name: "Overwrite", value: "F") } - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -203,7 +184,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -214,15 +194,14 @@ extension NextcloudKit { } } - @objc public func readFileOrFolder(serverUrlFileName: String, - depth: String, - showHiddenFiles: Bool = true, - includeHiddenFiles: [String] = [], - requestBody: Data? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - + func readFileOrFolder(serverUrlFileName: String, + depth: String, + showHiddenFiles: Bool = true, + includeHiddenFiles: [String] = [], + requestBody: Data? = nil, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let user = self.nkCommonInstance.user let userId = self.nkCommonInstance.userId @@ -230,30 +209,26 @@ extension NextcloudKit { let dav = self.nkCommonInstance.dav var files: [NKFile] = [] var serverUrlFileName = serverUrlFileName - guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, files, nil, .urlError) } } - if depth == "0", serverUrlFileName.last == "/" { serverUrlFileName = String(serverUrlFileName.dropLast()) } else if depth != "0", serverUrlFileName.last != "/" { serverUrlFileName = serverUrlFileName + "/" } - let method = HTTPMethod(rawValue: "PROPFIND") - var headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") headers.update(name: "Depth", value: depth) - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) if requestBody != nil { urlRequest.httpBody = requestBody! urlRequest.timeoutInterval = options.timeout } else { - urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyFile.data(using: .utf8) + urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFile(removeProperties: options.removeProperties).data(using: .utf8) } } catch { return options.queue.async { completion(account, files, nil, NKError(error: error)) } @@ -266,7 +241,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -282,23 +256,21 @@ extension NextcloudKit { } } - @objc public func getFileFromFileId(fileId: String? = nil, - link: String? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ file: NKFile?, _ data: Data?, _ error: NKError) -> Void) { - + func getFileFromFileId(fileId: String? = nil, + link: String? = nil, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ file: NKFile?, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase var httpBody: Data? - if let fileId = fileId { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodySearchFileId, userId, fileId).data(using: .utf8)! + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(removeProperties: options.removeProperties), userId, fileId).data(using: .utf8)! } else if let link = link { let linkArray = link.components(separatedBy: "/") if let fileId = linkArray.last { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodySearchFileId, userId, fileId).data(using: .utf8)! + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(removeProperties: options.removeProperties), userId, fileId).data(using: .utf8)! } } guard let httpBody = httpBody else { @@ -313,14 +285,13 @@ extension NextcloudKit { } } - @objc public func searchBodyRequest(serverUrl: String, - requestBody: String, - showHiddenFiles: Bool, - includeHiddenFiles: [String] = [], - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - + func searchBodyRequest(serverUrl: String, + requestBody: String, + showHiddenFiles: Bool, + includeHiddenFiles: [String] = [], + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let httpBody = requestBody.data(using: .utf8)! search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in @@ -330,23 +301,20 @@ extension NextcloudKit { } } - @objc public func searchLiteral(serverUrl: String, - depth: String, - literal: String, - showHiddenFiles: Bool, - includeHiddenFiles: [String] = [], - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - + func searchLiteral(serverUrl: String, + depth: String, + literal: String, + showHiddenFiles: Bool, + includeHiddenFiles: [String] = [], + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId - guard let href = ("/files/" + userId).urlEncoded else { return options.queue.async { completion(account, [], nil, .urlError) } } - - let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodySearchFileName, href, depth, "%" + literal + "%") + let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(removeProperties: options.removeProperties), href, depth, "%" + literal + "%") let httpBody = requestBody.data(using: .utf8)! search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in @@ -356,47 +324,41 @@ extension NextcloudKit { } } - @objc public func searchMedia(path: String = "", - lessDate: Any, - greaterDate: Any, - elementDate: String, - limit: Int, - showHiddenFiles: Bool, - includeHiddenFiles: [String] = [], - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - + func searchMedia(path: String = "", + lessDate: Any, + greaterDate: Any, + elementDate: String, + limit: Int, + showHiddenFiles: Bool, + includeHiddenFiles: [String] = [], + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase let files: [NKFile] = [] var greaterDateString: String?, lessDateString: String? let href = "/files/" + userId + path - if let lessDate = lessDate as? Date { lessDateString = self.nkCommonInstance.convertDate(lessDate, format: "yyyy-MM-dd'T'HH:mm:ssZZZZZ") } else if let lessDate = lessDate as? Int { lessDateString = String(lessDate) } - if let greaterDate = greaterDate as? Date { greaterDateString = self.nkCommonInstance.convertDate(greaterDate, format: "yyyy-MM-dd'T'HH:mm:ssZZZZZ") } else if let greaterDate = greaterDate as? Int { greaterDateString = String(greaterDate) } - if lessDateString == nil || greaterDateString == nil { return options.queue.async { completion(account, files, nil, .invalidDate) } } - var requestBody = "" if limit > 0 { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodySearchMediaWithLimit, href, elementDate, elementDate, lessDateString!, elementDate, greaterDateString!, String(limit)) + requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMediaWithLimit(removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString!, elementDate, greaterDateString!, String(limit)) } else { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodySearchMedia, href, elementDate, elementDate, lessDateString!, elementDate, greaterDateString!) + requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMedia(removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString!, elementDate, greaterDateString!) } - let httpBody = requestBody.data(using: .utf8)! search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in @@ -413,22 +375,17 @@ extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let user = self.nkCommonInstance.user let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav var files: [NKFile] = [] - guard let url = (serverUrl + "/" + dav).encodedToUrl else { return options.queue.async { completion(account, files, nil, .urlError) } } - let method = HTTPMethod(rawValue: "SEARCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") - var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: method, headers: headers) @@ -445,7 +402,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -461,27 +417,23 @@ extension NextcloudKit { } } - @objc public func setFavorite(fileName: String, - favorite: Bool, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - + func setFavorite(fileName: String, + favorite: Bool, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + "/" + fileName - guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let method = HTTPMethod(rawValue: "PROPPATCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) let body = NSString(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyFileSetFavorite as NSString, (favorite ? 1 : 0)) as String @@ -498,7 +450,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -509,12 +460,11 @@ extension NextcloudKit { } } - @objc public func listingFavorites(showHiddenFiles: Bool, - includeHiddenFiles: [String] = [], - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - + func listingFavorites(showHiddenFiles: Bool, + includeHiddenFiles: [String] = [], + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let user = self.nkCommonInstance.user let userId = self.nkCommonInstance.userId @@ -522,19 +472,16 @@ extension NextcloudKit { let dav = self.nkCommonInstance.dav let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId var files: [NKFile] = [] - guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, files, nil, .urlError) } } - let method = HTTPMethod(rawValue: "REPORT") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") - var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) - urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyFileListingFavorites.data(using: .utf8) + urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFileListingFavorites(removeProperties: options.removeProperties).data(using: .utf8) urlRequest.timeoutInterval = options.timeout } catch { return options.queue.async { completion(account, files, nil, NKError(error: error)) } @@ -547,7 +494,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) @@ -563,12 +509,11 @@ extension NextcloudKit { } } - @objc public func listingTrash(filename: String? = nil, - showHiddenFiles: Bool, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ items: [NKTrash], _ data: Data?, _ error: NKError) -> Void) { - + func listingTrash(filename: String? = nil, + showHiddenFiles: Bool, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ items: [NKTrash], _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase @@ -578,17 +523,14 @@ extension NextcloudKit { serverUrlFileName = serverUrlFileName + filename } var items: [NKTrash] = [] - guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, items, nil, .urlError) } } - let method = HTTPMethod(rawValue: "PROPFIND") - var headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") 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).requestBodyTrash.data(using: .utf8) @@ -604,7 +546,6 @@ extension NextcloudKit { if self.nkCommonInstance.levelLog > 0 { debugPrint(response) } - switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index ab4d4fb1..cfb7f3a4 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -29,14 +29,12 @@ import UIKit import Alamofire import SwiftyJSON -@objc open class NextcloudKit: SessionDelegate { - @objc public static let shared: NextcloudKit = { +open class NextcloudKit: SessionDelegate { + public static let shared: NextcloudKit = { let instance = NextcloudKit() return instance }() - internal lazy var internalSessionManager: Alamofire.Session = { - return Alamofire.Session(configuration: nkCommonInstance.sessionConfiguration, delegate: self, rootQueue: nkCommonInstance.rootQueue, @@ -49,17 +47,14 @@ import SwiftyJSON cachedResponseHandler: nil, eventMonitors: [AlamofireLogger(nkCommonInstance: self.nkCommonInstance)]) }() - public var sessionManager: Alamofire.Session { return internalSessionManager } - #if !os(watchOS) private let reachabilityManager = Alamofire.NetworkReachabilityManager() #endif // private var cookies: [String:[HTTPCookie]] = [:] - - @objc public let nkCommonInstance = NKCommon() + public let nkCommonInstance = NKCommon() override public init(fileManager: FileManager = .default) { super.init(fileManager: fileManager) @@ -76,16 +71,14 @@ import SwiftyJSON // MARK: - Setup - @objc public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, userAgent: String, nextcloudVersion: Int, delegate: NKCommonDelegate?) { - + public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, userAgent: String, nextcloudVersion: Int, delegate: NKCommonDelegate?) { self.setup(account: account, user: user, userId: userId, password: password, urlBase: urlBase) self.setup(userAgent: userAgent) self.setup(nextcloudVersion: nextcloudVersion) self.setup(delegate: delegate) } - @objc public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String) { - + public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String) { if (self.nkCommonInstance.account != account) || (self.nkCommonInstance.urlBase != urlBase && self.nkCommonInstance.user != user) { if let cookieStore = sessionManager.session.configuration.httpCookieStorage { for cookie in cookieStore.cookies ?? [] { @@ -106,26 +99,22 @@ import SwiftyJSON self.nkCommonInstance.internalUrlBase = urlBase } - @objc public func setup(delegate: NKCommonDelegate?) { - + public func setup(delegate: NKCommonDelegate?) { self.nkCommonInstance.delegate = delegate } - @objc public func setup(userAgent: String) { - + public func setup(userAgent: String) { self.nkCommonInstance.internalUserAgent = userAgent } - @objc public func setup(nextcloudVersion: Int) { - + public func setup(nextcloudVersion: Int) { self.nkCommonInstance.internalNextcloudVersion = nextcloudVersion } - @objc public func setupSessionManager(sessionConfiguration: URLSessionConfiguration?, - rootQueue: DispatchQueue?, - requestQueue: DispatchQueue?, - serializationQueue: DispatchQueue?) { - + public func setupSessionManager(sessionConfiguration: URLSessionConfiguration?, + rootQueue: DispatchQueue?, + requestQueue: DispatchQueue?, + serializationQueue: DispatchQueue?) { if let sessionConfiguration = sessionConfiguration { self.nkCommonInstance.sessionConfiguration = sessionConfiguration } @@ -168,39 +157,33 @@ import SwiftyJSON // MARK: - Reachability #if !os(watchOS) - @objc public func isNetworkReachable() -> Bool { + public func isNetworkReachable() -> Bool { return reachabilityManager?.isReachable ?? false } private func startNetworkReachabilityObserver() { - reachabilityManager?.startListening(onUpdatePerforming: { status in switch status { - case .unknown: - self.nkCommonInstance.delegate?.networkReachabilityObserver?(NKCommon.TypeReachability.unknown) - + self.nkCommonInstance.delegate?.networkReachabilityObserver(NKCommon.TypeReachability.unknown) case .notReachable: - self.nkCommonInstance.delegate?.networkReachabilityObserver?(NKCommon.TypeReachability.notReachable) - + self.nkCommonInstance.delegate?.networkReachabilityObserver(NKCommon.TypeReachability.notReachable) case .reachable(.ethernetOrWiFi): - self.nkCommonInstance.delegate?.networkReachabilityObserver?(NKCommon.TypeReachability.reachableEthernetOrWiFi) - + self.nkCommonInstance.delegate?.networkReachabilityObserver(NKCommon.TypeReachability.reachableEthernetOrWiFi) case .reachable(.cellular): - self.nkCommonInstance.delegate?.networkReachabilityObserver?(NKCommon.TypeReachability.reachableCellular) + self.nkCommonInstance.delegate?.networkReachabilityObserver(NKCommon.TypeReachability.reachableCellular) } }) } private func stopNetworkReachabilityObserver() { - reachabilityManager?.stopListening() } #endif // MARK: - Session utility - @objc public func getSessionManager() -> URLSession { + public func getSessionManager() -> URLSession { return sessionManager.session } @@ -229,74 +212,44 @@ import SwiftyJSON // MARK: - download / upload - @objc public func download(serverUrlFileName: Any, - fileNameLocalPath: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, - completionHandler: @escaping (_ account: String, _ etag: String?, _ date: NSDate?, _ lenght: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ nkError: NKError) -> Void) { - - download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, options: options) { _ in - // not available in objc - } taskHandler: { task in - taskHandler(task) - } progressHandler: { progress in - progressHandler(progress) - } completionHandler: { account, etag, date, lenght, allHeaderFields, _, nkError in - // error not available in objc - completionHandler(account, etag, date, lenght, allHeaderFields, nkError) - } - } - public func download(serverUrlFileName: Any, fileNameLocalPath: String, options: NKRequestOptions = NKRequestOptions(), requestHandler: @escaping (_ request: DownloadRequest) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, - completionHandler: @escaping (_ account: String, _ etag: String?, _ date: NSDate?, _ lenght: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nKError: NKError) -> Void) { - + completionHandler: @escaping (_ account: String, _ etag: String?, _ date: Date?, _ lenght: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nKError: NKError) -> Void) { let account = self.nkCommonInstance.account var convertible: URLConvertible? - if serverUrlFileName is URL { convertible = serverUrlFileName as? URLConvertible } else if serverUrlFileName is String || serverUrlFileName is NSString { convertible = (serverUrlFileName as? String)?.encodedToUrl } - guard let url = convertible else { options.queue.async { completionHandler(account, nil, nil, 0, nil, nil, .urlError) } return } - var destination: Alamofire.DownloadRequest.Destination? let fileNamePathLocalDestinationURL = NSURL.fileURL(withPath: fileNameLocalPath) let destinationFile: DownloadRequest.Destination = { _, _ in return (fileNamePathLocalDestinationURL, [.removePreviousFile, .createIntermediateDirectories]) } destination = destinationFile - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let request = sessionManager.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } - } .downloadProgress { progress in - options.queue.async { progressHandler(progress) } - } .response(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { case .failure(let error): let resultError = NKError(error: error, afResponse: response, responseData: nil) options.queue.async { completionHandler(account, nil, nil, 0, nil, error, resultError) } case .success: - - var date: NSDate? + var date: Date? var etag: String? var length: Int64 = 0 let allHeaderFields = response.response?.allHeaderFields @@ -304,17 +257,14 @@ import SwiftyJSON if let result = response.response?.allHeaderFields["Content-Length"] as? String { length = Int64(result) ?? 0 } - if self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) != nil { etag = self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) } else if self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) != nil { etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) } - if etag != nil { - etag = etag!.replacingOccurrences(of: "\"", with: "") + etag = etag?.replacingOccurrences(of: "\"", with: "") } - if let dateString = self.nkCommonInstance.findHeader("Date", allHeaderFields: response.response?.allHeaderFields) { date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") } @@ -326,27 +276,6 @@ import SwiftyJSON options.queue.async { requestHandler(request) } } - @objc public func upload(serverUrlFileName: String, - fileNameLocalPath: String, - dateCreationFile: Date? = nil, - dateModificationFile: Date? = nil, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, - completionHandler: @escaping (_ account: String, _ ocId: String?, _ etag: String?, _ date: NSDate?, _ size: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ nkError: NKError) -> Void) { - - upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, dateCreationFile: dateCreationFile, dateModificationFile: dateModificationFile, options: options) { _ in - // not available in objc - } taskHandler: { task in - taskHandler(task) - } progressHandler: { progress in - progressHandler(progress) - } completionHandler: { account, ocId, etag, date, size, allHeaderFields, _, nkError in - // error not available in objc - completionHandler(account, ocId, etag, date, size, allHeaderFields, nkError) - } - } - public func upload(serverUrlFileName: Any, fileNameLocalPath: String, dateCreationFile: Date? = nil, @@ -355,27 +284,21 @@ import SwiftyJSON requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, - completionHandler: @escaping (_ account: String, _ ocId: String?, _ etag: String?, _ date: NSDate?, _ size: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nkError: NKError) -> Void) { - + completionHandler: @escaping (_ account: String, _ ocId: String?, _ etag: String?, _ date: Date?, _ size: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nkError: NKError) -> Void) { let account = self.nkCommonInstance.account var convertible: URLConvertible? var size: Int64 = 0 - if serverUrlFileName is URL { convertible = serverUrlFileName as? URLConvertible } else if serverUrlFileName is String || serverUrlFileName is NSString { convertible = (serverUrlFileName as? String)?.encodedToUrl } - guard let url = convertible else { options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, nil, .urlError) } return } - let fileNameLocalPathUrl = URL(fileURLWithPath: fileNameLocalPath) - var headers = self.nkCommonInstance.getStandardHeaders(options: options) - // Epoch of linux do not permitted negativ value if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { headers.update(name: "X-OC-CTime", value: "\(dateCreationFile.timeIntervalSince1970)") @@ -386,17 +309,12 @@ import SwiftyJSON } let request = sessionManager.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in - task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } - }) .uploadProgress { progress in - options.queue.async { progressHandler(progress) } size = progress.totalUnitCount - } .response(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { case .failure(let error): let resultError = NKError(error: error, afResponse: response, responseData: response.data) @@ -404,21 +322,19 @@ import SwiftyJSON case .success: var ocId: String?, etag: String? let allHeaderFields = response.response?.allHeaderFields - if self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: response.response?.allHeaderFields) != nil { ocId = self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: response.response?.allHeaderFields) } else if self.nkCommonInstance.findHeader("fileid", allHeaderFields: response.response?.allHeaderFields) != nil { ocId = self.nkCommonInstance.findHeader("fileid", allHeaderFields: response.response?.allHeaderFields) } - if self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) != nil { etag = self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) } else if self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) != nil { etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) } - - if etag != nil { etag = etag!.replacingOccurrences(of: "\"", with: "") } - + if etag != nil { + etag = etag?.replacingOccurrences(of: "\"", with: "") + } if let dateString = self.nkCommonInstance.findHeader("date", allHeaderFields: response.response?.allHeaderFields) { if let date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") { options.queue.async { completionHandler(account, ocId, etag, date, size, allHeaderFields, nil, .success) } @@ -462,12 +378,10 @@ import SwiftyJSON progressHandler: @escaping (_ totalBytesExpected: Int64, _ totalBytes: Int64, _ fractionCompleted: Double) -> Void = { _, _, _ in }, uploaded: @escaping (_ fileChunk: (fileName: String, size: Int64)) -> Void = { _ in }, completion: @escaping (_ account: String, _ filesChunk: [(fileName: String, size: Int64)]?, _ file: NKFile?, _ afError: AFError?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav - let fileNameLocalSize = self.nkCommonInstance.getFileSize(filePath: directory + "/" + fileName) let serverUrlChunkFolder = urlBase + "/" + dav + "/uploads/" + userId + "/" + chunkFolder let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl) + "/" + fileName @@ -506,7 +420,6 @@ import SwiftyJSON #endif func createFolder(completion: @escaping (_ errorCode: NKError) -> Void) { - readFileOrFolder(serverUrlFileName: serverUrlChunkFolder, depth: "0", options: options) { _, _, _, error in if error == .success { completion(NKError()) @@ -521,11 +434,9 @@ import SwiftyJSON } createFolder { error in - guard error == .success else { return completion(account, nil, nil, nil, NKError(errorCode: NKError.chunkCreateFolder, errorDescription: error.errorDescription)) } - var uploadNKError = NKError() var uploadAFError: AFError? @@ -539,22 +450,18 @@ import SwiftyJSON let error = NKError(errorCode: NKError.chunkFilesNull, errorDescription: "_chunk_files_null_") return completion(account, nil, nil, nil, error) } - var filesChunkOutput = filesChunk start(filesChunkOutput) for fileChunk in filesChunk { - let serverUrlFileName = serverUrlChunkFolder + "/" + fileChunk.fileName let fileNameLocalPath = directory + "/" + fileChunk.fileName - let fileSize = self.nkCommonInstance.getFileSize(filePath: fileNameLocalPath) if fileSize == 0 { // The file could not be sent let error = NKError(errorCode: NKError.chunkFileNull, errorDescription: "_chunk_file_null_") return completion(account, nil, nil, .explicitlyCancelled, error) } - let semaphore = DispatchSemaphore(value: 0) self.upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, options: options, requestHandler: { request in requestHandler(request) @@ -595,7 +502,6 @@ import SwiftyJSON if let date, date.timeIntervalSince1970 > 0 { options.customHeader?["X-OC-MTime"] = "\(date.timeIntervalSince1970)" } - // Calculate Assemble Timeout let assembleSizeInGB = Double(fileNameLocalSize) / 1e9 let assembleTimePerGB: Double = 3 * 60 // 3 min @@ -604,7 +510,6 @@ import SwiftyJSON options.timeout = max(assembleTimeMin, min(assembleTimePerGB * assembleSizeInGB, assembleTimeMax)) self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, options: options) { _, error in - guard error == .success else { return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) } @@ -624,12 +529,11 @@ import SwiftyJSON // MARK: - SessionDelegate public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - if self.nkCommonInstance.delegate == nil { self.nkCommonInstance.writeLog("[WARNING] URLAuthenticationChallenge, no delegate found, perform with default handling") completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) } else { - self.nkCommonInstance.delegate?.authenticationChallenge?(session, didReceive: challenge, completionHandler: { authChallengeDisposition, credential in + self.nkCommonInstance.delegate?.authenticationChallenge(session, didReceive: challenge, completionHandler: { authChallengeDisposition, credential in if self.nkCommonInstance.levelLog > 1 { self.nkCommonInstance.writeLog("[INFO AUTH] Challenge Disposition: \(authChallengeDisposition.rawValue)") } @@ -647,13 +551,9 @@ final class AlamofireLogger: EventMonitor { } func requestDidResume(_ request: Request) { - if self.nkCommonInstance.levelLog > 0 { - self.nkCommonInstance.writeLog("Network request started: \(request)") - if self.nkCommonInstance.levelLog > 1 { - let allHeaders = request.request.flatMap { $0.allHTTPHeaderFields.map { $0.description } } ?? "None" let body = request.request.flatMap { $0.httpBody.map { String(decoding: $0, as: UTF8.self) } } ?? "None" @@ -664,25 +564,20 @@ final class AlamofireLogger: EventMonitor { } func request(_ request: DataRequest, didParseResponse response: AFDataResponse) { - guard let date = self.nkCommonInstance.convertDate(Date(), format: "yyyy-MM-dd' 'HH:mm:ss") else { return } let responseResultString = String("\(response.result)") let responseDebugDescription = String("\(response.debugDescription)") let responseAllHeaderFields = String("\(String(describing: response.response?.allHeaderFields))") if self.nkCommonInstance.levelLog > 0 { - if self.nkCommonInstance.levelLog == 1 { - if let request = response.request { let requestString = "\(request)" self.nkCommonInstance.writeLog("Network response request: " + requestString + ", result: " + responseResultString) } else { self.nkCommonInstance.writeLog("Network response result: " + responseResultString) } - } else { - self.nkCommonInstance.writeLog("Network response result: \(date) " + responseDebugDescription) self.nkCommonInstance.writeLog("Network response all headers: \(date) " + responseAllHeaderFields) } diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index e6993a22..0be14e91 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -23,7 +23,7 @@ import Foundation -@objc public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, URLSessionDownloadDelegate { +public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, URLSessionDownloadDelegate { let nkCommonInstance: NKCommon public init(nkCommonInstance: NKCommon) { @@ -33,21 +33,17 @@ import Foundation // MARK: - Download - @objc public func download(serverUrlFileName: Any, - fileNameLocalPath: String, - taskDescription: String? = nil, - session: URLSession) -> URLSessionDownloadTask? { - + public func download(serverUrlFileName: Any, + fileNameLocalPath: String, + taskDescription: String? = nil, + session: URLSession) -> URLSessionDownloadTask? { var url: URL? - if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { url = (serverUrlFileName as? String)?.encodedToUrl as? URL } - guard let urlForRequest = url else { return nil } - var request = URLRequest(url: urlForRequest) let loginString = "\(self.nkCommonInstance.user):\(self.nkCommonInstance.password)" guard let loginData = loginString.data(using: String.Encoding.utf8) else { @@ -68,26 +64,22 @@ import Foundation // MARK: - Upload - @objc public func upload(serverUrlFileName: Any, - fileNameLocalPath: String, - dateCreationFile: Date?, - dateModificationFile: Date?, - taskDescription: String? = nil, - session: URLSession) -> URLSessionUploadTask? { - + public func upload(serverUrlFileName: Any, + fileNameLocalPath: String, + dateCreationFile: Date?, + dateModificationFile: Date?, + taskDescription: String? = nil, + session: URLSession) -> URLSessionUploadTask? { var url: URL? - if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { url = (serverUrlFileName as? String)?.encodedToUrl as? URL } - guard let urlForRequest = url else { return nil } var request = URLRequest(url: urlForRequest) - let loginString = "\(self.nkCommonInstance.user):\(self.nkCommonInstance.password)" guard let loginData = loginString.data(using: String.Encoding.utf8) else { return nil @@ -117,40 +109,36 @@ import Foundation // MARK: - SessionDelegate public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { - guard totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown else { return } guard let url = downloadTask.currentRequest?.url?.absoluteString.removingPercentEncoding else { return } let fileName = (url as NSString).lastPathComponent let serverUrl = url.replacingOccurrences(of: "/" + fileName, with: "") let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) - self.nkCommonInstance.delegate?.downloadProgress?(progress, totalBytes: totalBytesWritten, totalBytesExpected: totalBytesExpectedToWrite, fileName: fileName, serverUrl: serverUrl, session: session, task: downloadTask) + self.nkCommonInstance.delegate?.downloadProgress(progress, totalBytes: totalBytesWritten, totalBytesExpected: totalBytesExpectedToWrite, fileName: fileName, serverUrl: serverUrl, session: session, task: downloadTask) } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { - self.nkCommonInstance.delegate?.downloadingFinish?(session, downloadTask: downloadTask, didFinishDownloadingTo: location) + self.nkCommonInstance.delegate?.downloadingFinish(session, downloadTask: downloadTask, didFinishDownloadingTo: location) } public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - guard totalBytesExpectedToSend != NSURLSessionTransferSizeUnknown else { return } guard let url = task.currentRequest?.url?.absoluteString.removingPercentEncoding else { return } let fileName = (url as NSString).lastPathComponent let serverUrl = url.replacingOccurrences(of: "/" + fileName, with: "") let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend) - self.nkCommonInstance.delegate?.uploadProgress?(progress, totalBytes: totalBytesSent, totalBytesExpected: totalBytesExpectedToSend, fileName: fileName, serverUrl: serverUrl, session: session, task: task) + self.nkCommonInstance.delegate?.uploadProgress(progress, totalBytes: totalBytesSent, totalBytesExpected: totalBytesExpectedToSend, fileName: fileName, serverUrl: serverUrl, session: session, task: task) } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - - var fileName: String = "", serverUrl: String = "", etag: String?, ocId: String?, date: NSDate?, dateLastModified: NSDate?, length: Int64 = 0 + var fileName: String = "", serverUrl: String = "", etag: String?, ocId: String?, date: Date?, dateLastModified: Date?, length: Int64 = 0 let url = task.currentRequest?.url?.absoluteString.removingPercentEncoding if url != nil { fileName = (url! as NSString).lastPathComponent serverUrl = url!.replacingOccurrences(of: "/" + fileName, with: "") } - var nkError: NKError = .success if let httpResponse = (task.response as? HTTPURLResponse) { @@ -189,9 +177,9 @@ import Foundation } if task is URLSessionDownloadTask { - self.nkCommonInstance.delegate?.downloadComplete?(fileName: fileName, serverUrl: serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, task: task, error: nkError) + self.nkCommonInstance.delegate?.downloadComplete(fileName: fileName, serverUrl: serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, task: task, error: nkError) } else if task is URLSessionUploadTask { - self.nkCommonInstance.delegate?.uploadComplete?(fileName: fileName, serverUrl: serverUrl, ocId: ocId, etag: etag, date: date, size: task.countOfBytesExpectedToSend, task: task, error: nkError) + self.nkCommonInstance.delegate?.uploadComplete(fileName: fileName, serverUrl: serverUrl, ocId: ocId, etag: etag, date: date, size: task.countOfBytesExpectedToSend, task: task, error: nkError) } if nkError.errorCode == 0 { @@ -202,12 +190,11 @@ import Foundation } public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - if self.nkCommonInstance.delegate == nil { self.nkCommonInstance.writeLog("[WARNING] URLAuthenticationChallenge, no delegate found, perform with default handling") completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) } else { - self.nkCommonInstance.delegate?.authenticationChallenge?(session, didReceive: challenge, completionHandler: { authChallengeDisposition, credential in + self.nkCommonInstance.delegate?.authenticationChallenge(session, didReceive: challenge, completionHandler: { authChallengeDisposition, credential in if self.nkCommonInstance.levelLog > 1 { self.nkCommonInstance.writeLog("[INFO AUTH] Challenge Disposition: \(authChallengeDisposition.rawValue)") } @@ -217,6 +204,6 @@ import Foundation } public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { - self.nkCommonInstance.delegate?.urlSessionDidFinishEvents?(forBackgroundURLSession: session) + self.nkCommonInstance.delegate?.urlSessionDidFinishEvents(forBackgroundURLSession: session) } } diff --git a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV_Async.swift b/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV_Async.swift deleted file mode 100644 index 7f1015e8..00000000 --- a/Sources/NextcloudKit/WebDAV/NextcloudKit+WebDAV_Async.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// NextcloudKit+WebDAV_Async.swift -// NextcloudKit -// -// Created by Marino Faggiana on 19/10/22. -// -// Copyright © 2022 Marino Faggiana. All rights reserved. -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import Foundation - -@available(iOS 13.0, *) -extension NextcloudKit { - - public func createFolder(serverUrlFileName: String, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, ocId: String?, date: NSDate?, error: NKError) { - - await withUnsafeContinuation({ continuation in - createFolder(serverUrlFileName: serverUrlFileName, options: options) { account, ocId, date, error in - continuation.resume(returning: (account: account, ocId: ocId, date: date, error: error)) - } - }) - } - - public func deleteFileOrFolder(serverUrlFileName: String, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { - - await withUnsafeContinuation({ continuation in - deleteFileOrFolder(serverUrlFileName: serverUrlFileName, options: options) { account, error in - continuation.resume(returning: (account: account, error: error)) - } - }) - } - - public func readFileOrFolder(serverUrlFileName: String, - depth: String, - showHiddenFiles: Bool = true, - requestBody: Data? = nil, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, files: [NKFile], data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: depth, showHiddenFiles: showHiddenFiles, requestBody: requestBody, options: options) { account, files, data, error in - continuation.resume(returning: (account: account, files: files, data: data, error: error)) - } - }) - } - - public func copyFileOrFolder(serverUrlFileNameSource: String, - serverUrlFileNameDestination: String, - overwrite: Bool, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { - - await withUnsafeContinuation({ continuation in - copyFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileNameDestination, overwrite: overwrite, options: options) { account, error in - continuation.resume(returning: (account: account, error: error)) - } - }) - } - - public func moveFileOrFolder(serverUrlFileNameSource: String, - serverUrlFileNameDestination: String, - overwrite: Bool, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { - - await withUnsafeContinuation({ continuation in - moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileNameDestination, overwrite: overwrite, options: options) { account, error in - continuation.resume(returning: (account: account, error: error)) - } - }) - } - - public func searchMedia(path: String = "", - lessDate: Any, - greaterDate: Any, - elementDate: String, - limit: Int, - showHiddenFiles: Bool, - includeHiddenFiles: [String] = [], - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, files: [NKFile], data: Data?, error: NKError) { - - await withUnsafeContinuation({ continuation in - searchMedia(path: path, lessDate: lessDate, greaterDate: greaterDate, elementDate: elementDate, limit: limit, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { account, files, data, error in - continuation.resume(returning: (account, files, data, error)) - } - }) - } -} From e4e557e28a664faae31d58140403d723ffb65eeb Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 2 Jul 2024 11:36:35 +0200 Subject: [PATCH 039/135] improved code Signed-off-by: Marino Faggiana --- .../Extensions/Image+Extension.swift | 4 +- Sources/NextcloudKit/NKCommon.swift | 19 +++---- Sources/NextcloudKit/NextcloudKit+API.swift | 10 ++-- .../NextcloudKit/NextcloudKit+WebDAV.swift | 54 ++++++++++--------- .../NextcloudKit/NextcloudKitBackground.swift | 8 +-- 5 files changed, 48 insertions(+), 47 deletions(-) diff --git a/Sources/NextcloudKit/Extensions/Image+Extension.swift b/Sources/NextcloudKit/Extensions/Image+Extension.swift index 0d743573..d155e3bb 100644 --- a/Sources/NextcloudKit/Extensions/Image+Extension.swift +++ b/Sources/NextcloudKit/Extensions/Image+Extension.swift @@ -54,7 +54,7 @@ public extension NSImage { return nil } - func resizeImage(size: CGSize, isAspectRation: Bool) -> NSImage? { + func resizeImage(size: CGSize, isAspectRation: Bool = true) -> NSImage? { if let bitmapRep = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, @@ -78,7 +78,7 @@ public extension NSImage { import UIKit extension UIImage { - internal func resizeImage(size: CGSize, isAspectRation: Bool) -> UIImage? { + internal func resizeImage(size: CGSize, isAspectRation: Bool = true) -> UIImage? { let originRatio = self.size.width / self.size.height let newRatio = size.width / size.height var newSize = size diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index a606b2b8..6cbd77f5 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -260,7 +260,9 @@ public class NKCommon: NSObject { } else { if let unmanagedFileUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil) { inUTI = unmanagedFileUTI.takeRetainedValue() - utiCache.setObject(inUTI!, forKey: ext as NSString) + if let inUTI { + utiCache.setObject(inUTI, forKey: ext as NSString) + } } } @@ -604,8 +606,8 @@ public class NKCommon: NSObject { public func clearFileLog() { FileManager.default.createFile(atPath: filenamePathLog, contents: nil, attributes: nil) - if copyLogToDocumentDirectory { - let filenameCopyToDocumentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/" + filenameLog + if copyLogToDocumentDirectory, let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first { + let filenameCopyToDocumentDirectory = path + "/" + filenameLog FileManager.default.createFile(atPath: filenameCopyToDocumentDirectory, contents: nil, attributes: nil) } @@ -616,17 +618,12 @@ public class NKCommon: NSObject { guard let date = self.convertDate(Date(), format: "yyyy-MM-dd' 'HH:mm:ss") else { return } let textToWrite = "\(date) " + text + "\n" - if printLog { - print(textToWrite) - } - + if printLog { print(textToWrite) } if levelLog > 0 { - queueLog.async(flags: .barrier) { self.writeLogToDisk(filename: self.filenamePathLog, text: textToWrite) - - if self.copyLogToDocumentDirectory { - let filenameCopyToDocumentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/" + self.filenameLog + if self.copyLogToDocumentDirectory, let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first { + let filenameCopyToDocumentDirectory = path + "/" + self.filenameLog self.writeLogToDisk(filename: filenameCopyToDocumentDirectory, text: textToWrite) } } diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 559d5243..dda74a83 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -346,11 +346,11 @@ public extension NextcloudKit { try data.write(to: URL(fileURLWithPath: fileNamePreviewLocalPath), options: .atomic) imagePreview = UIImage(data: data) } - if fileNameIconLocalPath != nil && sizeIcon > 0 { - imageIcon = imageOriginal.resizeImage(size: CGSize(width: sizeIcon, height: sizeIcon), isAspectRation: true) + if let fileNameIconLocalPath, sizeIcon > 0 { + imageIcon = imageOriginal.resizeImage(size: CGSize(width: sizeIcon, height: sizeIcon)) if let data = imageIcon?.jpegData(compressionQuality: compressionQualityIcon) { - try data.write(to: URL(fileURLWithPath: fileNameIconLocalPath!), options: .atomic) - imageIcon = UIImage(data: data)! + try data.write(to: URL(fileURLWithPath: fileNameIconLocalPath), options: .atomic) + imageIcon = UIImage(data: data) } } options.queue.async { completion(account, imagePreview, imageIcon, imageOriginal, etag, .success) } @@ -838,7 +838,7 @@ public extension NextcloudKit { let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications/\(idNotification)" url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) } else { - url = serverUrl!.asUrl + url = serverUrl?.asUrl } guard let urlRequest = url else { return options.queue.async { completion(account, .urlError) } diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index b7a48ab9..7d89d906 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -224,8 +224,8 @@ public extension NextcloudKit { do { try urlRequest = URLRequest(url: url, method: method, headers: headers) - if requestBody != nil { - urlRequest.httpBody = requestBody! + if let requestBody { + urlRequest.httpBody = requestBody urlRequest.timeoutInterval = options.timeout } else { urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFile(removeProperties: options.removeProperties).data(using: .utf8) @@ -266,11 +266,11 @@ public extension NextcloudKit { let urlBase = self.nkCommonInstance.urlBase var httpBody: Data? if let fileId = fileId { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(removeProperties: options.removeProperties), userId, fileId).data(using: .utf8)! + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) } else if let link = link { let linkArray = link.components(separatedBy: "/") if let fileId = linkArray.last { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(removeProperties: options.removeProperties), userId, fileId).data(using: .utf8)! + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) } } guard let httpBody = httpBody else { @@ -292,12 +292,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let httpBody = requestBody.data(using: .utf8)! - - search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in - taskHandler(task) - } completion: { account, files, data, error in - options.queue.async { completion(account, files, data, error) } + if let httpBody = requestBody.data(using: .utf8) { + search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in + taskHandler(task) + } completion: { account, files, data, error in + options.queue.async { completion(account, files, data, error) } + } } } @@ -315,12 +315,12 @@ public extension NextcloudKit { return options.queue.async { completion(account, [], nil, .urlError) } } let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(removeProperties: options.removeProperties), href, depth, "%" + literal + "%") - let httpBody = requestBody.data(using: .utf8)! - - search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in - taskHandler(task) - } completion: { account, files, data, error in - options.queue.async { completion(account, files, data, error) } + if let httpBody = requestBody.data(using: .utf8) { + search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in + taskHandler(task) + } completion: { account, files, data, error in + options.queue.async { completion(account, files, data, error) } + } } } @@ -354,17 +354,21 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, .invalidDate) } } var requestBody = "" - if limit > 0 { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMediaWithLimit(removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString!, elementDate, greaterDateString!, String(limit)) - } else { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMedia(removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString!, elementDate, greaterDateString!) + + if let lessDateString, let greaterDateString { + if limit > 0 { + requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMediaWithLimit(removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString, String(limit)) + } else { + requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMedia(removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString) + } } - let httpBody = requestBody.data(using: .utf8)! - search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in - taskHandler(task) - } completion: { account, files, data, error in - options.queue.async { completion(account, files, data, error) } + if let httpBody = requestBody.data(using: .utf8) { + search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in + taskHandler(task) + } completion: { account, files, data, error in + options.queue.async { completion(account, files, data, error) } + } } } diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 0be14e91..b52ce971 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -135,9 +135,9 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { var fileName: String = "", serverUrl: String = "", etag: String?, ocId: String?, date: Date?, dateLastModified: Date?, length: Int64 = 0 let url = task.currentRequest?.url?.absoluteString.removingPercentEncoding - if url != nil { - fileName = (url! as NSString).lastPathComponent - serverUrl = url!.replacingOccurrences(of: "/" + fileName, with: "") + if let url { + fileName = (url as NSString).lastPathComponent + serverUrl = url.replacingOccurrences(of: "/" + fileName, with: "") } var nkError: NKError = .success @@ -166,7 +166,7 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, } else if self.nkCommonInstance.findHeader("etag", allHeaderFields: header) != nil { etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: header) } - if etag != nil { etag = etag!.replacingOccurrences(of: "\"", with: "") } + if etag != nil { etag = etag?.replacingOccurrences(of: "\"", with: "") } if let dateString = self.nkCommonInstance.findHeader("date", allHeaderFields: header) { date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") } From 3e5ffc7fc3840d88890e6948cfc4a100e6248ef1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 3 Jul 2024 14:05:25 +0200 Subject: [PATCH 040/135] fix paramenters default Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+API.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index dda74a83..1530d447 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -259,9 +259,9 @@ public extension NextcloudKit { compressionQualityPreview: CGFloat = 0.5, compressionQualityIcon: CGFloat = 0.5, etag: String? = nil, - crop: Int = 0, - cropMode: String = "fill", - forceIcon: Int = 1, + crop: Int = 1, + cropMode: String = "cover", + forceIcon: Int = 0, mimeFallback: Int = 0, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, From b61e465c520e0738ecd3f109b05eb91c64a711fd Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 3 Jul 2024 14:06:50 +0200 Subject: [PATCH 041/135] fix paramenters default Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+API.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 1530d447..6541a564 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -283,9 +283,9 @@ public extension NextcloudKit { sizeIcon: Int = 512, compressionQualityPreview: CGFloat = 0.5, compressionQualityIcon: CGFloat = 0.5, - crop: Int = 0, - cropMode: String = "fill", - forceIcon: Int = 1, + crop: Int = 1, + cropMode: String = "cover", + forceIcon: Int = 0, mimeFallback: Int = 0, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, From eb260e3dffb206e6755aa5be98d1b7bf05cc4dad Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 5 Jul 2024 14:45:41 +0200 Subject: [PATCH 042/135] improvements Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 34 +++++++++++-------- Sources/NextcloudKit/NKRequestOptions.swift | 7 ++-- .../NextcloudKit/NextcloudKit+WebDAV.swift | 14 ++++---- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index d69f68c7..c10b5e6a 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -81,10 +81,14 @@ public enum NKProperties: String, CaseIterable { case sharepermissionscollaboration = "" case sharepermissionscloudmesh = "" - static func properties(removeProperties: [String] = []) -> String { + static func properties(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { var properties = allCases.map { $0.rawValue }.joined() - for property in removeProperties { - properties = properties.replacingOccurrences(of: property, with: "") + if !add.isEmpty { + properties = "" + properties = add.map { $0.rawValue }.joined(separator: "") + } + for removeProperty in remove { + properties = properties.replacingOccurrences(of: removeProperty.rawValue, with: "") } return properties } @@ -355,12 +359,12 @@ class NKDataFileXML: NSObject { """ - func getRequestBodyFile(removeProperties: [String] = []) -> String { + func getRequestBodyFile(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(removeProperties: removeProperties) + """ + """ + NKProperties.properties(add: add, remove: remove) + """ """ @@ -379,12 +383,12 @@ class NKDataFileXML: NSObject { """ - func getRequestBodyFileListingFavorites(removeProperties: [String] = []) -> String { + func getRequestBodyFileListingFavorites(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(removeProperties: removeProperties) + """ + """ + NKProperties.properties(add: add, remove: remove) + """ 1 @@ -394,14 +398,14 @@ class NKDataFileXML: NSObject { return request } - func getRequestBodySearchFileName(removeProperties: [String] = []) -> String { + func getRequestBodySearchFileName(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(removeProperties: removeProperties) + """ + """ + NKProperties.properties(add: add, remove: remove) + """ @@ -422,14 +426,14 @@ class NKDataFileXML: NSObject { return request } - func getRequestBodySearchFileId(removeProperties: [String] = []) -> String { + func getRequestBodySearchFileId(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(removeProperties: removeProperties) + """ + """ + NKProperties.properties(add: add, remove: remove) + """ @@ -450,14 +454,14 @@ class NKDataFileXML: NSObject { return request } - func getRequestBodySearchMedia(removeProperties: [String] = []) -> String { + func getRequestBodySearchMedia(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(removeProperties: removeProperties) + """ + """ + NKProperties.properties(add: add, remove: remove) + """ @@ -508,14 +512,14 @@ class NKDataFileXML: NSObject { return request } - func getRequestBodySearchMediaWithLimit(removeProperties: [String] = []) -> String { + func getRequestBodySearchMediaWithLimit(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(removeProperties: removeProperties) + """ + """ + NKProperties.properties(add: add, remove: remove) + """ diff --git a/Sources/NextcloudKit/NKRequestOptions.swift b/Sources/NextcloudKit/NKRequestOptions.swift index 2bfaff3e..a5c0886c 100644 --- a/Sources/NextcloudKit/NKRequestOptions.swift +++ b/Sources/NextcloudKit/NKRequestOptions.swift @@ -32,7 +32,8 @@ public class NKRequestOptions: NSObject { var e2eToken: String? var timeout: TimeInterval var taskDescription: String? - var removeProperties: [String] + var addProperties: [NKProperties] + var removeProperties: [NKProperties] var queue: DispatchQueue public init(endpoint: String? = nil, @@ -43,7 +44,8 @@ public class NKRequestOptions: NSObject { e2eToken: String? = nil, timeout: TimeInterval = 60, taskDescription: String? = nil, - removeProperties: [String] = [], + addProperties: [NKProperties] = [], + removeProperties: [NKProperties] = [], queue: DispatchQueue = .main) { self.endpoint = endpoint @@ -54,6 +56,7 @@ public class NKRequestOptions: NSObject { self.e2eToken = e2eToken self.timeout = timeout self.taskDescription = taskDescription + self.addProperties = addProperties self.removeProperties = removeProperties self.queue = queue } diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 7d89d906..2ea33e74 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -228,7 +228,7 @@ public extension NextcloudKit { urlRequest.httpBody = requestBody urlRequest.timeoutInterval = options.timeout } else { - urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFile(removeProperties: options.removeProperties).data(using: .utf8) + urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFile(add: options.addProperties, remove: options.removeProperties).data(using: .utf8) } } catch { return options.queue.async { completion(account, files, nil, NKError(error: error)) } @@ -266,11 +266,11 @@ public extension NextcloudKit { let urlBase = self.nkCommonInstance.urlBase var httpBody: Data? if let fileId = fileId { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(add: options.addProperties, remove: options.removeProperties), userId, fileId).data(using: .utf8) } else if let link = link { let linkArray = link.components(separatedBy: "/") if let fileId = linkArray.last { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(add: options.addProperties, remove: options.removeProperties), userId, fileId).data(using: .utf8) } } guard let httpBody = httpBody else { @@ -314,7 +314,7 @@ public extension NextcloudKit { guard let href = ("/files/" + userId).urlEncoded else { return options.queue.async { completion(account, [], nil, .urlError) } } - let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(removeProperties: options.removeProperties), href, depth, "%" + literal + "%") + let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(add: options.addProperties, remove: options.removeProperties), href, depth, "%" + literal + "%") if let httpBody = requestBody.data(using: .utf8) { search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in taskHandler(task) @@ -357,9 +357,9 @@ public extension NextcloudKit { if let lessDateString, let greaterDateString { if limit > 0 { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMediaWithLimit(removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString, String(limit)) + requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMediaWithLimit(add: options.addProperties, remove: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString, String(limit)) } else { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMedia(removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString) + requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMedia(add: options.addProperties, remove: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString) } } @@ -485,7 +485,7 @@ public extension NextcloudKit { do { try urlRequest = URLRequest(url: url, method: method, headers: headers) - urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFileListingFavorites(removeProperties: options.removeProperties).data(using: .utf8) + urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFileListingFavorites(add: options.addProperties, remove: options.removeProperties).data(using: .utf8) urlRequest.timeoutInterval = options.timeout } catch { return options.queue.async { completion(account, files, nil, NKError(error: error)) } From 44731fdd4e0d4b4cf6bfb33dc0c137e000518c0f Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 5 Jul 2024 14:52:08 +0200 Subject: [PATCH 043/135] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index c10b5e6a..076e8947 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -34,6 +34,7 @@ import SwiftyJSON // MARK: - public enum NKProperties: String, CaseIterable { + /// DAV case displayname = "" case getlastmodified = "" case getetag = "" @@ -42,7 +43,7 @@ public enum NKProperties: String, CaseIterable { case quotaavailablebytes = "" case quotausedbytes = "" case getcontentlength = "" - + /// owncloud.org case permissions = "" case id = "" case fileid = "" @@ -55,7 +56,7 @@ public enum NKProperties: String, CaseIterable { case checksums = "" case downloadURL = "" case datafingerprint = "" - + /// nextcloud.org case creationtime = "" case uploadtime = "" case isencrypted = "" @@ -77,8 +78,9 @@ public enum NKProperties: String, CaseIterable { case metadataphotosgps = "" case metadatafileslivephoto = "" case hidden = "" - + /// open-collaboration-services.org case sharepermissionscollaboration = "" + /// open-cloud-mesh.org case sharepermissionscloudmesh = "" static func properties(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { From 5b667cb8b13dea9b39b11cbbffb75ad6805a5c15 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 6 Jul 2024 09:23:43 +0200 Subject: [PATCH 044/135] improvements Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 32 +++++++++---------- Sources/NextcloudKit/NKRequestOptions.swift | 8 ++--- .../NextcloudKit/NextcloudKit+WebDAV.swift | 14 ++++---- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 076e8947..77df849f 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -83,13 +83,13 @@ public enum NKProperties: String, CaseIterable { /// open-cloud-mesh.org case sharepermissionscloudmesh = "" - static func properties(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { + static func properties(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { var properties = allCases.map { $0.rawValue }.joined() - if !add.isEmpty { + if let createProperties { properties = "" - properties = add.map { $0.rawValue }.joined(separator: "") + properties = createProperties.map { $0.rawValue }.joined(separator: "") } - for removeProperty in remove { + for removeProperty in removeProperties { properties = properties.replacingOccurrences(of: removeProperty.rawValue, with: "") } return properties @@ -361,12 +361,12 @@ class NKDataFileXML: NSObject { """ - func getRequestBodyFile(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { + func getRequestBodyFile(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(add: add, remove: remove) + """ + """ + NKProperties.properties(createProperties: createProperties, removeProperties: removeProperties) + """ """ @@ -385,12 +385,12 @@ class NKDataFileXML: NSObject { """ - func getRequestBodyFileListingFavorites(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { + func getRequestBodyFileListingFavorites(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(add: add, remove: remove) + """ + """ + NKProperties.properties(createProperties: createProperties, removeProperties: removeProperties) + """ 1 @@ -400,14 +400,14 @@ class NKDataFileXML: NSObject { return request } - func getRequestBodySearchFileName(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { + func getRequestBodySearchFileName(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(add: add, remove: remove) + """ + """ + NKProperties.properties(createProperties: createProperties, removeProperties: removeProperties) + """ @@ -428,14 +428,14 @@ class NKDataFileXML: NSObject { return request } - func getRequestBodySearchFileId(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { + func getRequestBodySearchFileId(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(add: add, remove: remove) + """ + """ + NKProperties.properties(createProperties: createProperties, removeProperties: removeProperties) + """ @@ -456,14 +456,14 @@ class NKDataFileXML: NSObject { return request } - func getRequestBodySearchMedia(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { + func getRequestBodySearchMedia(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(add: add, remove: remove) + """ + """ + NKProperties.properties(createProperties: createProperties, removeProperties: removeProperties) + """ @@ -514,14 +514,14 @@ class NKDataFileXML: NSObject { return request } - func getRequestBodySearchMediaWithLimit(add: [NKProperties] = [], remove: [NKProperties] = []) -> String { + func getRequestBodySearchMediaWithLimit(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { let request = """ - """ + NKProperties.properties(add: add, remove: remove) + """ + """ + NKProperties.properties(createProperties: createProperties, removeProperties: removeProperties) + """ diff --git a/Sources/NextcloudKit/NKRequestOptions.swift b/Sources/NextcloudKit/NKRequestOptions.swift index a5c0886c..6c364b69 100644 --- a/Sources/NextcloudKit/NKRequestOptions.swift +++ b/Sources/NextcloudKit/NKRequestOptions.swift @@ -32,11 +32,11 @@ public class NKRequestOptions: NSObject { var e2eToken: String? var timeout: TimeInterval var taskDescription: String? - var addProperties: [NKProperties] + var createProperties: [NKProperties]? var removeProperties: [NKProperties] var queue: DispatchQueue - public init(endpoint: String? = nil, + public init(endpoint: String? = nil, version: String? = nil, customHeader: [String: String]? = nil, customUserAgent: String? = nil, @@ -44,7 +44,7 @@ public class NKRequestOptions: NSObject { e2eToken: String? = nil, timeout: TimeInterval = 60, taskDescription: String? = nil, - addProperties: [NKProperties] = [], + createProperties: [NKProperties]? = nil, removeProperties: [NKProperties] = [], queue: DispatchQueue = .main) { @@ -56,7 +56,7 @@ public class NKRequestOptions: NSObject { self.e2eToken = e2eToken self.timeout = timeout self.taskDescription = taskDescription - self.addProperties = addProperties + self.createProperties = createProperties self.removeProperties = removeProperties self.queue = queue } diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 2ea33e74..21b47540 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -228,7 +228,7 @@ public extension NextcloudKit { urlRequest.httpBody = requestBody urlRequest.timeoutInterval = options.timeout } else { - urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFile(add: options.addProperties, remove: options.removeProperties).data(using: .utf8) + urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFile(createProperties: options.createProperties, removeProperties: options.removeProperties).data(using: .utf8) } } catch { return options.queue.async { completion(account, files, nil, NKError(error: error)) } @@ -266,11 +266,11 @@ public extension NextcloudKit { let urlBase = self.nkCommonInstance.urlBase var httpBody: Data? if let fileId = fileId { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(add: options.addProperties, remove: options.removeProperties), userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) } else if let link = link { let linkArray = link.components(separatedBy: "/") if let fileId = linkArray.last { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(add: options.addProperties, remove: options.removeProperties), userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) } } guard let httpBody = httpBody else { @@ -314,7 +314,7 @@ public extension NextcloudKit { guard let href = ("/files/" + userId).urlEncoded else { return options.queue.async { completion(account, [], nil, .urlError) } } - let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(add: options.addProperties, remove: options.removeProperties), href, depth, "%" + literal + "%") + let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(createProperties: options.createProperties, removeProperties: options.removeProperties), href, depth, "%" + literal + "%") if let httpBody = requestBody.data(using: .utf8) { search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in taskHandler(task) @@ -357,9 +357,9 @@ public extension NextcloudKit { if let lessDateString, let greaterDateString { if limit > 0 { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMediaWithLimit(add: options.addProperties, remove: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString, String(limit)) + requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMediaWithLimit(createProperties: options.createProperties, removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString, String(limit)) } else { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMedia(add: options.addProperties, remove: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString) + requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMedia(createProperties: options.createProperties, removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString) } } @@ -485,7 +485,7 @@ public extension NextcloudKit { do { try urlRequest = URLRequest(url: url, method: method, headers: headers) - urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFileListingFavorites(add: options.addProperties, remove: options.removeProperties).data(using: .utf8) + urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFileListingFavorites(createProperties: options.createProperties, removeProperties: options.removeProperties).data(using: .utf8) urlRequest.timeoutInterval = options.timeout } catch { return options.queue.async { completion(account, files, nil, NKError(error: error)) } From 6b90394a82315d723981c5cbacdc3dd0946e2207 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 10 Jul 2024 08:56:03 +0200 Subject: [PATCH 045/135] downloadPreview Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+API.swift | 30 ++++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 6541a564..e8641a75 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -222,14 +222,30 @@ public extension NextcloudKit { // MARK: - - func getPreview(url: URL, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + func downloadPreview(fileId: String, + widthPreview: Int = 512, + heightPreview: Int = 512, + etag: String? = nil, + crop: Int = 1, + cropMode: String = "cover", + forceIcon: Int = 0, + mimeFallback: Int = 0, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { let account = self.nkCommonInstance.account - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let urlBase = self.nkCommonInstance.urlBase + let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + guard let urlRequest = url else { + return options.queue.async { completion(account, nil, .urlError) } + } + var headers = self.nkCommonInstance.getStandardHeaders(options: options) + if var etag = etag { + etag = "\"" + etag + "\"" + headers.update(name: "If-None-Match", value: etag) + } + sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in From f75a16bb3d77831c8c4fd8819ce280856f86f05b Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 10 Jul 2024 09:46:28 +0200 Subject: [PATCH 046/135] added downloadPreview url Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+API.swift | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index e8641a75..733f2625 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -222,6 +222,40 @@ public extension NextcloudKit { // MARK: - + func downloadPreview(url: URL, + etag: String? = nil, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + let account = self.nkCommonInstance.account + var headers = self.nkCommonInstance.getStandardHeaders(options: options) + if var etag = etag { + etag = "\"" + etag + "\"" + headers.update(name: "If-None-Match", value: etag) + } + + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.response(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, nil, error) } + case .success: + if let data = response.data { + options.queue.async { completion(account, data, .success) } + } else { + options.queue.async { completion(account, nil, .invalidData) } + } + } + } + } + func downloadPreview(fileId: String, widthPreview: Int = 512, heightPreview: Int = 512, @@ -245,6 +279,7 @@ public extension NextcloudKit { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } + sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) From 4cf36e5dcbd5874218d5e75bf08e3e5ab71b44b6 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 19 Jul 2024 11:01:50 +0200 Subject: [PATCH 047/135] httpCookieStorage (#80) Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 62 ++++++++++++------------- Sources/NextcloudKit/NextcloudKit.swift | 43 ++++++----------- 2 files changed, 44 insertions(+), 61 deletions(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 6cbd77f5..210c11f1 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -97,19 +97,26 @@ public class NKCommon: NSObject { internal lazy var sessionConfiguration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.af.default configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + if let groupIdentifier { + let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: groupIdentifier) + configuration.httpCookieStorage = cookieStorage + } else { + configuration.httpCookieStorage = nil + } return configuration }() internal var rootQueue: DispatchQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.sessionManagerData.rootQueue") internal var requestQueue: DispatchQueue? internal var serializationQueue: DispatchQueue? - internal var internalUser = "" - internal var internalUserId = "" - internal var internalPassword = "" - internal var internalAccount = "" - internal var internalUrlBase = "" - internal var internalUserAgent: String? - internal var internalNextcloudVersion: Int = 0 + internal var _user = "" + internal var _userId = "" + internal var _password = "" + internal var _account = "" + internal var _urlBase = "" + internal var _userAgent: String? + internal var _nextcloudVersion: Int = 0 + internal var _groupIdentifier: String? internal var internalTypeIdentifiers: [UTTypeConformsToServer] = [] internal var utiCache = NSCache() @@ -126,31 +133,35 @@ public class NKCommon: NSObject { private let queueLog = DispatchQueue(label: "com.nextcloud.nextcloudkit.queuelog", attributes: .concurrent ) public var user: String { - return internalUser + return _user } public var userId: String { - return internalUserId + return _userId } public var password: String { - return internalPassword + return _password } public var account: String { - return internalAccount + return _account } public var urlBase: String { - return internalUrlBase + return _urlBase } public var userAgent: String? { - return internalUserAgent + return _userAgent } public var nextcloudVersion: Int { - return internalNextcloudVersion + return _nextcloudVersion + } + + public var groupIdentifier: String? { + return _groupIdentifier } public let backgroundQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.backgroundqueue", qos: .background, attributes: .concurrent) @@ -225,7 +236,6 @@ public class NKCommon: NSObject { // MARK: - Type Identifier public func getInternalTypeIdentifier(typeIdentifier: String) -> [UTTypeConformsToServer] { - var results: [UTTypeConformsToServer] = [] for internalTypeIdentifier in internalTypeIdentifiers { @@ -233,7 +243,6 @@ public class NKCommon: NSObject { results.append(internalTypeIdentifier) } } - return results } @@ -305,7 +314,6 @@ public class NKCommon: NSObject { iconName = fileProperties.iconName } } - return(mimeType: mimeType, classFile: classFile, iconName: iconName, typeIdentifier: typeIdentifier, fileNameWithoutExt: fileNameWithoutExt, ext: ext) } @@ -367,17 +375,19 @@ public class NKCommon: NSObject { } } } - return fileProperty } // MARK: - Chunked File - public func chunkedFile(inputDirectory: String, outputDirectory: String, fileName: String, chunkSize: Int, filesChunk: [(fileName: String, size: Int64)], + public func chunkedFile(inputDirectory: String, + outputDirectory: String, + fileName: String, + chunkSize: Int, + filesChunk: [(fileName: String, size: Int64)], numChunks: @escaping (_ num: Int) -> Void = { _ in }, counterChunk: @escaping (_ counter: Int) -> Void = { _ in }, completion: @escaping (_ filesChunk: [(fileName: String, size: Int64)]) -> Void = { _ in }) { - // Check if filesChunk is empty if !filesChunk.isEmpty { return completion(filesChunk) } @@ -422,13 +432,10 @@ public class NKCommon: NSObject { } repeat { - if stop { return completion([]) } - if autoreleasepool(invoking: { () -> Int in - if chunk >= chunkSize { writer?.closeFile() writer = nil @@ -461,9 +468,7 @@ public class NKCommon: NSObject { } filesChunk = [] return 0 - }) == 0 { break } - } while true writer?.closeFile() @@ -476,7 +481,6 @@ public class NKCommon: NSObject { filesChunk[counter].size = incrementalSize counter += 1 } - return completion(filesChunk) } @@ -510,18 +514,16 @@ public class NKCommon: NSObject { headers.update(name: "Accept", value: "application/json") } headers.update(name: "OCS-APIRequest", value: "true") - for (key, value) in appendHeaders ?? [:] { headers.update(name: key, value: value) } - return headers } public func createStandardUrl(serverUrl: String, endpoint: String) -> URLConvertible? { guard var serverUrl = serverUrl.urlEncoded else { return nil } - if serverUrl.last != "/" { serverUrl = serverUrl + "/" } + if serverUrl.last != "/" { serverUrl = serverUrl + "/" } serverUrl = serverUrl + endpoint return serverUrl.asUrl } @@ -532,7 +534,6 @@ public class NKCommon: NSObject { dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.dateFormat = format - guard let date = dateFormatter.date(from: dateString) else { return nil } return date } @@ -542,7 +543,6 @@ public class NKCommon: NSObject { dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.dateFormat = format - return dateFormatter.string(from: date) } diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index cfb7f3a4..9cdc5cfd 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -71,14 +71,15 @@ open class NextcloudKit: SessionDelegate { // MARK: - Setup - public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, userAgent: String, nextcloudVersion: Int, delegate: NKCommonDelegate?) { - self.setup(account: account, user: user, userId: userId, password: password, urlBase: urlBase) + public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, userAgent: String, nextcloudVersion: Int, groupIdentifier: String? = nil, delegate: NKCommonDelegate?) { + self.setup(account: account, user: user, userId: userId, password: password, urlBase: urlBase, groupIdentifier: groupIdentifier) self.setup(userAgent: userAgent) self.setup(nextcloudVersion: nextcloudVersion) self.setup(delegate: delegate) } - public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String) { + public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, groupIdentifier: String? = nil) { + self.nkCommonInstance._groupIdentifier = groupIdentifier if (self.nkCommonInstance.account != account) || (self.nkCommonInstance.urlBase != urlBase && self.nkCommonInstance.user != user) { if let cookieStore = sessionManager.session.configuration.httpCookieStorage { for cookie in cookieStore.cookies ?? [] { @@ -88,15 +89,15 @@ open class NextcloudKit: SessionDelegate { self.nkCommonInstance.internalTypeIdentifiers = [] } - if let account = account { - self.nkCommonInstance.internalAccount = account + if let account { + self.nkCommonInstance._account = account } else { - self.nkCommonInstance.internalAccount = "" + self.nkCommonInstance._account = "" } - self.nkCommonInstance.internalUser = user - self.nkCommonInstance.internalUserId = userId - self.nkCommonInstance.internalPassword = password - self.nkCommonInstance.internalUrlBase = urlBase + self.nkCommonInstance._user = user + self.nkCommonInstance._userId = userId + self.nkCommonInstance._password = password + self.nkCommonInstance._urlBase = urlBase } public func setup(delegate: NKCommonDelegate?) { @@ -104,29 +105,11 @@ open class NextcloudKit: SessionDelegate { } public func setup(userAgent: String) { - self.nkCommonInstance.internalUserAgent = userAgent + self.nkCommonInstance._userAgent = userAgent } public func setup(nextcloudVersion: Int) { - self.nkCommonInstance.internalNextcloudVersion = nextcloudVersion - } - - public func setupSessionManager(sessionConfiguration: URLSessionConfiguration?, - rootQueue: DispatchQueue?, - requestQueue: DispatchQueue?, - serializationQueue: DispatchQueue?) { - if let sessionConfiguration = sessionConfiguration { - self.nkCommonInstance.sessionConfiguration = sessionConfiguration - } - if let rootQueue = rootQueue { - self.nkCommonInstance.rootQueue = rootQueue - } - if let requestQueue = requestQueue { - self.nkCommonInstance.requestQueue = requestQueue - } - if let serializationQueue = serializationQueue { - self.nkCommonInstance.serializationQueue = serializationQueue - } + self.nkCommonInstance._nextcloudVersion = nextcloudVersion } /* From 499fa9b04d574c88a91b6bd8a848f567f53c0882 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 20 Jul 2024 11:59:59 +0200 Subject: [PATCH 048/135] new getUserProfile() Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 8 ++--- Sources/NextcloudKit/NextcloudKit+API.swift | 37 +++++++++++++++++++-- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 210c11f1..2b9795a8 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -497,15 +497,15 @@ public class NKCommon: NSObject { public func getStandardHeaders(user: String?, password: String?, appendHeaders: [String: String]?, customUserAgent: String?, contentType: String? = nil) -> HTTPHeaders { var headers: HTTPHeaders = [] - if let username = user, let password = password { - headers.update(.authorization(username: username, password: password)) + if let user, let password { + headers.update(.authorization(username: user, password: password)) } - if let customUserAgent = customUserAgent { + if let customUserAgent { headers.update(.userAgent(customUserAgent)) } else if let userAgent = userAgent { headers.update(.userAgent(userAgent)) } - if let contentType = contentType { + if let contentType { headers.update(.contentType(contentType)) } else { headers.update(.contentType("application/x-www-form-urlencoded")) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 733f2625..9d61205a 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -536,6 +536,25 @@ public extension NextcloudKit { // MARK: - + func getUserProfile(url: String, + user: String, + password: String, + userAgent: String, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { + let endpoint = "ocs/v2.php/cloud/user" + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint) else { + return completion(nil, nil, .urlError) + } + let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: nil, customUserAgent: userAgent) + + getUserProfile(url: url, headers: headers, options: NKRequestOptions()) { task in + taskHandler(task) + } completion: { userProfile, data, error in + completion(userProfile, data, error) + } + } + func getUserProfile(options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { @@ -547,6 +566,18 @@ public extension NextcloudKit { } let headers = self.nkCommonInstance.getStandardHeaders(options: options) + getUserProfile(url: url, headers: headers, options: options) { task in + taskHandler(task) + } completion: { userProfile, data, error in + completion(account, userProfile, data, error) + } + } + + private func getUserProfile(url: URLConvertible, + headers: HTTPHeaders, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) @@ -557,7 +588,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(nil, nil, error) } case .success(let jsonData): let json = JSON(jsonData) let ocs = json["ocs"] @@ -597,9 +628,9 @@ public extension NextcloudKit { userProfile.twitter = data["twitter"].stringValue userProfile.website = data["website"].stringValue - options.queue.async { completion(account, userProfile, jsonData, .success) } + options.queue.async { completion(userProfile, jsonData, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } From 93b4913e4d56234a8929b0ca8d7ac5a84933082f Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 20 Jul 2024 12:24:08 +0200 Subject: [PATCH 049/135] change paramenter name Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Login.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index e67c712d..0d3e585a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -27,18 +27,18 @@ import SwiftyJSON public extension NextcloudKit { // MARK: - App Password - func getAppPassword(serverUrl: String, - username: String, + func getAppPassword(url: String, + user: String, password: String, userAgent: String? = nil, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/getapppassword" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint) else { return options.queue.async { completion(nil, nil, .urlError) } } - var headers: HTTPHeaders = [.authorization(username: username, password: password)] + var headers: HTTPHeaders = [.authorization(username: user, password: password)] if let userAgent = userAgent { headers.update(.userAgent(userAgent)) } From f4f645fa11675344fe201ebe5adbb0fb40c9bb72 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 31 Jul 2024 10:25:40 +0200 Subject: [PATCH 050/135] Account (#82) * cod Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 4 +- Sources/NextcloudKit/NextcloudKit+API.swift | 85 ++++++------------- .../NextcloudKit/NextcloudKit+Assistant.swift | 12 +-- .../NextcloudKit/NextcloudKit+Comments.swift | 10 +-- .../NextcloudKit/NextcloudKit+Dashboard.swift | 6 +- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 30 +++---- .../NextcloudKit/NextcloudKit+FilesLock.swift | 2 +- .../NextcloudKit+Groupfolders.swift | 4 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 2 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 2 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 7 +- .../NextcloudKit/NextcloudKit+NCText.swift | 12 +-- .../NextcloudKit+PushNotification.swift | 22 ++--- .../NextcloudKit+Richdocuments.swift | 8 +- .../NextcloudKit/NextcloudKit+Search.swift | 10 +-- Sources/NextcloudKit/NextcloudKit+Share.swift | 26 +++--- .../NextcloudKit+UserStatus.swift | 18 ++-- .../NextcloudKit/NextcloudKit+WebDAV.swift | 39 ++++----- Sources/NextcloudKit/NextcloudKit.swift | 16 ++-- .../NextcloudKit/NextcloudKitBackground.swift | 2 + .../FilesIntegrationTests.swift | 8 +- .../ShareIntegrationTests.swift | 4 +- 22 files changed, 153 insertions(+), 176 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 77df849f..c1f4168c 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -607,7 +607,7 @@ class NKDataFileXML: NSObject { return xml["ocs", "data", "apppassword"].text } - func convertDataFile(xmlData: Data, dav: String, urlBase: String, user: String, userId: String, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] { + func convertDataFile(xmlData: Data, dav: String, urlBase: String, user: String, userId: String, account: String, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] { var files: [NKFile] = [] let rootFiles = "/" + dav + "/files/" guard let baseUrl = self.nkCommonInstance.getHostName(urlString: urlBase) else { @@ -641,7 +641,7 @@ class NKDataFileXML: NSObject { } // account - file.account = self.nkCommonInstance.account + file.account = account // path file.path = (fileNamePath as NSString).deletingLastPathComponent + "/" diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 9d61205a..f7f3382e 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -57,7 +57,7 @@ public extension NextcloudKit { return options.queue.async { completion(.urlError) } } - sessionManager.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in + AF.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -78,10 +78,10 @@ public extension NextcloudKit { func generalWithEndpoint(_ endpoint: String, method: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, .urlError) } @@ -108,10 +108,10 @@ public extension NextcloudKit { // MARK: - - func getExternalSite(options: NKRequestOptions = NKRequestOptions(), + func getExternalSite(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var externalSites: [NKExternalSite] = [] let endpoint = "ocs/v2.php/apps/external/api/v1" @@ -223,11 +223,11 @@ public extension NextcloudKit { // MARK: - func downloadPreview(url: URL, + account: String, etag: String? = nil, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account var headers = self.nkCommonInstance.getStandardHeaders(options: options) if var etag = etag { etag = "\"" + etag + "\"" @@ -264,10 +264,10 @@ public extension NextcloudKit { cropMode: String = "cover", forceIcon: Int = 0, mimeFallback: Int = 0, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) @@ -314,12 +314,13 @@ public extension NextcloudKit { cropMode: String = "cover", forceIcon: Int = 0, mimeFallback: Int = 0, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: etag, endpoint: endpoint, options: options) { task in + downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: etag, endpoint: endpoint, account: account, options: options) { task in taskHandler(task) } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in completion(account, imagePreview, imageIcon, imageOriginal,etag,error) @@ -338,12 +339,13 @@ public extension NextcloudKit { cropMode: String = "cover", forceIcon: Int = 0, mimeFallback: Int = 0, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: nil, endpoint: endpoint, options: options) { task in + downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: nil, endpoint: endpoint, account: account, options: options) { task in taskHandler(task) } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in completion(account, imagePreview, imageIcon, imageOriginal,etag,error) @@ -357,10 +359,10 @@ public extension NextcloudKit { compressionQualityIcon: CGFloat, etag: String? = nil, endpoint: String?, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var url: URLConvertible? if let endpoint { @@ -417,10 +419,10 @@ public extension NextcloudKit { sizeImage: Int, avatarSizeRounded: Int = 0, etag: String?, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/avatar/\(user)/\(sizeImage)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -504,10 +506,10 @@ public extension NextcloudKit { } func downloadContent(serverUrl: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account guard let url = serverUrl.asUrl else { return options.queue.async { completion(account, nil, .urlError) } } @@ -536,29 +538,10 @@ public extension NextcloudKit { // MARK: - - func getUserProfile(url: String, - user: String, - password: String, - userAgent: String, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { - let endpoint = "ocs/v2.php/cloud/user" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint) else { - return completion(nil, nil, .urlError) - } - let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: nil, customUserAgent: userAgent) - - getUserProfile(url: url, headers: headers, options: NKRequestOptions()) { task in - taskHandler(task) - } completion: { userProfile, data, error in - completion(userProfile, data, error) - } - } - - func getUserProfile(options: NKRequestOptions = NKRequestOptions(), + func getUserProfile(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/cloud/user" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -566,18 +549,6 @@ public extension NextcloudKit { } let headers = self.nkCommonInstance.getStandardHeaders(options: options) - getUserProfile(url: url, headers: headers, options: options) { task in - taskHandler(task) - } completion: { userProfile, data, error in - completion(account, userProfile, data, error) - } - } - - private func getUserProfile(url: URLConvertible, - headers: HTTPHeaders, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) @@ -588,7 +559,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(nil, nil, error) } + options.queue.async { completion(account, nil, nil, error) } case .success(let jsonData): let json = JSON(jsonData) let ocs = json["ocs"] @@ -628,18 +599,18 @@ public extension NextcloudKit { userProfile.twitter = data["twitter"].stringValue userProfile.website = data["website"].stringValue - options.queue.async { completion(userProfile, jsonData, .success) } + options.queue.async { completion(account, userProfile, jsonData, .success) } } else { - options.queue.async { completion(nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } } - func getCapabilities(options: NKRequestOptions = NKRequestOptions(), + func getCapabilities(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v1.php/cloud/capabilities" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -672,10 +643,10 @@ public extension NextcloudKit { func getRemoteWipeStatus(serverUrl: String, token: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ wipe: Bool, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let endpoint = "index.php/core/wipe/check" let parameters: [String: Any] = ["token": token] let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") @@ -704,10 +675,10 @@ public extension NextcloudKit { func setRemoteWipeCompletition(serverUrl: String, token: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let endpoint = "index.php/core/wipe/success" let parameters: [String: Any] = ["token": token] let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") @@ -739,10 +710,10 @@ public extension NextcloudKit { objectId: String?, objectType: String?, previews: Bool, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var activities: [NKActivity] = [] var activityFirstKnown = 0 @@ -836,10 +807,10 @@ public extension NextcloudKit { // MARK: - - func getNotifications(options: NKRequestOptions = NKRequestOptions(), + func getNotifications(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications" var notifications: [NKNotifications] = [] @@ -910,10 +881,10 @@ public extension NextcloudKit { func setNotification(serverUrl: String?, idNotification: Int, method: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var url: URLConvertible? if serverUrl == nil { @@ -948,10 +919,10 @@ public extension NextcloudKit { // MARK: - func getDirectDownload(fileId: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/dav/api/v1/direct" let parameters: [String: Any] = [ @@ -986,10 +957,10 @@ public extension NextcloudKit { // MARK: - func sendClientDiagnosticsRemoteOperation(data: Data, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/security_guard/diagnostics" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 7f889c55..15e75380 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -26,10 +26,10 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func textProcessingGetTypes(options: NKRequestOptions = NKRequestOptions(), + func textProcessingGetTypes(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ types: [NKTextProcessingTaskType]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/textprocessing/tasktypes" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -66,10 +66,10 @@ public extension NextcloudKit { typeId: String, appId: String = "assistant", identifier: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/schedule" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -104,10 +104,10 @@ public extension NextcloudKit { } func textProcessingGetTask(taskId: Int, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -141,10 +141,10 @@ public extension NextcloudKit { } func textProcessingDeleteTask(taskId: Int, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -178,10 +178,10 @@ public extension NextcloudKit { } func textProcessingTaskList(appId: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: [NKTextProcessingTask]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/tasks/app/\(appId)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index d0257196..61bd2174 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -26,10 +26,10 @@ import Alamofire public extension NextcloudKit { func getComments(fileId: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ items: [NKComments]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" @@ -72,10 +72,10 @@ public extension NextcloudKit { func putComments(fileId: String, message: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" @@ -114,10 +114,10 @@ public extension NextcloudKit { func updateComments(fileId: String, messageId: String, message: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" @@ -156,10 +156,10 @@ public extension NextcloudKit { func deleteComments(fileId: String, messageId: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" @@ -187,10 +187,10 @@ public extension NextcloudKit { } func markAsReadComments(fileId: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 280f17ce..4c4651eb 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -26,11 +26,11 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func getDashboardWidget(options: NKRequestOptions = NKRequestOptions(), + func getDashboardWidget(account: String, + options: NKRequestOptions = NKRequestOptions(), request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardWidgets: [NCCDashboardWidget]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var url: URLConvertible? if let endpoint = options.endpoint { @@ -71,11 +71,11 @@ public extension NextcloudKit { } func getDashboardWidgetsApplication(_ items: String, + account: String, options: NKRequestOptions = NKRequestOptions(), request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardApplications: [NCCDashboardApplication]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var url: URLConvertible? if let endpoint = options.endpoint { diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index f4711fca..be39affc 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -28,10 +28,10 @@ import SwiftyJSON public extension NextcloudKit { func markE2EEFolder(fileId: String, delete: Bool, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { @@ -71,10 +71,10 @@ public extension NextcloudKit { e2eToken: String?, e2eCounter: String?, method: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eToken: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { @@ -122,10 +122,10 @@ public extension NextcloudKit { func getE2EEMetadata(fileId: String, e2eToken: String?, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eMetadata: String?, _ signature: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { @@ -172,10 +172,10 @@ public extension NextcloudKit { e2eMetadata: String?, signature: String?, method: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ metadata: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { @@ -226,10 +226,10 @@ public extension NextcloudKit { // MARK: - func getE2EECertificate(user: String? = nil, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ certificateUser: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let userId = self.nkCommonInstance.userId var version = "v1" @@ -279,10 +279,10 @@ public extension NextcloudKit { } } - func getE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions(), + func getE2EEPrivateKey(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { @@ -318,10 +318,10 @@ public extension NextcloudKit { } } - func getE2EEPublicKey(options: NKRequestOptions = NKRequestOptions(), + func getE2EEPublicKey(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { @@ -358,10 +358,10 @@ public extension NextcloudKit { } func signE2EECertificate(certificate: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { @@ -400,10 +400,10 @@ public extension NextcloudKit { } func storeE2EEPrivateKey(privateKey: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { @@ -440,10 +440,10 @@ public extension NextcloudKit { } } - func deleteE2EECertificate(options: NKRequestOptions = NKRequestOptions(), + func deleteE2EECertificate(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { @@ -472,10 +472,10 @@ public extension NextcloudKit { } } - func deleteE2EEPrivateKey(options: NKRequestOptions = NKRequestOptions(), + func deleteE2EEPrivateKey(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index d4b7f0d2..851915df 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -29,10 +29,10 @@ import SwiftyJSON public extension NextcloudKit { func lockUnlockFile(serverUrlFileName: String, shouldLock: Bool, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, .urlError) } diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 68b63269..5c2b1e85 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -26,10 +26,10 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func getGroupfolders(options: NKRequestOptions = NKRequestOptions(), + func getGroupfolders(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ results: [NKGroupfolders]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/apps/groupfolders/folders?applicable=1" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index 9509295c..e1964925 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -28,10 +28,10 @@ import SwiftyJSON public extension NextcloudKit { func getHovercard(for userId: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ result: NKHovercard?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/hovercard/v1/\(userId)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 6f589252..8259d105 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -27,10 +27,10 @@ import Alamofire public extension NextcloudKit { func setLivephoto(serverUrlfileNamePath: String, livePhotoFile: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account guard let url = serverUrlfileNamePath.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 0d3e585a..1bb056b1 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -51,7 +51,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + AF.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -77,6 +77,7 @@ public extension NextcloudKit { username: String, password: String, userAgent: String? = nil, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { @@ -129,7 +130,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + AF.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -166,7 +167,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + AF.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index 7639ac2a..f6d7cfc2 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -26,10 +26,10 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func NCTextObtainEditorDetails(options: NKRequestOptions = NKRequestOptions(), + func NCTextObtainEditorDetails(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing" var editors: [NKEditorDetailsEditors] = [] @@ -92,10 +92,10 @@ public extension NextcloudKit { func NCTextOpenFile(fileNamePath: String, fileId: String? = nil, editor: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } @@ -128,10 +128,10 @@ public extension NextcloudKit { } } - func NCTextGetListOfTemplates(options: NKRequestOptions = NKRequestOptions(), + func NCTextGetListOfTemplates(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ templates: [NKEditorTemplates], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/templates/text/textdocumenttemplate" var templates: [NKEditorTemplates] = [] @@ -174,10 +174,10 @@ public extension NextcloudKit { editorId: String, creatorId: String, templateId: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index 003f063a..656704ca 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -27,12 +27,12 @@ import SwiftyJSON public extension NextcloudKit { func subscribingPushNotification(serverUrl: String, - account: String, user: String, password: String, pushTokenHash: String, devicePublicKey: String, proxyServerUrl: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { @@ -74,9 +74,9 @@ public extension NextcloudKit { } func unsubscribingPushNotification(serverUrl: String, - account: String, user: String, password: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { @@ -108,13 +108,14 @@ public extension NextcloudKit { deviceIdentifier: String, signature: String, publicKey: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "devices?format=json" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), let userAgent = options.customUserAgent else { - return options.queue.async { completion(.urlError) } + return options.queue.async { completion(account, .urlError) } } let parameters = [ "pushToken": pushToken, @@ -134,9 +135,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(error) } + options.queue.async { completion(account, error) } case .success: - options.queue.async { completion(.success) } + options.queue.async { completion(account, .success) } } } } @@ -145,13 +146,14 @@ public extension NextcloudKit { deviceIdentifier: String, signature: String, publicKey: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "devices" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), let userAgent = options.customUserAgent else { - return options.queue.async { completion(.urlError) } + return options.queue.async { completion(account, .urlError) } } let parameters = [ "deviceIdentifier": deviceIdentifier, @@ -170,9 +172,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(error) } + options.queue.async { completion(account, error) } case .success: - options.queue.async { completion(.success) } + options.queue.async { completion(account, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index 215193e5..a4a55f4d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -27,10 +27,10 @@ import SwiftyJSON public extension NextcloudKit { func createUrlRichdocuments(fileID: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/document" let parameters: [String: Any] = ["fileId": fileID] @@ -63,10 +63,10 @@ public extension NextcloudKit { } func getTemplatesRichdocuments(typeTemplate: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ templates: [NKRichdocumentsTemplate]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/\(typeTemplate)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -111,10 +111,10 @@ public extension NextcloudKit { func createRichdocuments(path: String, templateId: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/new" let parameters: [String: Any] = ["path": path, "template": templateId] @@ -147,10 +147,10 @@ public extension NextcloudKit { } func createAssetRichdocuments(path: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/apps/richdocuments/assets" let parameters: [String: Any] = ["path": path] diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index 165ad45e..e501727f 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -44,6 +44,7 @@ public extension NextcloudKit { 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, @@ -51,7 +52,6 @@ public extension NextcloudKit { providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, update: @escaping (_ account: String, _ searchResult: NKSearchResult?, _ provider: NKSearchProvider, _ error: NKError) -> Void, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/search/providers" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -80,7 +80,7 @@ public extension NextcloudKit { for provider in filteredProviders { group.enter() - let requestSearchProvider = self.searchProvider(provider.id, account: account, term: term, options: options, timeout: timeoutProvider) { account, partial, _, error in + 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() } @@ -114,14 +114,14 @@ public extension NextcloudKit { /// - update: Callback, notifying that a search provider return its result. Does not include previous results. /// - completion: Callback, notifying that all search results. func searchProvider(_ id: String, - account: String, term: String, limit: Int? = nil, cursor: Int? = nil, - options: NKRequestOptions = NKRequestOptions(), timeout: TimeInterval = 60, + account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ accoun: String, NKSearchResult?, _ data: Data?, _ error: NKError) -> Void) -> DataRequest? { + completion: @escaping (_ account: String, NKSearchResult?, _ data: Data?, _ error: NKError) -> Void) -> DataRequest? { let urlBase = self.nkCommonInstance.urlBase guard let term = term.urlEncoded else { completion(account, nil, nil, .urlError) diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 27d08118..70186482 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -76,10 +76,10 @@ public class NKShareParameter: NSObject { public extension NextcloudKit { func readShares(parameters: NKShareParameter, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ shares: [NKShare]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: parameters.endpoint) else { @@ -108,7 +108,7 @@ public extension NextcloudKit { } var shares: [NKShare] = [] for (_, subJson): (String, JSON) in json["ocs"]["data"] { - let share = self.convertResponseShare(json: subJson) + let share = self.convertResponseShare(json: subJson, account: account) shares.append(share) } options.queue.async { completion(account, shares, jsonData, .success) } @@ -128,10 +128,10 @@ public extension NextcloudKit { page: Int = 1, perPage: Int = 200, itemType: String = "file", lookup: Bool = false, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ sharees: [NKSharee]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/sharees" var lookupString = "false" @@ -246,10 +246,11 @@ public extension NextcloudKit { publicUpload: Bool = false, password: String? = nil, permissions: Int = 1, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - createShare(path: path, shareType: 3, shareWith: nil, publicUpload: publicUpload, hideDownload: hideDownload, password: password, permissions: permissions, options: options) { task in + createShare(path: path, shareType: 3, shareWith: nil, publicUpload: publicUpload, hideDownload: hideDownload, password: password, permissions: permissions, account: account, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) } completion: { account, share, data, error in @@ -264,10 +265,11 @@ public extension NextcloudKit { note: String? = nil, permissions: Int = 1, attributes: String? = nil, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - createShare(path: path, shareType: shareType, shareWith: shareWith, publicUpload: false, note: note, hideDownload: false, password: password, permissions: permissions, attributes: attributes, options: options) { task in + createShare(path: path, shareType: shareType, shareWith: shareWith, publicUpload: false, note: note, hideDownload: false, password: password, permissions: permissions, attributes: attributes, account: account, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) } completion: { account, share, data, error in @@ -284,11 +286,10 @@ public extension NextcloudKit { password: String? = nil, permissions: Int = 1, attributes: String? = nil, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -334,7 +335,7 @@ public extension NextcloudKit { let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - options.queue.async { completion(account, self.convertResponseShare(json: json["ocs"]["data"]), jsonData, .success) } + options.queue.async { completion(account, self.convertResponseShare(json: json["ocs"]["data"], account: account), jsonData, .success) } } else { options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } @@ -371,10 +372,10 @@ public extension NextcloudKit { label: String? = nil, hideDownload: Bool, attributes: String? = nil, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -419,7 +420,7 @@ public extension NextcloudKit { let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - options.queue.async { completion(account, self.convertResponseShare(json: json["ocs"]["data"]), jsonData, .success) } + options.queue.async { completion(account, self.convertResponseShare(json: json["ocs"]["data"], account: account), jsonData, .success) } } else { options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } @@ -431,10 +432,10 @@ public extension NextcloudKit { * @param idShare Identifier of the share to update */ func deleteShare(idShare: Int, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -461,10 +462,9 @@ public extension NextcloudKit { // MARK: - - private func convertResponseShare(json: JSON) -> NKShare { + private func convertResponseShare(json: JSON, account: String) -> NKShare { let share = NKShare() - share.account = self.nkCommonInstance.account share.canDelete = json["can_delete"].boolValue share.canEdit = json["can_edit"].boolValue share.displaynameFileOwner = json["displayname_file_owner"].stringValue diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index eaa58f94..ce488f90 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -27,10 +27,10 @@ import SwiftyJSON public extension NextcloudKit { func getUserStatus(userId: String? = nil, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ clearAt: Date?, _ icon: String?, _ message: String?, _ messageId: String?, _ messageIsPredefined: Bool, _ status: String?, _ statusIsUserDefined: Bool, _ userId: String?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status" if let userId = userId { @@ -78,10 +78,10 @@ public extension NextcloudKit { } func setUserStatus(status: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/status" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -117,10 +117,10 @@ public extension NextcloudKit { func setCustomMessagePredefined(messageId: String, clearAt: Double, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/predefined" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -161,10 +161,10 @@ public extension NextcloudKit { func setCustomMessageUserDefined(statusIcon: String?, message: String, clearAt: Double, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/custom" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -205,10 +205,10 @@ public extension NextcloudKit { } } - func clearMessage(options: NKRequestOptions = NKRequestOptions(), + func clearMessage(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { @@ -240,10 +240,10 @@ public extension NextcloudKit { } } - func getUserStatusPredefinedStatuses(options: NKRequestOptions = NKRequestOptions(), + func getUserStatusPredefinedStatuses(account: String, + options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var userStatuses: [NKUserStatus] = [] let endpoint = "ocs/v2.php/apps/user_status/api/v1/predefined_statuses" @@ -294,10 +294,10 @@ public extension NextcloudKit { func getUserStatusRetrieveStatuses(limit: Int, offset: Int, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let urlBase = self.nkCommonInstance.urlBase var userStatuses: [NKUserStatus] = [] let endpoint = "ocs/v2.php/apps/user_status/api/v1/statuses" diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 21b47540..61d79eb4 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -27,10 +27,10 @@ import SwiftyJSON public extension NextcloudKit { func createFolder(serverUrlFileName: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -72,10 +72,10 @@ public extension NextcloudKit { } func deleteFileOrFolder(serverUrlFileName: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } @@ -109,10 +109,10 @@ public extension NextcloudKit { func moveFileOrFolder(serverUrlFileNameSource: String, serverUrlFileNameDestination: String, overwrite: Bool, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account guard let url = serverUrlFileNameSource.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } @@ -153,10 +153,10 @@ public extension NextcloudKit { func copyFileOrFolder(serverUrlFileNameSource: String, serverUrlFileNameDestination: String, overwrite: Bool, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account guard let url = serverUrlFileNameSource.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } @@ -199,10 +199,10 @@ public extension NextcloudKit { showHiddenFiles: Bool = true, includeHiddenFiles: [String] = [], requestBody: Data? = nil, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let user = self.nkCommonInstance.user let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase @@ -247,7 +247,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -258,10 +258,10 @@ public extension NextcloudKit { func getFileFromFileId(fileId: String? = nil, link: String? = nil, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ file: NKFile?, _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase var httpBody: Data? @@ -277,7 +277,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], options: options) { task in + search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], account: account, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) } completion: { account, files, data, error in @@ -289,11 +289,12 @@ public extension NextcloudKit { requestBody: String, showHiddenFiles: Bool, includeHiddenFiles: [String] = [], + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { if let httpBody = requestBody.data(using: .utf8) { - search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in + search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) } completion: { account, files, data, error in options.queue.async { completion(account, files, data, error) } @@ -306,17 +307,17 @@ public extension NextcloudKit { literal: String, showHiddenFiles: Bool, includeHiddenFiles: [String] = [], + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId guard let href = ("/files/" + userId).urlEncoded else { return options.queue.async { completion(account, [], nil, .urlError) } } let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(createProperties: options.createProperties, removeProperties: options.removeProperties), href, depth, "%" + literal + "%") if let httpBody = requestBody.data(using: .utf8) { - search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in + search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) } completion: { account, files, data, error in options.queue.async { completion(account, files, data, error) } @@ -331,10 +332,10 @@ public extension NextcloudKit { limit: Int, showHiddenFiles: Bool, includeHiddenFiles: [String] = [], + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase let files: [NKFile] = [] @@ -364,7 +365,7 @@ public extension NextcloudKit { } if let httpBody = requestBody.data(using: .utf8) { - search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, options: options) { task in + search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) } completion: { account, files, data, error in options.queue.async { completion(account, files, data, error) } @@ -376,10 +377,10 @@ public extension NextcloudKit { httpBody: Data, showHiddenFiles: Bool, includeHiddenFiles: [String], + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let user = self.nkCommonInstance.user let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase @@ -412,7 +413,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -423,10 +424,10 @@ public extension NextcloudKit { func setFavorite(fileName: String, favorite: Bool, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav @@ -466,10 +467,10 @@ public extension NextcloudKit { func listingFavorites(showHiddenFiles: Bool, includeHiddenFiles: [String] = [], + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let user = self.nkCommonInstance.user let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase @@ -504,7 +505,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -515,10 +516,10 @@ public extension NextcloudKit { func listingTrash(filename: String? = nil, showHiddenFiles: Bool, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ items: [NKTrash], _ data: Data?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 9cdc5cfd..351d98e7 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -197,12 +197,12 @@ open class NextcloudKit: SessionDelegate { public func download(serverUrlFileName: Any, fileNameLocalPath: String, + account: String, options: NKRequestOptions = NKRequestOptions(), requestHandler: @escaping (_ request: DownloadRequest) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, completionHandler: @escaping (_ account: String, _ etag: String?, _ date: Date?, _ lenght: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nKError: NKError) -> Void) { - let account = self.nkCommonInstance.account var convertible: URLConvertible? if serverUrlFileName is URL { convertible = serverUrlFileName as? URLConvertible @@ -263,12 +263,12 @@ open class NextcloudKit: SessionDelegate { fileNameLocalPath: String, dateCreationFile: Date? = nil, dateModificationFile: Date? = nil, + account: String, options: NKRequestOptions = NKRequestOptions(), requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, completionHandler: @escaping (_ account: String, _ ocId: String?, _ etag: String?, _ date: Date?, _ size: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nkError: NKError) -> Void) { - let account = self.nkCommonInstance.account var convertible: URLConvertible? var size: Int64 = 0 if serverUrlFileName is URL { @@ -352,6 +352,7 @@ open class NextcloudKit: SessionDelegate { chunkFolder: String, filesChunk: [(fileName: String, size: Int64)], chunkSize: Int, + account: String, options: NKRequestOptions = NKRequestOptions(), numChunks: @escaping (_ num: Int) -> Void = { _ in }, counterChunk: @escaping (_ counter: Int) -> Void = { _ in }, @@ -361,7 +362,6 @@ open class NextcloudKit: SessionDelegate { progressHandler: @escaping (_ totalBytesExpected: Int64, _ totalBytes: Int64, _ fractionCompleted: Double) -> Void = { _, _, _ in }, uploaded: @escaping (_ fileChunk: (fileName: String, size: Int64)) -> Void = { _ in }, completion: @escaping (_ account: String, _ filesChunk: [(fileName: String, size: Int64)]?, _ file: NKFile?, _ afError: AFError?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account let userId = self.nkCommonInstance.userId let urlBase = self.nkCommonInstance.urlBase let dav = self.nkCommonInstance.dav @@ -403,11 +403,11 @@ open class NextcloudKit: SessionDelegate { #endif func createFolder(completion: @escaping (_ errorCode: NKError) -> Void) { - readFileOrFolder(serverUrlFileName: serverUrlChunkFolder, depth: "0", options: options) { _, _, _, error in + readFileOrFolder(serverUrlFileName: serverUrlChunkFolder, depth: "0", account: account, options: options) { _, _, _, error in if error == .success { completion(NKError()) } else if error.errorCode == 404 { - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, options: options) { _, _, _, error in + NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, account: account, options: options) { _, _, _, error in completion(error) } } else { @@ -446,7 +446,7 @@ open class NextcloudKit: SessionDelegate { return completion(account, nil, nil, .explicitlyCancelled, error) } let semaphore = DispatchSemaphore(value: 0) - self.upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, options: options, requestHandler: { request in + self.upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: account, options: options, requestHandler: { request in requestHandler(request) }, taskHandler: { task in taskHandler(task) @@ -492,12 +492,12 @@ open class NextcloudKit: SessionDelegate { let assembleTimeMax: Double = 30 * 60 // 30 min options.timeout = max(assembleTimeMin, min(assembleTimePerGB * assembleSizeInGB, assembleTimeMax)) - self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, options: options) { _, error in + self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account, options: options) { _, error in guard error == .success else { return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) } - self.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", options: NKRequestOptions(queue: self.nkCommonInstance.backgroundQueue)) { _, files, _, error in + self.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account, options: NKRequestOptions(queue: self.nkCommonInstance.backgroundQueue)) { _, files, _, error in guard error == .success, let file = files.first else { return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index b52ce971..3ab08a12 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -36,6 +36,7 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, public func download(serverUrlFileName: Any, fileNameLocalPath: String, taskDescription: String? = nil, + account: String, session: URLSession) -> URLSessionDownloadTask? { var url: URL? if serverUrlFileName is URL { @@ -69,6 +70,7 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, dateCreationFile: Date?, dateModificationFile: Date?, taskDescription: String? = nil, + account: String, session: URLSession) -> URLSessionUploadTask? { var url: URL? if serverUrlFileName is URL { diff --git a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift index c7097363..00b57c56 100644 --- a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift @@ -32,7 +32,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: baseUrl) // Test creating folder - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName) { account, ocId, date, error in + NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in XCTAssertEqual(self.account, account) XCTAssertEqual(NKError.success.errorCode, error.errorCode) @@ -41,7 +41,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { Thread.sleep(forTimeInterval: 0.2) // Test reading folder, should exist - NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0") { account, files, data, error in + NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account) { account, files, data, error in XCTAssertEqual(self.account, account) XCTAssertEqual(NKError.success.errorCode, error.errorCode) XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) @@ -50,7 +50,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { Thread.sleep(forTimeInterval: 0.2) // Test deleting folder - NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName) { account, error in + NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: account) { account, error in XCTAssertEqual(self.account, account) XCTAssertEqual(NKError.success.errorCode, error.errorCode) XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) @@ -58,7 +58,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { Thread.sleep(forTimeInterval: 0.2) // Test reading folder, should NOT exist - NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0") { account, files, data, error in + NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account) { account, files, data, error in defer { expectation.fulfill() } XCTAssertEqual(404, error.errorCode) diff --git a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift index 7f39892a..1da28e7e 100644 --- a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift @@ -33,7 +33,7 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: baseUrl) - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName) { account, ocId, date, error in + NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in XCTAssertEqual(self.account, account) XCTAssertEqual(NKError.success.errorCode, error.errorCode) @@ -43,7 +43,7 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { let note = "Test note" - NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note) { account, share, data, error in + NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: account) { account, share, data, error in defer { expectation.fulfill() } XCTAssertEqual(self.account, account) From 83b75a17686ad446e7f720c28b542c5cabdda4c9 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 31 Jul 2024 13:58:25 +0200 Subject: [PATCH 051/135] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 115 ++---- Sources/NextcloudKit/NKModel.swift | 20 +- Sources/NextcloudKit/NKSession.swift | 114 ++++++ Sources/NextcloudKit/NextcloudKit+API.swift | 157 ++++---- .../NextcloudKit/NextcloudKit+Assistant.swift | 50 +-- .../NextcloudKit/NextcloudKit+Comments.swift | 67 ++-- .../NextcloudKit/NextcloudKit+Dashboard.swift | 36 +- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 96 +++-- .../NextcloudKit/NextcloudKit+FilesLock.swift | 7 +- .../NextcloudKit+Groupfolders.swift | 11 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 11 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 13 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 9 +- .../NextcloudKit/NextcloudKit+NCText.swift | 44 +-- .../NextcloudKit+PushNotification.swift | 28 +- .../NextcloudKit+Richdocuments.swift | 48 +-- .../NextcloudKit/NextcloudKit+Search.swift | 22 +- Sources/NextcloudKit/NextcloudKit+Share.swift | 50 +-- .../NextcloudKit+UserStatus.swift | 63 ++-- .../NextcloudKit/NextcloudKit+WebDAV.swift | 159 ++++---- Sources/NextcloudKit/NextcloudKit.swift | 136 +++---- .../NextcloudKit/NextcloudKitBackground.swift | 35 +- .../Utility/ThreadSafeArray.swift | 353 ++++++++++++++++++ .../FilesIntegrationTests.swift | 6 +- .../ShareIntegrationTests.swift | 5 +- .../LoginUnitTests.swift | 6 +- 26 files changed, 1054 insertions(+), 607 deletions(-) create mode 100644 Sources/NextcloudKit/NKSession.swift create mode 100644 Sources/NextcloudKit/Utility/ThreadSafeArray.swift diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 2b9795a8..66f299d6 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -30,7 +30,7 @@ import MobileCoreServices import CoreServices #endif -public protocol NKCommonDelegate { +public protocol NextcloudKitDelegate { func authenticationChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) @@ -40,15 +40,19 @@ public protocol NKCommonDelegate { func uploadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) func downloadingFinish(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) - + func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: Date?, dateLastModified: Date?, length: Int64, task: URLSessionTask, error: NKError) func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: Date?, size: Int64, task: URLSessionTask, error: NKError) } public class NKCommon: NSObject { public let dav: String = "remote.php/dav" - public let sessionIdentifierDownload: String = "com.nextcloud.nextcloudkit.session.download" - public let sessionIdentifierUpload: String = "com.nextcloud.nextcloudkit.session.upload" + public let identifierSessionDownload: String = "com.nextcloud.nextcloudkit.session.download" + public let identifierSessionUpload: String = "com.nextcloud.nextcloudkit.session.upload" + public let identifierSessionDownloadBackground: String = "com.nextcloud.session.download.background" + public let identifierSessionUploadBackground: String = "com.nextcloud.session.upload.background" + public let identifierSessionUploadBackgroundWWan: String = "com.nextcloud.session.upload.backgroundWWan" + public let identifierSessionUploadBackgroundExt: String = "com.nextcloud.session.upload.extension" public enum TypeReachability: Int { case unknown = 0 @@ -92,37 +96,11 @@ public class NKCommon: NSObject { var name: String } - public let notificationCenterChunkedFileStop = NSNotification.Name(rawValue: "NextcloudKit.chunkedFile.stop") - - internal lazy var sessionConfiguration: URLSessionConfiguration = { - let configuration = URLSessionConfiguration.af.default - configuration.requestCachePolicy = .reloadIgnoringLocalCacheData - if let groupIdentifier { - let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: groupIdentifier) - configuration.httpCookieStorage = cookieStorage - } else { - configuration.httpCookieStorage = nil - } - return configuration - }() - internal var rootQueue: DispatchQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.sessionManagerData.rootQueue") - internal var requestQueue: DispatchQueue? - internal var serializationQueue: DispatchQueue? - - internal var _user = "" - internal var _userId = "" - internal var _password = "" - internal var _account = "" - internal var _urlBase = "" - internal var _userAgent: String? - internal var _nextcloudVersion: Int = 0 - internal var _groupIdentifier: String? - - internal var internalTypeIdentifiers: [UTTypeConformsToServer] = [] internal var utiCache = NSCache() internal var mimeTypeCache = NSCache() internal var filePropertiesCache = NSCache() - internal var delegate: NKCommonDelegate? + internal var internalTypeIdentifiers: [UTTypeConformsToServer] = [] + internal var delegate: NextcloudKitDelegate? private var _filenameLog: String = "communication.log" private var _pathLog: String = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! @@ -132,39 +110,9 @@ public class NKCommon: NSObject { private var _copyLogToDocumentDirectory: Bool = false private let queueLog = DispatchQueue(label: "com.nextcloud.nextcloudkit.queuelog", attributes: .concurrent ) - public var user: String { - return _user - } - - public var userId: String { - return _userId - } - - public var password: String { - return _password - } - - public var account: String { - return _account - } - - public var urlBase: String { - return _urlBase - } - - public var userAgent: String? { - return _userAgent - } - - public var nextcloudVersion: Int { - return _nextcloudVersion - } - - public var groupIdentifier: String? { - return _groupIdentifier - } - public let backgroundQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.backgroundqueue", qos: .background, attributes: .concurrent) + public let notificationCenterChunkedFileStop = NSNotification.Name(rawValue: "NextcloudKit.chunkedFile.stop") + public var nksessions = ThreadSafeArray() public var filenameLog: String { get { @@ -486,41 +434,44 @@ public class NKCommon: NSObject { // MARK: - Common - public func getStandardHeaders(options: NKRequestOptions) -> HTTPHeaders { - return getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent, contentType: options.contentType) - } - - public func getStandardHeaders(_ appendHeaders: [String: String]? = nil, customUserAgent: String? = nil, contentType: String? = nil) -> HTTPHeaders { - return getStandardHeaders(user: user, password: password, appendHeaders: appendHeaders, customUserAgent: customUserAgent, contentType: contentType) + public func getSession(account: String) -> NKSession? { + var session: NKSession? + nksessions.forEach { result in + if result.account == account { + session = result + } + } + return session } - public func getStandardHeaders(user: String?, password: String?, appendHeaders: [String: String]?, customUserAgent: String?, contentType: String? = nil) -> HTTPHeaders { + public func getStandardHeaders(account: String, options: NKRequestOptions? = nil) -> HTTPHeaders? { + guard let session = nksessions.filter({ $0.account == account }).first else { return nil} var headers: HTTPHeaders = [] - if let user, let password { - headers.update(.authorization(username: user, password: password)) - } - if let customUserAgent { + headers.update(.authorization(username: session.user, password: session.password)) + headers.update(.userAgent(session.userAgent)) + if let customUserAgent = options?.customUserAgent { headers.update(.userAgent(customUserAgent)) - } else if let userAgent = userAgent { - headers.update(.userAgent(userAgent)) } - if let contentType { + if let contentType = options?.contentType { headers.update(.contentType(contentType)) } else { headers.update(.contentType("application/x-www-form-urlencoded")) } - if contentType != "application/xml" { + if options?.contentType != "application/xml" { headers.update(name: "Accept", value: "application/json") } headers.update(name: "OCS-APIRequest", value: "true") - for (key, value) in appendHeaders ?? [:] { + for (key, value) in options?.customHeader ?? [:] { headers.update(name: key, value: value) } return headers } - public func createStandardUrl(serverUrl: String, endpoint: String) -> URLConvertible? { + public func createStandardUrl(serverUrl: String, endpoint: String, options: NKRequestOptions) -> URLConvertible? { + if let endpoint = options.endpoint { + return URL(string: endpoint) + } guard var serverUrl = serverUrl.urlEncoded else { return nil } if serverUrl.last != "/" { serverUrl = serverUrl + "/" } @@ -586,7 +537,7 @@ public class NKCommon: NSObject { return 0 } - public func returnPathfromServerUrl(_ serverUrl: String) -> String { + public func returnPathfromServerUrl(_ serverUrl: String, urlBase: String, userId: String) -> String { let home = urlBase + "/remote.php/dav/files/" + userId return serverUrl.replacingOccurrences(of: home, with: "") } diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index c1f4168c..be8fd7ef 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -607,10 +607,10 @@ class NKDataFileXML: NSObject { return xml["ocs", "data", "apppassword"].text } - func convertDataFile(xmlData: Data, dav: String, urlBase: String, user: String, userId: String, account: String, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] { + func convertDataFile(xmlData: Data, nkSession: NKSession, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] { var files: [NKFile] = [] - let rootFiles = "/" + dav + "/files/" - guard let baseUrl = self.nkCommonInstance.getHostName(urlString: urlBase) else { + let rootFiles = "/" + nkSession.dav + "/files/" + guard let baseUrl = self.nkCommonInstance.getHostName(urlString: nkSession.urlBase) else { return files } let xml = XML.parse(xmlData) @@ -641,7 +641,7 @@ class NKDataFileXML: NSObject { } // account - file.account = account + file.account = nkSession.account // path file.path = (fileNamePath as NSString).deletingLastPathComponent + "/" @@ -652,7 +652,7 @@ class NKDataFileXML: NSObject { file.fileName = file.fileName.removingPercentEncoding ?? "" // ServerUrl - if href == rootFiles + user + "/" { + if href == rootFiles + nkSession.user + "/" { file.fileName = "." file.serverUrl = ".." } else { @@ -864,9 +864,9 @@ class NKDataFileXML: NSObject { file.iconName = results.iconName file.name = "files" file.classFile = results.classFile - file.urlBase = urlBase - file.user = user - file.userId = userId + file.urlBase = nkSession.urlBase + file.user = nkSession.user + file.userId = nkSession.userId files.append(file) } @@ -891,10 +891,10 @@ class NKDataFileXML: NSObject { return files } - func convertDataTrash(xmlData: Data, urlBase: String, showHiddenFiles: Bool) -> [NKTrash] { + func convertDataTrash(xmlData: Data, nkSession: NKSession, showHiddenFiles: Bool) -> [NKTrash] { var files: [NKTrash] = [] var first: Bool = true - guard let baseUrl = self.nkCommonInstance.getHostName(urlString: urlBase) else { + guard let baseUrl = self.nkCommonInstance.getHostName(urlString: nkSession.urlBase) else { return files } let xml = XML.parse(xmlData) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift new file mode 100644 index 00000000..5401966c --- /dev/null +++ b/Sources/NextcloudKit/NKSession.swift @@ -0,0 +1,114 @@ +// +// NKSession.swift +// +// +// Created by Marino Faggiana on 20/07/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation +import Alamofire + +public class NKSession { + public var urlBase: String + public var user: String + public var userId: String + public var password: String + public let account: String + public var userAgent: String + public var nextcloudVersion: Int + public let groupIdentifier: String? + public let dav: String = "remote.php/dav" + public var internalTypeIdentifiers: [NKCommon.UTTypeConformsToServer] = [] + public let sessionData: Alamofire.Session + public let sessionDownloadBackground: URLSession + public let sessionUploadBackground: URLSession + public let sessionUploadBackgroundWWan: URLSession + public let sessionUploadBackgroundExt: URLSession + + init(urlBase: String, + user: String, + userId: String, + password: String, + account: String, + userAgent: String, + nextcloudVersion: Int, + groupIdentifier: String? = nil) { + self.urlBase = urlBase + self.user = user + self.userId = userId + self.password = password + self.account = account + self.userAgent = userAgent + self.nextcloudVersion = nextcloudVersion + self.groupIdentifier = groupIdentifier + + let backgroundSessionDelegate = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance) + /// Strange but works ?!?! + let sharedCookieStorage = UUID().uuidString + "_" + user + "@" + urlBase + + /// Session Alamofire + let configuration = URLSessionConfiguration.default + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + configuration.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionData = Alamofire.Session(configuration: configuration, + // delegate: NextcloudKit.shared, + eventMonitors: [AlamofireLogger(nkCommonInstance: NextcloudKit.shared.nkCommonInstance)]) + + /// Session Download Background + let configurationDownloadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionDownloadBackground) + configurationDownloadBackground.allowsCellularAccess = true + configurationDownloadBackground.sessionSendsLaunchEvents = true + configurationDownloadBackground.isDiscretionary = false + configurationDownloadBackground.httpMaximumConnectionsPerHost = 5 + configurationDownloadBackground.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationDownloadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionDownloadBackground = URLSession(configuration: configurationDownloadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) + + /// Session Upload Background + let configurationUploadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackground) + configurationUploadBackground.allowsCellularAccess = true + configurationUploadBackground.sessionSendsLaunchEvents = true + configurationUploadBackground.isDiscretionary = false + configurationUploadBackground.httpMaximumConnectionsPerHost = 5 + configurationUploadBackground.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationUploadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionUploadBackground = URLSession(configuration: configurationUploadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) + + /// Session Upload Background WWan + let configurationUploadBackgroundWWan = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackgroundWWan) + configurationUploadBackgroundWWan.allowsCellularAccess = false + configurationUploadBackgroundWWan.sessionSendsLaunchEvents = true + configurationUploadBackgroundWWan.isDiscretionary = false + configurationUploadBackgroundWWan.httpMaximumConnectionsPerHost = 5 + configurationUploadBackgroundWWan.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationUploadBackgroundWWan.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionUploadBackgroundWWan = URLSession(configuration: configurationUploadBackgroundWWan, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) + + /// Session Upload Background Extension + let configurationUploadBackgroundExt = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackgroundExt) + configurationUploadBackgroundExt.allowsCellularAccess = true + configurationUploadBackgroundExt.sessionSendsLaunchEvents = true + configurationUploadBackgroundExt.isDiscretionary = false + configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = 5 + configurationUploadBackgroundExt.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationUploadBackgroundExt.sharedContainerIdentifier = groupIdentifier + configurationUploadBackgroundExt.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionUploadBackgroundExt = URLSession(configuration: configurationUploadBackgroundExt, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) + } +} diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index f7f3382e..886d922c 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -77,19 +77,19 @@ public extension NextcloudKit { // MARK: - func generalWithEndpoint(_ endpoint: String, - method: String, account: String, + method: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: method.uppercased()) - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -112,15 +112,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var externalSites: [NKExternalSite] = [] let endpoint = "ocs/v2.php/apps/external/api/v1" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, externalSites, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -174,12 +174,15 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (ServerInfoResult) -> Void) { let endpoint = "status.php" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { return options.queue.async { completion(ServerInfoResult.failure(.urlError)) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) + var headers: HTTPHeaders? + if let userAgent = options.customUserAgent { + headers = [HTTPHeader.userAgent(userAgent)] + } - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + AF.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -228,13 +231,16 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - var headers = self.nkCommonInstance.getStandardHeaders(options: options) + guard let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, .urlError) } + } if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -268,19 +274,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - guard let urlRequest = url else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } - var headers = self.nkCommonInstance.getStandardHeaders(options: options) + if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -358,26 +364,22 @@ public extension NextcloudKit { compressionQualityPreview: CGFloat, compressionQualityIcon: CGFloat, etag: String? = nil, - endpoint: String?, + endpoint: String, account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var url: URLConvertible? - if let endpoint { - url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - } - guard let urlRequest = url else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } - var headers = self.nkCommonInstance.getStandardHeaders(options: options) if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -423,18 +425,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/avatar/\(user)/\(sizeImage)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - var headers = self.nkCommonInstance.getStandardHeaders(options: options) + if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -510,12 +513,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - guard let url = serverUrl.asUrl else { + guard let url = serverUrl.asUrl, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -542,14 +546,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/cloud/user" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -611,14 +615,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v1.php/cloud/capabilities" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -649,12 +653,16 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ wipe: Bool, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "index.php/core/wipe/check" let parameters: [String: Any] = ["token": token] - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + /// + options.contentType = "application/json" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, false, nil, .urlError) } } - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -681,12 +689,16 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "index.php/core/wipe/success" let parameters: [String: Any] = ["token": token] - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + /// + options.contentType = "application/json" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -714,7 +726,6 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var activities: [NKActivity] = [] var activityFirstKnown = 0 var activityLastGiven = 0 @@ -737,12 +748,13 @@ public extension NextcloudKit { parameters["previews"] = "true" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -811,15 +823,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications" - var notifications: [NKNotifications] = [] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -834,6 +845,7 @@ public extension NextcloudKit { let json = JSON(jsonData) if json["ocs"]["meta"]["statuscode"].int == 200 { let ocsdata = json["ocs"]["data"] + var notifications: [NKNotifications] = [] for (_, subJson): (String, JSON) in ocsdata { let notification = NKNotifications() @@ -885,11 +897,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } var url: URLConvertible? if serverUrl == nil { let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications/\(idNotification)" - url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + url = self.nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options) } else { url = serverUrl?.asUrl } @@ -897,9 +912,8 @@ public extension NextcloudKit { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: method) - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -923,18 +937,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/dav/api/v1/direct" let parameters: [String: Any] = [ "fileId": fileId, "format": "json" ] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -961,12 +975,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/security_guard/diagnostics" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + /// + options.contentType = "application/json" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: .put, headers: headers) @@ -975,7 +992,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 15e75380..6b02e271 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -30,14 +30,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ types: [NKTextProcessingTaskType]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/textprocessing/tasktypes" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -70,15 +70,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/schedule" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -108,14 +108,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -145,14 +145,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -182,14 +182,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: [NKTextProcessingTask]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/tasks/app/\(appId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 61bd2174..3667b298 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -30,14 +30,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ items: [NKComments]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPFIND") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -47,7 +51,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -76,13 +80,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" + /// + options.contentType = "application/json" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") var urlRequest: URLRequest do { @@ -93,7 +101,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -118,14 +126,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)/\(messageId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -136,7 +148,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -160,15 +172,16 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)/\(messageId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -191,14 +204,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -209,7 +226,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 4c4651eb..4c4efe1a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -31,20 +31,14 @@ public extension NextcloudKit { request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardWidgets: [NCCDashboardWidget]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var url: URLConvertible? - if let endpoint = options.endpoint { - url = URL(string: endpoint) - } else { - let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widgets" - url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widgets" + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - guard let url = url else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -76,20 +70,14 @@ public extension NextcloudKit { request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardApplications: [NCCDashboardApplication]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var url: URLConvertible? - if let endpoint = options.endpoint { - url = URL(string: endpoint) - } else { - let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widget-items?widgets[]=\(items)" - url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - } - guard let url = url else { - return options.queue.async { completion(account, nil, nil, .urlError) } + let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widget-items?widgets[]=\(items)" + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index be39affc..2dee0358 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -32,19 +32,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/encrypted/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } let method: HTTPMethod = delete ? .delete : .put - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -75,17 +75,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eToken: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/lock/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: method) - var headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] if let e2eToken { @@ -96,7 +96,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-COUNTER", value: e2eCounter) } - sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -126,23 +126,22 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eMetadata: String?, _ signature: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] - if let e2eToken { parameters["e2e-token"] = e2eToken } - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -176,22 +175,20 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ metadata: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: method) - var headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] - parameters["e2e-token"] = e2eToken headers.update(name: "e2e-token", value: e2eToken) - if let e2eMetadata { parameters["metaData"] = e2eMetadata } @@ -199,7 +196,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-SIGNATURE", value: signature) } - sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -230,8 +227,7 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ certificateUser: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let userId = self.nkCommonInstance.userId + var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -245,12 +241,13 @@ public extension NextcloudKit { } else { endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -265,7 +262,7 @@ public extension NextcloudKit { let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { - let key = json["ocs"]["data"]["public-keys"][userId].stringValue + let key = json["ocs"]["data"]["public-keys"][nkSession.userId].stringValue if let user = user { let keyUser = json["ocs"]["data"]["public-keys"][user].string options.queue.async { completion(account, key, keyUser, jsonData, .success) } @@ -283,18 +280,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -322,18 +319,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/server-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -362,19 +359,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = ["csr": certificate] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -404,19 +401,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = ["privateKey": privateKey] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -444,18 +441,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -476,18 +472,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index 851915df..2cdfa92a 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -38,10 +38,13 @@ public extension NextcloudKit { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: shouldLock ? "LOCK" : "UNLOCK") - var headers = self.nkCommonInstance.getStandardHeaders(options: options) + guard let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } headers.update(name: "X-User-Lock", value: "1") - sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 5c2b1e85..3c54f4bb 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -30,15 +30,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ results: [NKGroupfolders]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/apps/groupfolders/folders?applicable=1" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index e1964925..93331745 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -32,15 +32,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ result: NKHovercard?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/hovercard/v1/\(userId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 8259d105..091047d5 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -31,13 +31,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlfileNamePath.encodedToUrl else { + guard let url = serverUrlfileNamePath.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account) else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") + /// + options.contentType = "application/xml" + /// + guard let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } var urlRequest: URLRequest - do { try urlRequest = URLRequest(url: url, method: method, headers: headers) let parameters = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyLivephoto, livePhotoFile) @@ -46,7 +51,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 1bb056b1..207dc1fb 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -35,7 +35,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/getapppassword" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint, options: options) else { return options.queue.async { completion(nil, nil, .urlError) } } var headers: HTTPHeaders = [.authorization(username: user, password: password)] @@ -82,7 +82,8 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/apppassword" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { return options.queue.async { completion(nil, .urlError) } } var headers: HTTPHeaders = [.authorization(username: username, password: password)] @@ -98,7 +99,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -122,7 +123,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ endpoint: String?, _ login: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "index.php/login/v2" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { return options.queue.async { completion(nil, nil, nil, nil, .urlError) } } var headers: HTTPHeaders? diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index f6d7cfc2..a23ab7c1 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -26,20 +26,20 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func NCTextObtainEditorDetails(account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase + func NCTextObtainEditorDetails(account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing" var editors: [NKEditorDetailsEditors] = [] var creators: [NKEditorDetailsCreators] = [] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, editors, creators, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -96,7 +96,6 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -104,12 +103,13 @@ public extension NextcloudKit { if let fileId = fileId { endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/open?path=/\(fileNamePath)&fileId=\(fileId)&editorId=\(editor)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -131,16 +131,16 @@ public extension NextcloudKit { func NCTextGetListOfTemplates(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ templates: [NKEditorTemplates], _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase + completion: @escaping (_ account: String, _ templates: [NKEditorTemplates]?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/templates/text/textdocumenttemplate" var templates: [NKEditorTemplates] = [] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, templates, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -178,7 +178,6 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -188,12 +187,13 @@ public extension NextcloudKit { } else { endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/create?path=/\(fileNamePath)&editorId=\(editorId)&creatorId=\(creatorId)&templateId=\(templateId)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index 656704ca..2c1cc184 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -27,8 +27,6 @@ import SwiftyJSON public extension NextcloudKit { func subscribingPushNotification(serverUrl: String, - user: String, - password: String, pushTokenHash: String, devicePublicKey: String, proxyServerUrl: String, @@ -37,7 +35,9 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } let parameters = [ @@ -45,9 +45,8 @@ public extension NextcloudKit { "devicePublicKey": devicePublicKey, "proxyServer": proxyServerUrl ] - let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -74,19 +73,18 @@ public extension NextcloudKit { } func unsubscribingPushNotification(serverUrl: String, - user: String, - password: String, account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -113,7 +111,8 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "devices?format=json" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint, options: options), let userAgent = options.customUserAgent else { return options.queue.async { completion(account, .urlError) } } @@ -125,7 +124,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -151,7 +150,8 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "devices" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint, options: options), let userAgent = options.customUserAgent else { return options.queue.async { completion(account, .urlError) } } @@ -162,7 +162,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - sessionManager.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index a4a55f4d..5cb9d6e9 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -26,20 +26,20 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func createUrlRichdocuments(fileID: String, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase + func createUrlRichdocuments(fileID: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/document" - let parameters: [String: Any] = ["fileId": fileID] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) + let parameters: [String: Any] = ["fileId": fileID] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -67,14 +67,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ templates: [NKRichdocumentsTemplate]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/\(typeTemplate)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -115,15 +115,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/new" - let parameters: [String: Any] = ["path": path, "template": templateId] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) + let parameters: [String: Any] = ["path": path, "template": templateId] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -151,15 +151,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/apps/richdocuments/assets" - let parameters: [String: Any] = ["path": path] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) + let parameters: [String: Any] = ["path": path] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index e501727f..d89f8a3d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -52,14 +52,14 @@ public extension NextcloudKit { providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, update: @escaping (_ account: String, _ searchResult: NKSearchResult?, _ provider: NKSearchProvider, _ error: NKError) -> Void, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/search/providers" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return completion(account, nil, .urlError) + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let requestUnifiedSearch = sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestUnifiedSearch = nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -122,8 +122,9 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, NKSearchResult?, _ data: Data?, _ error: NKError) -> Void) -> DataRequest? { - let urlBase = self.nkCommonInstance.urlBase - guard let term = term.urlEncoded else { + guard let term = term.urlEncoded, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { completion(account, nil, nil, .urlError) return nil } @@ -134,14 +135,11 @@ public extension NextcloudKit { if let cursor = cursor { endpoint += "&cursor=\(cursor)" } - guard let url = self.nkCommonInstance.createStandardUrl( - serverUrl: urlBase, - endpoint: endpoint) + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options) else { completion(account, nil, nil, .urlError) return nil } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var urlRequest: URLRequest do { @@ -152,7 +150,7 @@ public extension NextcloudKit { return nil } - let requestSearchProvider = sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestSearchProvider = nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 70186482..961ae284 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -80,14 +80,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ shares: [NKShare]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: parameters.endpoint) - else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: parameters.endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -132,16 +131,16 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ sharees: [NKSharee]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/sharees" var lookupString = "false" if lookup { lookupString = "true" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = [ "search": search, "page": String(page), @@ -150,7 +149,7 @@ public extension NextcloudKit { "lookup": lookupString ] - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -290,12 +289,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "path": path, "shareType": String(shareType), @@ -320,7 +319,7 @@ public extension NextcloudKit { parameters["attributes"] = attributes } - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -376,12 +375,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "permissions": String(permissions) ] @@ -405,7 +404,7 @@ public extension NextcloudKit { parameters["attributes"] = "[]" } - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -429,21 +428,21 @@ public extension NextcloudKit { } /* - * @param idShare Identifier of the share to update + * @param idShare: Identifier of the share to update */ func deleteShare(idShare: Int, account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -465,6 +464,7 @@ public extension NextcloudKit { private func convertResponseShare(json: JSON, account: String) -> NKShare { let share = NKShare() + share.account = account share.canDelete = json["can_delete"].boolValue share.canEdit = json["can_edit"].boolValue share.displaynameFileOwner = json["displayname_file_owner"].stringValue diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index ce488f90..cb18b7c0 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -31,17 +31,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ clearAt: Date?, _ icon: String?, _ message: String?, _ messageId: String?, _ messageIsPredefined: Bool, _ status: String?, _ statusIsUserDefined: Bool, _ userId: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status" if let userId = userId { endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/\(userId)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -82,17 +82,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/status" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = [ "statusType": String(status) ] - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -121,12 +121,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/predefined" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "messageId": String(messageId) ] @@ -134,7 +134,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -165,12 +165,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/custom" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "message": String(message) ] @@ -181,7 +181,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -209,14 +209,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -244,15 +244,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var userStatuses: [NKUserStatus] = [] let endpoint = "ocs/v2.php/apps/user_status/api/v1/predefined_statuses" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -264,11 +263,11 @@ public extension NextcloudKit { let error = NKError(error: error, afResponse: response, responseData: response.data) options.queue.async { completion(account, nil, nil, error) } case .success(let jsonData): + var userStatuses: [NKUserStatus] = [] let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { let ocsdata = json["ocs"]["data"] - for (_, subJson): (String, JSON) in ocsdata { let userStatus = NKUserStatus() @@ -298,19 +297,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var userStatuses: [NKUserStatus] = [] let endpoint = "ocs/v2.php/apps/user_status/api/v1/statuses" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = [ "limit": String(limit), "offset": String(offset) ] - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -324,12 +322,11 @@ public extension NextcloudKit { case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError + var userStatuses: [NKUserStatus] = [] if statusCode == 200 { let ocsdata = json["ocs"]["data"] - for (_, subJson): (String, JSON) in ocsdata { let userStatus = NKUserStatus() - if let value = subJson["clearAt"].double { if value > 0 { userStatus.clearAt = Date(timeIntervalSince1970: value) diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 61d79eb4..81ad2d47 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -31,13 +31,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ error: NKError) -> Void) { - guard let url = serverUrlFileName.encodedToUrl else { + guard let url = serverUrlFileName.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: "MKCOL") - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var urlRequest: URLRequest - do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -45,7 +45,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -76,12 +76,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlFileName.encodedToUrl else { + guard let url = serverUrlFileName.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var urlRequest: URLRequest - do { try urlRequest = URLRequest(url: url, method: .delete, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -89,7 +89,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -113,11 +113,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlFileNameSource.encodedToUrl else { + guard let url = serverUrlFileNameSource.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "MOVE") - var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") if overwrite { headers.update(name: "Overwrite", value: "T") @@ -125,7 +126,6 @@ public extension NextcloudKit { headers.update(name: "Overwrite", value: "F") } var urlRequest: URLRequest - do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -133,7 +133,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -157,11 +157,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlFileNameSource.encodedToUrl else { + guard let url = serverUrlFileNameSource.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "COPY") - var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") if overwrite { headers.update(name: "Overwrite", value: "T") @@ -177,7 +178,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -202,15 +203,16 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let user = self.nkCommonInstance.user - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { var files: [NKFile] = [] var serverUrlFileName = serverUrlFileName - guard let url = serverUrlFileName.encodedToUrl else { - return options.queue.async { completion(account, files, nil, .urlError) } + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = serverUrlFileName.encodedToUrl, + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } if depth == "0", serverUrlFileName.last == "/" { serverUrlFileName = String(serverUrlFileName.dropLast()) @@ -218,7 +220,6 @@ public extension NextcloudKit { serverUrlFileName = serverUrlFileName + "/" } let method = HTTPMethod(rawValue: "PROPFIND") - var headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") headers.update(name: "Depth", value: depth) var urlRequest: URLRequest @@ -234,7 +235,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -247,7 +248,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -262,26 +263,27 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ file: NKFile?, _ data: Data?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase + guard let nkSession = nkCommonInstance.getSession(account: account) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } var httpBody: Data? if let fileId = fileId { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), nkSession.userId, fileId).data(using: .utf8) } else if let link = link { let linkArray = link.components(separatedBy: "/") if let fileId = linkArray.last { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), nkSession.userId, fileId).data(using: .utf8) } } guard let httpBody = httpBody else { return options.queue.async { completion(account, nil, nil, .urlError) } } - search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], account: account, options: options) { task in + search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], account: account, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) } completion: { account, files, data, error in - options.queue.async { completion(account, files.first, data, error) } + options.queue.async { completion(account, files?.first, data, error) } } } @@ -292,7 +294,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { if let httpBody = requestBody.data(using: .utf8) { search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) @@ -310,10 +312,10 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - guard let href = ("/files/" + userId).urlEncoded else { - return options.queue.async { completion(account, [], nil, .urlError) } + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + guard let nkSession = nkCommonInstance.getSession(account: account), + let href = ("/files/" + nkSession.userId).urlEncoded else { + return options.queue.async { completion(account, nil, nil, .urlError) } } let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(createProperties: options.createProperties, removeProperties: options.removeProperties), href, depth, "%" + literal + "%") if let httpBody = requestBody.data(using: .utf8) { @@ -335,12 +337,13 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + guard let nkSession = nkCommonInstance.getSession(account: account) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } let files: [NKFile] = [] var greaterDateString: String?, lessDateString: String? - let href = "/files/" + userId + path + let href = "/files/" + nkSession.userId + path if let lessDate = lessDate as? Date { lessDateString = self.nkCommonInstance.convertDate(lessDate, format: "yyyy-MM-dd'T'HH:mm:ssZZZZZ") } else if let lessDate = lessDate as? Int { @@ -365,7 +368,7 @@ public extension NextcloudKit { } if let httpBody = requestBody.data(using: .utf8) { - search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in + search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) } completion: { account, files, data, error in options.queue.async { completion(account, files, data, error) } @@ -380,17 +383,19 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let user = self.nkCommonInstance.user - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } var files: [NKFile] = [] - guard let url = (serverUrl + "/" + dav).encodedToUrl else { + guard let url = (serverUrl + "/" + nkSession.dav).encodedToUrl else { return options.queue.async { completion(account, files, nil, .urlError) } } let method = HTTPMethod(rawValue: "SEARCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: method, headers: headers) @@ -400,7 +405,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -413,7 +418,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -428,15 +433,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + "/" + fileName + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + "/" + fileName guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -448,7 +456,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -470,18 +478,20 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let user = self.nkCommonInstance.user - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId var files: [NKFile] = [] guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, files, nil, .urlError) } } let method = HTTPMethod(rawValue: "REPORT") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -492,7 +502,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -505,7 +515,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -519,11 +529,15 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ items: [NKTrash], _ data: Data?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - var serverUrlFileName = urlBase + "/" + dav + "/trashbin/" + userId + "/trash/" + completion: @escaping (_ account: String, _ items: [NKTrash]?, _ data: Data?, _ error: NKError) -> Void) { + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + var serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/trashbin/" + nkSession.userId + "/trash/" if let filename { serverUrlFileName = serverUrlFileName + filename } @@ -532,7 +546,6 @@ public extension NextcloudKit { return options.queue.async { completion(account, items, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPFIND") - var headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") headers.update(name: "Depth", value: "1") var urlRequest: URLRequest @@ -544,7 +557,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, items, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -557,7 +570,7 @@ public extension NextcloudKit { options.queue.async { completion(account, items, nil, error) } case .success: if let xmlData = response.data { - items = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataTrash(xmlData: xmlData, urlBase: urlBase, showHiddenFiles: showHiddenFiles) + items = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataTrash(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles) options.queue.async { completion(account, items, xmlData, .success) } } else { options.queue.async { completion(account, items, nil, .xmlError) } diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 351d98e7..4213e163 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -34,22 +34,6 @@ open class NextcloudKit: SessionDelegate { let instance = NextcloudKit() return instance }() - internal lazy var internalSessionManager: Alamofire.Session = { - return Alamofire.Session(configuration: nkCommonInstance.sessionConfiguration, - delegate: self, - rootQueue: nkCommonInstance.rootQueue, - startRequestsImmediately: true, - requestQueue: nkCommonInstance.requestQueue, - serializationQueue: nkCommonInstance.serializationQueue, - interceptor: nil, - serverTrustManager: nil, - redirectHandler: nil, - cachedResponseHandler: nil, - eventMonitors: [AlamofireLogger(nkCommonInstance: self.nkCommonInstance)]) - }() - public var sessionManager: Alamofire.Session { - return internalSessionManager - } #if !os(watchOS) private let reachabilityManager = Alamofire.NetworkReachabilityManager() #endif @@ -71,50 +55,58 @@ open class NextcloudKit: SessionDelegate { // MARK: - Setup - public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, userAgent: String, nextcloudVersion: Int, groupIdentifier: String? = nil, delegate: NKCommonDelegate?) { - self.setup(account: account, user: user, userId: userId, password: password, urlBase: urlBase, groupIdentifier: groupIdentifier) - self.setup(userAgent: userAgent) - self.setup(nextcloudVersion: nextcloudVersion) - self.setup(delegate: delegate) + public func setup(delegate: NextcloudKitDelegate?) { + self.nkCommonInstance.delegate = delegate } - public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, groupIdentifier: String? = nil) { - self.nkCommonInstance._groupIdentifier = groupIdentifier - if (self.nkCommonInstance.account != account) || (self.nkCommonInstance.urlBase != urlBase && self.nkCommonInstance.user != user) { - if let cookieStore = sessionManager.session.configuration.httpCookieStorage { - for cookie in cookieStore.cookies ?? [] { - cookieStore.deleteCookie(cookie) - } - } - self.nkCommonInstance.internalTypeIdentifiers = [] + public func appendAccount(_ account: String, + urlBase: String, + user: String, + userId: String, + password: String, + userAgent: String, + nextcloudVersion: Int, + groupIdentifier: String) { + if nkCommonInstance.nksessions.filter({ $0.account == account }).first != nil { + return updateAccount(account, urlBase: urlBase, userId: userId, password: password, userAgent: userAgent, nextcloudVersion: nextcloudVersion) } + let nkSession = NKSession(urlBase: urlBase, user: user, userId: userId, password: password, account: account, userAgent: userAgent, nextcloudVersion: nextcloudVersion, groupIdentifier: groupIdentifier) - if let account { - self.nkCommonInstance._account = account - } else { - self.nkCommonInstance._account = "" - } - self.nkCommonInstance._user = user - self.nkCommonInstance._userId = userId - self.nkCommonInstance._password = password - self.nkCommonInstance._urlBase = urlBase + nkCommonInstance.nksessions.append(nkSession) } - public func setup(delegate: NKCommonDelegate?) { - self.nkCommonInstance.delegate = delegate - } - - public func setup(userAgent: String) { - self.nkCommonInstance._userAgent = userAgent + public func removeAccount(_ account: String) { + if let index = nkCommonInstance.nksessions.index(where: { $0.account == account}) { + nkCommonInstance.nksessions.remove(at: index) + } } - public func setup(nextcloudVersion: Int) { - self.nkCommonInstance._nextcloudVersion = nextcloudVersion + public func updateAccount(_ account: String, + urlBase: String? = nil, + userId: String? = nil, + password: String? = nil, + userAgent: String? = nil, + nextcloudVersion: Int? = nil) { + guard let session = nkCommonInstance.nksessions.filter({ $0.account == account }).first else { return } + if let urlBase { + session.urlBase = urlBase + } + if let userId { + session.userId = userId + } + if let password { + session.password = password + } + if let userAgent { + session.userAgent = userAgent + } + if let nextcloudVersion { + session.nextcloudVersion = nextcloudVersion + } } /* internal func saveCookies(response : HTTPURLResponse?) { - if let headerFields = response?.allHeaderFields as? [String : String] { if let url = URL(string: self.nkCommonInstance.urlBase) { let HTTPCookie = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url) @@ -128,7 +120,6 @@ open class NextcloudKit: SessionDelegate { } internal func injectsCookies() { - if let cookies = cookies[self.nkCommonInstance.account] { if let url = URL(string: self.nkCommonInstance.urlBase) { sessionManager.session.configuration.httpCookieStorage?.setCookies(cookies, for: url, mainDocumentURL: nil) @@ -145,6 +136,7 @@ open class NextcloudKit: SessionDelegate { } private func startNetworkReachabilityObserver() { + /* reachabilityManager?.startListening(onUpdatePerforming: { status in switch status { case .unknown: @@ -157,6 +149,7 @@ open class NextcloudKit: SessionDelegate { self.nkCommonInstance.delegate?.networkReachabilityObserver(NKCommon.TypeReachability.reachableCellular) } }) + */ } private func stopNetworkReachabilityObserver() { @@ -164,15 +157,7 @@ open class NextcloudKit: SessionDelegate { } #endif - // MARK: - Session utility - - public func getSessionManager() -> URLSession { - return sessionManager.session - } - /* - //MARK: - - private func makeEvents() -> [EventMonitor] { let events = ClosureEventMonitor() @@ -209,9 +194,10 @@ open class NextcloudKit: SessionDelegate { } else if serverUrlFileName is String || serverUrlFileName is NSString { convertible = (serverUrlFileName as? String)?.encodedToUrl } - guard let url = convertible else { - options.queue.async { completionHandler(account, nil, nil, 0, nil, nil, .urlError) } - return + guard let url = convertible, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completionHandler(account, nil, nil, 0, nil, nil, .urlError) } } var destination: Alamofire.DownloadRequest.Destination? let fileNamePathLocalDestinationURL = NSURL.fileURL(withPath: fileNameLocalPath) @@ -219,9 +205,8 @@ open class NextcloudKit: SessionDelegate { return (fileNamePathLocalDestinationURL, [.removePreviousFile, .createIntermediateDirectories]) } destination = destinationFile - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let request = sessionManager.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let request = nkSession.sessionData.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } } .downloadProgress { progress in @@ -276,12 +261,12 @@ open class NextcloudKit: SessionDelegate { } else if serverUrlFileName is String || serverUrlFileName is NSString { convertible = (serverUrlFileName as? String)?.encodedToUrl } - guard let url = convertible else { - options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, nil, .urlError) } - return + guard let url = convertible, + let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, nil, .urlError) } } let fileNameLocalPathUrl = URL(fileURLWithPath: fileNameLocalPath) - var headers = self.nkCommonInstance.getStandardHeaders(options: options) // Epoch of linux do not permitted negativ value if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { headers.update(name: "X-OC-CTime", value: "\(dateCreationFile.timeIntervalSince1970)") @@ -291,7 +276,7 @@ open class NextcloudKit: SessionDelegate { headers.update(name: "X-OC-MTime", value: "\(dateModificationFile.timeIntervalSince1970)") } - let request = sessionManager.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in + let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } }) .uploadProgress { progress in @@ -362,12 +347,13 @@ open class NextcloudKit: SessionDelegate { progressHandler: @escaping (_ totalBytesExpected: Int64, _ totalBytes: Int64, _ fractionCompleted: Double) -> Void = { _, _, _ in }, uploaded: @escaping (_ fileChunk: (fileName: String, size: Int64)) -> Void = { _ in }, completion: @escaping (_ account: String, _ filesChunk: [(fileName: String, size: Int64)]?, _ file: NKFile?, _ afError: AFError?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav + + guard let nkSession = nkCommonInstance.getSession(account: account) else { + return completion(account, nil, nil, nil, .urlError) + } let fileNameLocalSize = self.nkCommonInstance.getFileSize(filePath: directory + "/" + fileName) - let serverUrlChunkFolder = urlBase + "/" + dav + "/uploads/" + userId + "/" + chunkFolder - let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl) + "/" + fileName + let serverUrlChunkFolder = nkSession.urlBase + "/" + nkSession.dav + "/uploads/" + nkSession.userId + "/" + chunkFolder + let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl, urlBase: nkSession.urlBase, userId: nkSession.userId) + "/" + fileName if options.customHeader == nil { options.customHeader = [:] } @@ -492,14 +478,12 @@ open class NextcloudKit: SessionDelegate { let assembleTimeMax: Double = 30 * 60 // 30 min options.timeout = max(assembleTimeMin, min(assembleTimePerGB * assembleSizeInGB, assembleTimeMax)) - self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account, options: options) { _, error in + self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account,options: options) { _, error in guard error == .success else { return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) } - self.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account, options: NKRequestOptions(queue: self.nkCommonInstance.backgroundQueue)) { _, files, _, error in - - guard error == .success, let file = files.first else { + guard error == .success, let file = files?.first else { return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) } return completion(account, filesChunkOutput, file, nil, error) diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 3ab08a12..9a304eb5 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -36,26 +36,28 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, public func download(serverUrlFileName: Any, fileNameLocalPath: String, taskDescription: String? = nil, - account: String, - session: URLSession) -> URLSessionDownloadTask? { + account: String) -> URLSessionDownloadTask? { var url: URL? if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { url = (serverUrlFileName as? String)?.encodedToUrl as? URL } + guard let nkSession = nkCommonInstance.getSession(account: account) else { + return nil + } guard let urlForRequest = url else { return nil } var request = URLRequest(url: urlForRequest) - let loginString = "\(self.nkCommonInstance.user):\(self.nkCommonInstance.password)" + let loginString = "\(nkSession.user):\(nkSession.password)" guard let loginData = loginString.data(using: String.Encoding.utf8) else { return nil } let base64LoginString = loginData.base64EncodedString() - request.setValue(self.nkCommonInstance.userAgent, forHTTPHeaderField: "User-Agent") + request.setValue(nkSession.userAgent, forHTTPHeaderField: "User-Agent") request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") - let task = session.downloadTask(with: request) + let task = nkSession.sessionDownloadBackground.downloadTask(with: request) task.taskDescription = taskDescription task.resume() self.nkCommonInstance.writeLog("Network start download file: \(serverUrlFileName)") @@ -71,8 +73,10 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, dateModificationFile: Date?, taskDescription: String? = nil, account: String, - session: URLSession) -> URLSessionUploadTask? { + sessionIdentifier: String) -> URLSessionUploadTask? { var url: URL? + var uploadSession: URLSession? + guard let nkSession = nkCommonInstance.getSession(account: account) else { return nil } if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { @@ -82,14 +86,14 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, return nil } var request = URLRequest(url: urlForRequest) - let loginString = "\(self.nkCommonInstance.user):\(self.nkCommonInstance.password)" + let loginString = "\(nkSession.user):\(nkSession.password)" guard let loginData = loginString.data(using: String.Encoding.utf8) else { return nil } let base64LoginString = loginData.base64EncodedString() request.httpMethod = "PUT" - request.setValue(self.nkCommonInstance.userAgent, forHTTPHeaderField: "User-Agent") + request.setValue(nkSession.userAgent, forHTTPHeaderField: "User-Agent") request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") // Epoch of linux do not permitted negativ value if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { @@ -100,11 +104,18 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, request.setValue("\(dateModificationFile.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-MTime") } - let task = session.uploadTask(with: request, fromFile: URL(fileURLWithPath: fileNameLocalPath)) - task.taskDescription = taskDescription - task.resume() - self.nkCommonInstance.writeLog("Network start upload file: \(serverUrlFileName)") + if sessionIdentifier == nkCommonInstance.identifierSessionUploadBackground { + uploadSession = nkSession.sessionUploadBackground + } else if sessionIdentifier == nkCommonInstance.identifierSessionUploadBackgroundWWan { + uploadSession = nkSession.sessionUploadBackgroundWWan + } else if sessionIdentifier == nkCommonInstance.identifierSessionUploadBackgroundExt { + uploadSession = nkSession.sessionUploadBackgroundExt + } + let task = uploadSession?.uploadTask(with: request, fromFile: URL(fileURLWithPath: fileNameLocalPath)) + task?.taskDescription = taskDescription + task?.resume() + self.nkCommonInstance.writeLog("Network start upload file: \(serverUrlFileName)") return task } diff --git a/Sources/NextcloudKit/Utility/ThreadSafeArray.swift b/Sources/NextcloudKit/Utility/ThreadSafeArray.swift new file mode 100644 index 00000000..acd6f736 --- /dev/null +++ b/Sources/NextcloudKit/Utility/ThreadSafeArray.swift @@ -0,0 +1,353 @@ +// +// ThreadSafeArray.swift +// Nextcloud +// +// Created by Marino Faggiana on 31/01/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// http://basememara.com/creating-thread-safe-arrays-in-swift/ +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation + +/// A thread-safe array. +public class ThreadSafeArray { + + private var array = [Element]() + private let queue = DispatchQueue(label: "com.nextcloud.ThreadSafeArray", attributes: .concurrent) + + public init() { } + + public convenience init(_ array: [Element]) { + self.init() + self.array = array + } +} + +// MARK: - Properties + +public extension ThreadSafeArray { + + /// The first element of the collection. + var first: Element? { + var result: Element? + queue.sync { result = self.array.first } + return result + } + + /// The last element of the collection. + var last: Element? { + var result: Element? + queue.sync { result = self.array.last } + return result + } + + /// The number of elements in the array. + var count: Int { + var result = 0 + queue.sync { result = self.array.count } + return result + } + + /// A Boolean value indicating whether the collection is empty. + var isEmpty: Bool { + var result = false + queue.sync { result = self.array.isEmpty } + return result + } + + /// A textual representation of the array and its elements. + var description: String { + var result = "" + queue.sync { result = self.array.description } + return result + } +} + +// MARK: - Immutable + +public extension ThreadSafeArray { + + /// Returns the first element of the sequence that satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - Returns: The first element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. + func first(where predicate: (Element) -> Bool) -> Element? { + var result: Element? + queue.sync { result = self.array.first(where: predicate) } + return result + } + + /// Returns the last element of the sequence that satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - Returns: The last element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. + func last(where predicate: (Element) -> Bool) -> Element? { + var result: Element? + queue.sync { result = self.array.last(where: predicate) } + return result + } + + /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate. + /// + /// - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array. + /// - Returns: An array of the elements that includeElement allowed. + func filter(_ isIncluded: @escaping (Element) -> Bool) -> ThreadSafeArray { + var result: ThreadSafeArray? + queue.sync { result = ThreadSafeArray(self.array.filter(isIncluded)) } + return result! + } + + /// Returns the first index in which an element of the collection satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match. + /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil. + func index(where predicate: (Element) -> Bool) -> Int? { + var result: Int? + queue.sync { result = self.array.firstIndex(where: predicate) } + return result + } + + /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements. + /// + /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false. + /// - Returns: A sorted array of the collection’s elements. + func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> ThreadSafeArray { + var result: ThreadSafeArray? + queue.sync { result = ThreadSafeArray(self.array.sorted(by: areInIncreasingOrder)) } + return result! + } + + /// Returns an array containing the results of mapping the given closure over the sequence’s elements. + /// + /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. + /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. + func map(_ transform: @escaping (Element) -> ElementOfResult) -> [ElementOfResult] { + var result = [ElementOfResult]() + queue.sync { result = self.array.map(transform) } + return result + } + + /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. + /// + /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. + /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. + func compactMap(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] { + var result = [ElementOfResult]() + queue.sync { result = self.array.compactMap(transform) } + return result + } + + /// Returns the result of combining the elements of the sequence using the given closure. + /// + /// - Parameters: + /// - initialResult: The value to use as the initial accumulating value. initialResult is passed to nextPartialResult the first time the closure is executed. + /// - nextPartialResult: A closure that combines an accumulating value and an element of the sequence into a new accumulating value, to be used in the next call of the nextPartialResult closure or returned to the caller. + /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. + func reduce(_ initialResult: ElementOfResult, _ nextPartialResult: @escaping (ElementOfResult, Element) -> ElementOfResult) -> ElementOfResult { + var result: ElementOfResult? + queue.sync { result = self.array.reduce(initialResult, nextPartialResult) } + return result ?? initialResult + } + + /// Returns the result of combining the elements of the sequence using the given closure. + /// + /// - Parameters: + /// - initialResult: The value to use as the initial accumulating value. + /// - updateAccumulatingResult: A closure that updates the accumulating value with an element of the sequence. + /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. + func reduce(into initialResult: ElementOfResult, _ updateAccumulatingResult: @escaping (inout ElementOfResult, Element) -> Void) -> ElementOfResult { + var result: ElementOfResult? + queue.sync { result = self.array.reduce(into: initialResult, updateAccumulatingResult) } + return result ?? initialResult + } + + /// Calls the given closure on each element in the sequence in the same order as a for-in loop. + /// + /// - Parameter body: A closure that takes an element of the sequence as a parameter. + func forEach(_ body: (Element) -> Void) { + queue.sync { self.array.forEach(body) } + } + + /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match. + /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false. + func contains(where predicate: (Element) -> Bool) -> Bool { + var result = false + queue.sync { result = self.array.contains(where: predicate) } + return result + } + + /// Returns a Boolean value indicating whether every element of a sequence satisfies a given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element satisfies a condition. + /// - Returns: true if the sequence contains only elements that satisfy predicate; otherwise, false. + func allSatisfy(_ predicate: (Element) -> Bool) -> Bool { + var result = false + queue.sync { result = self.array.allSatisfy(predicate) } + return result + } + + /// Returns the array + /// + /// - Returns: the array part. + func getArray() -> [Element]? { + var results: [Element]? + queue.sync { results = self.array } + return results + } +} + +// MARK: - Mutable + +public extension ThreadSafeArray { + + /// Adds a new element at the end of the array. + /// + /// - Parameter element: The element to append to the array. + func append(_ element: Element) { + queue.async(flags: .barrier) { + self.array.append(element) + } + } + + /// Adds new elements at the end of the array. + /// + /// - Parameter element: The elements to append to the array. + func append(_ elements: [Element]) { + queue.async(flags: .barrier) { + self.array += elements + } + } + + /// Inserts a new element at the specified position. + /// + /// - Parameters: + /// - element: The new element to insert into the array. + /// - index: The position at which to insert the new element. + func insert(_ element: Element, at index: Int) { + queue.async(flags: .barrier) { + self.array.insert(element, at: index) + } + } + + /// Removes and returns the element at the specified position. + /// + /// - Parameters: + /// - index: The position of the element to remove. + /// - completion: The handler with the removed element. + func remove(at index: Int, completion: ((Element) -> Void)? = nil) { + queue.async(flags: .barrier) { + let element = self.array.remove(at: index) + DispatchQueue.main.async { completion?(element) } + } + } + + /// Removes and returns the elements that meet the criteria. + /// + /// - Parameters: + /// - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - completion: The handler with the removed elements. + func remove(where predicate: @escaping (Element) -> Bool, completion: (([Element]) -> Void)? = nil) { + queue.async(flags: .barrier) { + var elements = [Element]() + + while let index = self.array.firstIndex(where: predicate) { + elements.append(self.array.remove(at: index)) + } + + DispatchQueue.main.async { completion?(elements) } + } + } + + /// Removes all elements from the array. + /// + /// - Parameter completion: The handler with the removed elements. + func removeAll(completion: (([Element]) -> Void)? = nil) { + queue.async(flags: .barrier) { + let elements = self.array + self.array.removeAll() + DispatchQueue.main.async { completion?(elements) } + } + } +} + +public extension ThreadSafeArray { + + /// Accesses the element at the specified position if it exists. + /// + /// - Parameter index: The position of the element to access. + /// - Returns: optional element if it exists. + subscript(index: Int) -> Element? { + get { + var result: Element? + + queue.sync { + guard self.array.startIndex.. Bool { + var result = false + queue.sync { result = self.array.contains(element) } + return result + } +} + +// MARK: - Infix operators + +public extension ThreadSafeArray { + + /// Adds a new element at the end of the array. + /// + /// - Parameters: + /// - left: The collection to append to. + /// - right: The element to append to the array. + static func += (left: inout ThreadSafeArray, right: Element) { + left.append(right) + } + + /// Adds new elements at the end of the array. + /// + /// - Parameters: + /// - left: The collection to append to. + /// - right: The elements to append to the array. + static func += (left: inout ThreadSafeArray, right: [Element]) { + left.append(right) + } +} diff --git a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift index 00b57c56..d30420ae 100644 --- a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift @@ -29,7 +29,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { let serverUrl = "\(baseUrl)/remote.php/dav/files/\(userId)" let serverUrlFileName = "\(serverUrl)/\(folderName)" - NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: baseUrl) + NextcloudKit.shared.appendAccount(account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") // Test creating folder NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in @@ -45,7 +45,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { XCTAssertEqual(self.account, account) XCTAssertEqual(NKError.success.errorCode, error.errorCode) XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) - XCTAssertEqual(files[0].fileName, folderName) + XCTAssertEqual(files?[0].fileName, folderName) Thread.sleep(forTimeInterval: 0.2) @@ -63,7 +63,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { XCTAssertEqual(404, error.errorCode) XCTAssertEqual(self.account, account) - XCTAssertTrue(files.isEmpty) + XCTAssertTrue(files?.isEmpty ?? false) } } } diff --git a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift index 1da28e7e..78f64141 100644 --- a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift @@ -21,6 +21,7 @@ // import XCTest +import Alamofire @testable import NextcloudKit final class ShareIntegrationTests: BaseIntegrationXCTestCase { @@ -31,7 +32,7 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { let serverUrl = "\(baseUrl)/remote.php/dav/files/\(userId)" let serverUrlFileName = "\(serverUrl)/\(folderName)" - NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: baseUrl) + NextcloudKit.shared.appendAccount(account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in XCTAssertEqual(self.account, account) @@ -43,7 +44,7 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { let note = "Test note" - NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: account) { account, share, data, error in + NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: "") { account, share, data, error in defer { expectation.fulfill() } XCTAssertEqual(self.account, account) diff --git a/Tests/NextcloudKitUnitTests/LoginUnitTests.swift b/Tests/NextcloudKitUnitTests/LoginUnitTests.swift index a6940d35..524ab820 100644 --- a/Tests/NextcloudKitUnitTests/LoginUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/LoginUnitTests.swift @@ -40,7 +40,7 @@ class LoginUnitTests: XCTestCase { override func setUp() { // Set our mock session manager as the one the API is going to use - NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() + // NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() } // Format of function names should be: func test_functionName_withCircumstances_shouldExpectation() {} @@ -59,7 +59,7 @@ class LoginUnitTests: XCTestCase { } mock.register() - NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() + //NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() // Now we call the function we want to test; it will use the mock session and request and return the mock data NextcloudKit.shared.getLoginFlowV2(serverUrl: serverUrl) { token, endpoint, login, data, error in @@ -109,7 +109,7 @@ class LoginUnitTests: XCTestCase { mock.register() // Set our mock session manager as the one the API is going to use - NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() + // NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() // Now we call the function we want to test; it will use the mock session and request and return the mock data NextcloudKit.shared.getLoginFlowV2(serverUrl: "badUrl") { token, endpoint, login, data, error in From f6c4c86e538b45a16b1b838c574aaa124c0c9790 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 31 Jul 2024 14:02:17 +0200 Subject: [PATCH 052/135] Revert "cod" This reverts commit 83b75a17686ad446e7f720c28b542c5cabdda4c9. Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 115 ++++-- Sources/NextcloudKit/NKModel.swift | 20 +- Sources/NextcloudKit/NKSession.swift | 114 ------ Sources/NextcloudKit/NextcloudKit+API.swift | 157 ++++---- .../NextcloudKit/NextcloudKit+Assistant.swift | 50 +-- .../NextcloudKit/NextcloudKit+Comments.swift | 67 ++-- .../NextcloudKit/NextcloudKit+Dashboard.swift | 36 +- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 96 ++--- .../NextcloudKit/NextcloudKit+FilesLock.swift | 7 +- .../NextcloudKit+Groupfolders.swift | 11 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 11 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 13 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 9 +- .../NextcloudKit/NextcloudKit+NCText.swift | 44 +-- .../NextcloudKit+PushNotification.swift | 28 +- .../NextcloudKit+Richdocuments.swift | 48 +-- .../NextcloudKit/NextcloudKit+Search.swift | 22 +- Sources/NextcloudKit/NextcloudKit+Share.swift | 50 +-- .../NextcloudKit+UserStatus.swift | 63 ++-- .../NextcloudKit/NextcloudKit+WebDAV.swift | 159 ++++---- Sources/NextcloudKit/NextcloudKit.swift | 136 ++++--- .../NextcloudKit/NextcloudKitBackground.swift | 35 +- .../Utility/ThreadSafeArray.swift | 353 ------------------ .../FilesIntegrationTests.swift | 6 +- .../ShareIntegrationTests.swift | 5 +- .../LoginUnitTests.swift | 6 +- 26 files changed, 607 insertions(+), 1054 deletions(-) delete mode 100644 Sources/NextcloudKit/NKSession.swift delete mode 100644 Sources/NextcloudKit/Utility/ThreadSafeArray.swift diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 66f299d6..2b9795a8 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -30,7 +30,7 @@ import MobileCoreServices import CoreServices #endif -public protocol NextcloudKitDelegate { +public protocol NKCommonDelegate { func authenticationChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) @@ -40,19 +40,15 @@ public protocol NextcloudKitDelegate { func uploadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) func downloadingFinish(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) - + func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: Date?, dateLastModified: Date?, length: Int64, task: URLSessionTask, error: NKError) func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: Date?, size: Int64, task: URLSessionTask, error: NKError) } public class NKCommon: NSObject { public let dav: String = "remote.php/dav" - public let identifierSessionDownload: String = "com.nextcloud.nextcloudkit.session.download" - public let identifierSessionUpload: String = "com.nextcloud.nextcloudkit.session.upload" - public let identifierSessionDownloadBackground: String = "com.nextcloud.session.download.background" - public let identifierSessionUploadBackground: String = "com.nextcloud.session.upload.background" - public let identifierSessionUploadBackgroundWWan: String = "com.nextcloud.session.upload.backgroundWWan" - public let identifierSessionUploadBackgroundExt: String = "com.nextcloud.session.upload.extension" + public let sessionIdentifierDownload: String = "com.nextcloud.nextcloudkit.session.download" + public let sessionIdentifierUpload: String = "com.nextcloud.nextcloudkit.session.upload" public enum TypeReachability: Int { case unknown = 0 @@ -96,11 +92,37 @@ public class NKCommon: NSObject { var name: String } + public let notificationCenterChunkedFileStop = NSNotification.Name(rawValue: "NextcloudKit.chunkedFile.stop") + + internal lazy var sessionConfiguration: URLSessionConfiguration = { + let configuration = URLSessionConfiguration.af.default + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + if let groupIdentifier { + let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: groupIdentifier) + configuration.httpCookieStorage = cookieStorage + } else { + configuration.httpCookieStorage = nil + } + return configuration + }() + internal var rootQueue: DispatchQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.sessionManagerData.rootQueue") + internal var requestQueue: DispatchQueue? + internal var serializationQueue: DispatchQueue? + + internal var _user = "" + internal var _userId = "" + internal var _password = "" + internal var _account = "" + internal var _urlBase = "" + internal var _userAgent: String? + internal var _nextcloudVersion: Int = 0 + internal var _groupIdentifier: String? + + internal var internalTypeIdentifiers: [UTTypeConformsToServer] = [] internal var utiCache = NSCache() internal var mimeTypeCache = NSCache() internal var filePropertiesCache = NSCache() - internal var internalTypeIdentifiers: [UTTypeConformsToServer] = [] - internal var delegate: NextcloudKitDelegate? + internal var delegate: NKCommonDelegate? private var _filenameLog: String = "communication.log" private var _pathLog: String = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! @@ -110,9 +132,39 @@ public class NKCommon: NSObject { private var _copyLogToDocumentDirectory: Bool = false private let queueLog = DispatchQueue(label: "com.nextcloud.nextcloudkit.queuelog", attributes: .concurrent ) + public var user: String { + return _user + } + + public var userId: String { + return _userId + } + + public var password: String { + return _password + } + + public var account: String { + return _account + } + + public var urlBase: String { + return _urlBase + } + + public var userAgent: String? { + return _userAgent + } + + public var nextcloudVersion: Int { + return _nextcloudVersion + } + + public var groupIdentifier: String? { + return _groupIdentifier + } + public let backgroundQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.backgroundqueue", qos: .background, attributes: .concurrent) - public let notificationCenterChunkedFileStop = NSNotification.Name(rawValue: "NextcloudKit.chunkedFile.stop") - public var nksessions = ThreadSafeArray() public var filenameLog: String { get { @@ -434,44 +486,41 @@ public class NKCommon: NSObject { // MARK: - Common - public func getSession(account: String) -> NKSession? { - var session: NKSession? - nksessions.forEach { result in - if result.account == account { - session = result - } - } - return session + public func getStandardHeaders(options: NKRequestOptions) -> HTTPHeaders { + return getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent, contentType: options.contentType) } - public func getStandardHeaders(account: String, options: NKRequestOptions? = nil) -> HTTPHeaders? { - guard let session = nksessions.filter({ $0.account == account }).first else { return nil} + public func getStandardHeaders(_ appendHeaders: [String: String]? = nil, customUserAgent: String? = nil, contentType: String? = nil) -> HTTPHeaders { + return getStandardHeaders(user: user, password: password, appendHeaders: appendHeaders, customUserAgent: customUserAgent, contentType: contentType) + } + + public func getStandardHeaders(user: String?, password: String?, appendHeaders: [String: String]?, customUserAgent: String?, contentType: String? = nil) -> HTTPHeaders { var headers: HTTPHeaders = [] - headers.update(.authorization(username: session.user, password: session.password)) - headers.update(.userAgent(session.userAgent)) - if let customUserAgent = options?.customUserAgent { + if let user, let password { + headers.update(.authorization(username: user, password: password)) + } + if let customUserAgent { headers.update(.userAgent(customUserAgent)) + } else if let userAgent = userAgent { + headers.update(.userAgent(userAgent)) } - if let contentType = options?.contentType { + if let contentType { headers.update(.contentType(contentType)) } else { headers.update(.contentType("application/x-www-form-urlencoded")) } - if options?.contentType != "application/xml" { + if contentType != "application/xml" { headers.update(name: "Accept", value: "application/json") } headers.update(name: "OCS-APIRequest", value: "true") - for (key, value) in options?.customHeader ?? [:] { + for (key, value) in appendHeaders ?? [:] { headers.update(name: key, value: value) } return headers } - public func createStandardUrl(serverUrl: String, endpoint: String, options: NKRequestOptions) -> URLConvertible? { - if let endpoint = options.endpoint { - return URL(string: endpoint) - } + public func createStandardUrl(serverUrl: String, endpoint: String) -> URLConvertible? { guard var serverUrl = serverUrl.urlEncoded else { return nil } if serverUrl.last != "/" { serverUrl = serverUrl + "/" } @@ -537,7 +586,7 @@ public class NKCommon: NSObject { return 0 } - public func returnPathfromServerUrl(_ serverUrl: String, urlBase: String, userId: String) -> String { + public func returnPathfromServerUrl(_ serverUrl: String) -> String { let home = urlBase + "/remote.php/dav/files/" + userId return serverUrl.replacingOccurrences(of: home, with: "") } diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index be8fd7ef..c1f4168c 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -607,10 +607,10 @@ class NKDataFileXML: NSObject { return xml["ocs", "data", "apppassword"].text } - func convertDataFile(xmlData: Data, nkSession: NKSession, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] { + func convertDataFile(xmlData: Data, dav: String, urlBase: String, user: String, userId: String, account: String, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] { var files: [NKFile] = [] - let rootFiles = "/" + nkSession.dav + "/files/" - guard let baseUrl = self.nkCommonInstance.getHostName(urlString: nkSession.urlBase) else { + let rootFiles = "/" + dav + "/files/" + guard let baseUrl = self.nkCommonInstance.getHostName(urlString: urlBase) else { return files } let xml = XML.parse(xmlData) @@ -641,7 +641,7 @@ class NKDataFileXML: NSObject { } // account - file.account = nkSession.account + file.account = account // path file.path = (fileNamePath as NSString).deletingLastPathComponent + "/" @@ -652,7 +652,7 @@ class NKDataFileXML: NSObject { file.fileName = file.fileName.removingPercentEncoding ?? "" // ServerUrl - if href == rootFiles + nkSession.user + "/" { + if href == rootFiles + user + "/" { file.fileName = "." file.serverUrl = ".." } else { @@ -864,9 +864,9 @@ class NKDataFileXML: NSObject { file.iconName = results.iconName file.name = "files" file.classFile = results.classFile - file.urlBase = nkSession.urlBase - file.user = nkSession.user - file.userId = nkSession.userId + file.urlBase = urlBase + file.user = user + file.userId = userId files.append(file) } @@ -891,10 +891,10 @@ class NKDataFileXML: NSObject { return files } - func convertDataTrash(xmlData: Data, nkSession: NKSession, showHiddenFiles: Bool) -> [NKTrash] { + func convertDataTrash(xmlData: Data, urlBase: String, showHiddenFiles: Bool) -> [NKTrash] { var files: [NKTrash] = [] var first: Bool = true - guard let baseUrl = self.nkCommonInstance.getHostName(urlString: nkSession.urlBase) else { + guard let baseUrl = self.nkCommonInstance.getHostName(urlString: urlBase) else { return files } let xml = XML.parse(xmlData) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift deleted file mode 100644 index 5401966c..00000000 --- a/Sources/NextcloudKit/NKSession.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// NKSession.swift -// -// -// Created by Marino Faggiana on 20/07/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import Foundation -import Alamofire - -public class NKSession { - public var urlBase: String - public var user: String - public var userId: String - public var password: String - public let account: String - public var userAgent: String - public var nextcloudVersion: Int - public let groupIdentifier: String? - public let dav: String = "remote.php/dav" - public var internalTypeIdentifiers: [NKCommon.UTTypeConformsToServer] = [] - public let sessionData: Alamofire.Session - public let sessionDownloadBackground: URLSession - public let sessionUploadBackground: URLSession - public let sessionUploadBackgroundWWan: URLSession - public let sessionUploadBackgroundExt: URLSession - - init(urlBase: String, - user: String, - userId: String, - password: String, - account: String, - userAgent: String, - nextcloudVersion: Int, - groupIdentifier: String? = nil) { - self.urlBase = urlBase - self.user = user - self.userId = userId - self.password = password - self.account = account - self.userAgent = userAgent - self.nextcloudVersion = nextcloudVersion - self.groupIdentifier = groupIdentifier - - let backgroundSessionDelegate = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance) - /// Strange but works ?!?! - let sharedCookieStorage = UUID().uuidString + "_" + user + "@" + urlBase - - /// Session Alamofire - let configuration = URLSessionConfiguration.default - configuration.requestCachePolicy = .reloadIgnoringLocalCacheData - configuration.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) - sessionData = Alamofire.Session(configuration: configuration, - // delegate: NextcloudKit.shared, - eventMonitors: [AlamofireLogger(nkCommonInstance: NextcloudKit.shared.nkCommonInstance)]) - - /// Session Download Background - let configurationDownloadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionDownloadBackground) - configurationDownloadBackground.allowsCellularAccess = true - configurationDownloadBackground.sessionSendsLaunchEvents = true - configurationDownloadBackground.isDiscretionary = false - configurationDownloadBackground.httpMaximumConnectionsPerHost = 5 - configurationDownloadBackground.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData - configurationDownloadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) - sessionDownloadBackground = URLSession(configuration: configurationDownloadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) - - /// Session Upload Background - let configurationUploadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackground) - configurationUploadBackground.allowsCellularAccess = true - configurationUploadBackground.sessionSendsLaunchEvents = true - configurationUploadBackground.isDiscretionary = false - configurationUploadBackground.httpMaximumConnectionsPerHost = 5 - configurationUploadBackground.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData - configurationUploadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) - sessionUploadBackground = URLSession(configuration: configurationUploadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) - - /// Session Upload Background WWan - let configurationUploadBackgroundWWan = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackgroundWWan) - configurationUploadBackgroundWWan.allowsCellularAccess = false - configurationUploadBackgroundWWan.sessionSendsLaunchEvents = true - configurationUploadBackgroundWWan.isDiscretionary = false - configurationUploadBackgroundWWan.httpMaximumConnectionsPerHost = 5 - configurationUploadBackgroundWWan.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData - configurationUploadBackgroundWWan.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) - sessionUploadBackgroundWWan = URLSession(configuration: configurationUploadBackgroundWWan, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) - - /// Session Upload Background Extension - let configurationUploadBackgroundExt = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackgroundExt) - configurationUploadBackgroundExt.allowsCellularAccess = true - configurationUploadBackgroundExt.sessionSendsLaunchEvents = true - configurationUploadBackgroundExt.isDiscretionary = false - configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = 5 - configurationUploadBackgroundExt.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData - configurationUploadBackgroundExt.sharedContainerIdentifier = groupIdentifier - configurationUploadBackgroundExt.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) - sessionUploadBackgroundExt = URLSession(configuration: configurationUploadBackgroundExt, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) - } -} diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 886d922c..f7f3382e 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -77,19 +77,19 @@ public extension NextcloudKit { // MARK: - func generalWithEndpoint(_ endpoint: String, - account: String, method: String, + account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let urlBase = self.nkCommonInstance.urlBase + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: method.uppercased()) + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -112,15 +112,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var externalSites: [NKExternalSite] = [] let endpoint = "ocs/v2.php/apps/external/api/v1" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, externalSites, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -174,15 +174,12 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (ServerInfoResult) -> Void) { let endpoint = "status.php" - guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(ServerInfoResult.failure(.urlError)) } } - var headers: HTTPHeaders? - if let userAgent = options.customUserAgent { - headers = [HTTPHeader.userAgent(userAgent)] - } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - AF.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -231,16 +228,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - guard let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, .urlError) } - } + var headers = self.nkCommonInstance.getStandardHeaders(options: options) if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -274,19 +268,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + guard let urlRequest = url else { return options.queue.async { completion(account, nil, .urlError) } } - + var headers = self.nkCommonInstance.getStandardHeaders(options: options) if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -364,22 +358,26 @@ public extension NextcloudKit { compressionQualityPreview: CGFloat, compressionQualityIcon: CGFloat, etag: String? = nil, - endpoint: String, + endpoint: String?, account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let urlBase = self.nkCommonInstance.urlBase + var url: URLConvertible? + if let endpoint { + url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + } + guard let urlRequest = url else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } + var headers = self.nkCommonInstance.getStandardHeaders(options: options) if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -425,19 +423,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/avatar/\(user)/\(sizeImage)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - + var headers = self.nkCommonInstance.getStandardHeaders(options: options) if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -513,13 +510,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - guard let url = serverUrl.asUrl, - let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = serverUrl.asUrl else { return options.queue.async { completion(account, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -546,14 +542,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/cloud/user" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -615,14 +611,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v1.php/cloud/capabilities" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -653,16 +649,12 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ wipe: Bool, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "index.php/core/wipe/check" let parameters: [String: Any] = ["token": token] - /// - options.contentType = "application/json" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(account, false, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -689,16 +681,12 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "index.php/core/wipe/success" let parameters: [String: Any] = ["token": token] - /// - options.contentType = "application/json" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -726,6 +714,7 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var activities: [NKActivity] = [] var activityFirstKnown = 0 var activityLastGiven = 0 @@ -748,13 +737,12 @@ public extension NextcloudKit { parameters["previews"] = "true" } - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -823,14 +811,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + var notifications: [NKNotifications] = [] + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -845,7 +834,6 @@ public extension NextcloudKit { let json = JSON(jsonData) if json["ocs"]["meta"]["statuscode"].int == 200 { let ocsdata = json["ocs"]["data"] - var notifications: [NKNotifications] = [] for (_, subJson): (String, JSON) in ocsdata { let notification = NKNotifications() @@ -897,14 +885,11 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } - } + let urlBase = self.nkCommonInstance.urlBase var url: URLConvertible? if serverUrl == nil { let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications/\(idNotification)" - url = self.nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options) + url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) } else { url = serverUrl?.asUrl } @@ -912,8 +897,9 @@ public extension NextcloudKit { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: method) + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -937,18 +923,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/dav/api/v1/direct" let parameters: [String: Any] = [ "fileId": fileId, "format": "json" ] - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -975,15 +961,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/security_guard/diagnostics" - /// - options.contentType = "application/json" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: .put, headers: headers) @@ -992,7 +975,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 6b02e271..15e75380 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -30,14 +30,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ types: [NKTextProcessingTaskType]?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/textprocessing/tasktypes" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -70,15 +70,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/schedule" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -108,14 +108,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -145,14 +145,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -182,14 +182,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: [NKTextProcessingTask]?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/tasks/app/\(appId)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 3667b298..61bd2174 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -30,18 +30,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ items: [NKComments]?, _ data: Data?, _ error: NKError) -> Void) { - /// - options.contentType = "application/xml" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } - } - let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav + let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPFIND") + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -51,7 +47,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -80,17 +76,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - /// - options.contentType = "application/json" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } - } - let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav + let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") var urlRequest: URLRequest do { @@ -101,7 +93,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -126,18 +118,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - /// - options.contentType = "application/xml" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } - } - let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)/\(messageId)" + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav + let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -148,7 +136,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -172,16 +160,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } - } - let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)/\(messageId)" + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav + let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -204,18 +191,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - /// - options.contentType = "application/xml" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } - } - let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav + let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -226,7 +209,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 4c4efe1a..4c4651eb 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -31,14 +31,20 @@ public extension NextcloudKit { request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardWidgets: [NCCDashboardWidget]?, _ data: Data?, _ error: NKError) -> Void) { - let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widgets" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + let urlBase = self.nkCommonInstance.urlBase + var url: URLConvertible? + if let endpoint = options.endpoint { + url = URL(string: endpoint) + } else { + let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widgets" + url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) } + guard let url = url else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -70,14 +76,20 @@ public extension NextcloudKit { request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardApplications: [NCCDashboardApplication]?, _ data: Data?, _ error: NKError) -> Void) { - let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widget-items?widgets[]=\(items)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + let urlBase = self.nkCommonInstance.urlBase + var url: URLConvertible? + if let endpoint = options.endpoint { + url = URL(string: endpoint) + } else { + let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widget-items?widgets[]=\(items)" + url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + } + guard let url = url else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index 2dee0358..be39affc 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -32,19 +32,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/encrypted/\(fileId)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } let method: HTTPMethod = delete ? .delete : .put + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -75,17 +75,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eToken: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/lock/\(fileId)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: method) + var headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] if let e2eToken { @@ -96,7 +96,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-COUNTER", value: e2eCounter) } - nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -126,22 +126,23 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eMetadata: String?, _ signature: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] + if let e2eToken { parameters["e2e-token"] = e2eToken } - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -175,20 +176,22 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ metadata: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: method) + var headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] + parameters["e2e-token"] = e2eToken headers.update(name: "e2e-token", value: e2eToken) + if let e2eMetadata { parameters["metaData"] = e2eMetadata } @@ -196,7 +199,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-SIGNATURE", value: signature) } - nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -227,7 +230,8 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ certificateUser: String?, _ data: Data?, _ error: NKError) -> Void) { - + let urlBase = self.nkCommonInstance.urlBase + let userId = self.nkCommonInstance.userId var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -241,13 +245,12 @@ public extension NextcloudKit { } else { endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" } - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -262,7 +265,7 @@ public extension NextcloudKit { let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { - let key = json["ocs"]["data"]["public-keys"][nkSession.userId].stringValue + let key = json["ocs"]["data"]["public-keys"][userId].stringValue if let user = user { let keyUser = json["ocs"]["data"]["public-keys"][user].string options.queue.async { completion(account, key, keyUser, jsonData, .success) } @@ -280,18 +283,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -319,18 +322,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/server-key" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -359,19 +362,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = ["csr": certificate] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -401,19 +404,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = ["privateKey": privateKey] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -441,17 +444,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let headers = self.nkCommonInstance.getStandardHeaders(options: options) + + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -472,18 +476,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index 2cdfa92a..851915df 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -38,13 +38,10 @@ public extension NextcloudKit { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: shouldLock ? "LOCK" : "UNLOCK") - guard let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } - } + var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "X-User-Lock", value: "1") - nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 3c54f4bb..5c2b1e85 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -30,14 +30,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ results: [NKGroupfolders]?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/apps/groupfolders/folders?applicable=1" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index 93331745..e1964925 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -32,14 +32,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ result: NKHovercard?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/hovercard/v1/\(userId)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 091047d5..8259d105 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -31,18 +31,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlfileNamePath.encodedToUrl, - let nkSession = nkCommonInstance.getSession(account: account) else { + guard let url = serverUrlfileNamePath.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") - /// - options.contentType = "application/xml" - /// - guard let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } - } + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) let parameters = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyLivephoto, livePhotoFile) @@ -51,7 +46,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 207dc1fb..1bb056b1 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -35,7 +35,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/getapppassword" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint) else { return options.queue.async { completion(nil, nil, .urlError) } } var headers: HTTPHeaders = [.authorization(username: user, password: password)] @@ -82,8 +82,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/apppassword" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(nil, .urlError) } } var headers: HTTPHeaders = [.authorization(username: username, password: password)] @@ -99,7 +98,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -123,7 +122,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ endpoint: String?, _ login: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "index.php/login/v2" - guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(nil, nil, nil, nil, .urlError) } } var headers: HTTPHeaders? diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index a23ab7c1..f6d7cfc2 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -26,20 +26,20 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func NCTextObtainEditorDetails(account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { + func NCTextObtainEditorDetails(account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing" var editors: [NKEditorDetailsEditors] = [] var creators: [NKEditorDetailsCreators] = [] - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, editors, creators, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -96,6 +96,7 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -103,13 +104,12 @@ public extension NextcloudKit { if let fileId = fileId { endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/open?path=/\(fileNamePath)&fileId=\(fileId)&editorId=\(editor)" } - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -131,16 +131,16 @@ public extension NextcloudKit { func NCTextGetListOfTemplates(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ templates: [NKEditorTemplates]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ templates: [NKEditorTemplates], _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/templates/text/textdocumenttemplate" var templates: [NKEditorTemplates] = [] - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, templates, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -178,6 +178,7 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -187,13 +188,12 @@ public extension NextcloudKit { } else { endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/create?path=/\(fileNamePath)&editorId=\(editorId)&creatorId=\(creatorId)&templateId=\(templateId)" } - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index 2c1cc184..656704ca 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -27,6 +27,8 @@ import SwiftyJSON public extension NextcloudKit { func subscribingPushNotification(serverUrl: String, + user: String, + password: String, pushTokenHash: String, devicePublicKey: String, proxyServerUrl: String, @@ -35,9 +37,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } let parameters = [ @@ -45,8 +45,9 @@ public extension NextcloudKit { "devicePublicKey": devicePublicKey, "proxyServer": proxyServerUrl ] + let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -73,18 +74,19 @@ public extension NextcloudKit { } func unsubscribingPushNotification(serverUrl: String, + user: String, + password: String, account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -111,8 +113,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "devices?format=json" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint, options: options), + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), let userAgent = options.customUserAgent else { return options.queue.async { completion(account, .urlError) } } @@ -124,7 +125,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -150,8 +151,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "devices" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint, options: options), + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), let userAgent = options.customUserAgent else { return options.queue.async { completion(account, .urlError) } } @@ -162,7 +162,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - nkSession.sessionData.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index 5cb9d6e9..a4a55f4d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -26,20 +26,20 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func createUrlRichdocuments(fileID: String, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + func createUrlRichdocuments(fileID: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/document" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let parameters: [String: Any] = ["fileId": fileID] + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let parameters: [String: Any] = ["fileId": fileID] + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -67,14 +67,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ templates: [NKRichdocumentsTemplate]?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/\(typeTemplate)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -115,15 +115,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/new" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let parameters: [String: Any] = ["path": path, "template": templateId] + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let parameters: [String: Any] = ["path": path, "template": templateId] + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -151,15 +151,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/apps/richdocuments/assets" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let parameters: [String: Any] = ["path": path] + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let parameters: [String: Any] = ["path": path] + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index d89f8a3d..e501727f 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -52,14 +52,14 @@ public extension NextcloudKit { providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, update: @escaping (_ account: String, _ searchResult: NKSearchResult?, _ provider: NKSearchProvider, _ error: NKError) -> Void, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/search/providers" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return completion(account, nil, .urlError) } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let requestUnifiedSearch = nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestUnifiedSearch = sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -122,9 +122,8 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, NKSearchResult?, _ data: Data?, _ error: NKError) -> Void) -> DataRequest? { - guard let term = term.urlEncoded, - let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let urlBase = self.nkCommonInstance.urlBase + guard let term = term.urlEncoded else { completion(account, nil, nil, .urlError) return nil } @@ -135,11 +134,14 @@ public extension NextcloudKit { if let cursor = cursor { endpoint += "&cursor=\(cursor)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options) + guard let url = self.nkCommonInstance.createStandardUrl( + serverUrl: urlBase, + endpoint: endpoint) else { completion(account, nil, nil, .urlError) return nil } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) var urlRequest: URLRequest do { @@ -150,7 +152,7 @@ public extension NextcloudKit { return nil } - let requestSearchProvider = nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestSearchProvider = sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 961ae284..70186482 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -80,13 +80,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ shares: [NKShare]?, _ data: Data?, _ error: NKError) -> Void) { - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: parameters.endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let urlBase = self.nkCommonInstance.urlBase + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: parameters.endpoint) + else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -131,16 +132,16 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ sharees: [NKSharee]?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/sharees" var lookupString = "false" if lookup { lookupString = "true" } - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = [ "search": search, "page": String(page), @@ -149,7 +150,7 @@ public extension NextcloudKit { "lookup": lookupString ] - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -289,12 +290,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "path": path, "shareType": String(shareType), @@ -319,7 +320,7 @@ public extension NextcloudKit { parameters["attributes"] = attributes } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -375,12 +376,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "permissions": String(permissions) ] @@ -404,7 +405,7 @@ public extension NextcloudKit { parameters["attributes"] = "[]" } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -428,21 +429,21 @@ public extension NextcloudKit { } /* - * @param idShare: Identifier of the share to update + * @param idShare Identifier of the share to update */ func deleteShare(idShare: Int, account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -464,7 +465,6 @@ public extension NextcloudKit { private func convertResponseShare(json: JSON, account: String) -> NKShare { let share = NKShare() - share.account = account share.canDelete = json["can_delete"].boolValue share.canEdit = json["can_edit"].boolValue share.displaynameFileOwner = json["displayname_file_owner"].stringValue diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index cb18b7c0..ce488f90 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -31,17 +31,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ clearAt: Date?, _ icon: String?, _ message: String?, _ messageId: String?, _ messageIsPredefined: Bool, _ status: String?, _ statusIsUserDefined: Bool, _ userId: String?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase var endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status" if let userId = userId { endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/\(userId)" } - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -82,17 +82,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/status" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = [ "statusType": String(status) ] - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -121,12 +121,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/predefined" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "messageId": String(messageId) ] @@ -134,7 +134,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -165,12 +165,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/custom" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "message": String(message) ] @@ -181,7 +181,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -209,14 +209,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -244,14 +244,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase + var userStatuses: [NKUserStatus] = [] let endpoint = "ocs/v2.php/apps/user_status/api/v1/predefined_statuses" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -263,11 +264,11 @@ public extension NextcloudKit { let error = NKError(error: error, afResponse: response, responseData: response.data) options.queue.async { completion(account, nil, nil, error) } case .success(let jsonData): - var userStatuses: [NKUserStatus] = [] let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { let ocsdata = json["ocs"]["data"] + for (_, subJson): (String, JSON) in ocsdata { let userStatus = NKUserStatus() @@ -297,18 +298,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { + let urlBase = self.nkCommonInstance.urlBase + var userStatuses: [NKUserStatus] = [] let endpoint = "ocs/v2.php/apps/user_status/api/v1/statuses" - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { return options.queue.async { completion(account, nil, nil, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = [ "limit": String(limit), "offset": String(offset) ] - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -322,11 +324,12 @@ public extension NextcloudKit { case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError - var userStatuses: [NKUserStatus] = [] if statusCode == 200 { let ocsdata = json["ocs"]["data"] + for (_, subJson): (String, JSON) in ocsdata { let userStatus = NKUserStatus() + if let value = subJson["clearAt"].double { if value > 0 { userStatus.clearAt = Date(timeIntervalSince1970: value) diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 81ad2d47..61d79eb4 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -31,13 +31,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ error: NKError) -> Void) { - guard let url = serverUrlFileName.encodedToUrl, - let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: "MKCOL") + let headers = self.nkCommonInstance.getStandardHeaders(options: options) var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -45,7 +45,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -76,12 +76,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlFileName.encodedToUrl, - let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } + let headers = self.nkCommonInstance.getStandardHeaders(options: options) var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: .delete, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -89,7 +89,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -113,12 +113,11 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlFileNameSource.encodedToUrl, - let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = serverUrlFileNameSource.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "MOVE") + var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") if overwrite { headers.update(name: "Overwrite", value: "T") @@ -126,6 +125,7 @@ public extension NextcloudKit { headers.update(name: "Overwrite", value: "F") } var urlRequest: URLRequest + do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -133,7 +133,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -157,12 +157,11 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlFileNameSource.encodedToUrl, - let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = serverUrlFileNameSource.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "COPY") + var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") if overwrite { headers.update(name: "Overwrite", value: "T") @@ -178,7 +177,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -203,16 +202,15 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { + let user = self.nkCommonInstance.user + let userId = self.nkCommonInstance.userId + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav var files: [NKFile] = [] var serverUrlFileName = serverUrlFileName - /// - options.contentType = "application/xml" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = serverUrlFileName.encodedToUrl, - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let url = serverUrlFileName.encodedToUrl else { + return options.queue.async { completion(account, files, nil, .urlError) } } if depth == "0", serverUrlFileName.last == "/" { serverUrlFileName = String(serverUrlFileName.dropLast()) @@ -220,6 +218,7 @@ public extension NextcloudKit { serverUrlFileName = serverUrlFileName + "/" } let method = HTTPMethod(rawValue: "PROPFIND") + var headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") headers.update(name: "Depth", value: depth) var urlRequest: URLRequest @@ -235,7 +234,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -248,7 +247,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -263,27 +262,26 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ file: NKFile?, _ data: Data?, _ error: NKError) -> Void) { - guard let nkSession = nkCommonInstance.getSession(account: account) else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } + let userId = self.nkCommonInstance.userId + let urlBase = self.nkCommonInstance.urlBase var httpBody: Data? if let fileId = fileId { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), nkSession.userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) } else if let link = link { let linkArray = link.components(separatedBy: "/") if let fileId = linkArray.last { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), nkSession.userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) } } guard let httpBody = httpBody else { return options.queue.async { completion(account, nil, nil, .urlError) } } - search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], account: account, options: options) { task in + search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], account: account, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) } completion: { account, files, data, error in - options.queue.async { completion(account, files?.first, data, error) } + options.queue.async { completion(account, files.first, data, error) } } } @@ -294,7 +292,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { if let httpBody = requestBody.data(using: .utf8) { search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) @@ -312,10 +310,10 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { - guard let nkSession = nkCommonInstance.getSession(account: account), - let href = ("/files/" + nkSession.userId).urlEncoded else { - return options.queue.async { completion(account, nil, nil, .urlError) } + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { + let userId = self.nkCommonInstance.userId + guard let href = ("/files/" + userId).urlEncoded else { + return options.queue.async { completion(account, [], nil, .urlError) } } let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(createProperties: options.createProperties, removeProperties: options.removeProperties), href, depth, "%" + literal + "%") if let httpBody = requestBody.data(using: .utf8) { @@ -337,13 +335,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { - guard let nkSession = nkCommonInstance.getSession(account: account) else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { + let userId = self.nkCommonInstance.userId + let urlBase = self.nkCommonInstance.urlBase let files: [NKFile] = [] var greaterDateString: String?, lessDateString: String? - let href = "/files/" + nkSession.userId + path + let href = "/files/" + userId + path if let lessDate = lessDate as? Date { lessDateString = self.nkCommonInstance.convertDate(lessDate, format: "yyyy-MM-dd'T'HH:mm:ssZZZZZ") } else if let lessDate = lessDate as? Int { @@ -368,7 +365,7 @@ public extension NextcloudKit { } if let httpBody = requestBody.data(using: .utf8) { - search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in + search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) } completion: { account, files, data, error in options.queue.async { completion(account, files, data, error) } @@ -383,19 +380,17 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { - /// - options.contentType = "application/xml" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { + let user = self.nkCommonInstance.user + let userId = self.nkCommonInstance.userId + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav var files: [NKFile] = [] - guard let url = (serverUrl + "/" + nkSession.dav).encodedToUrl else { + guard let url = (serverUrl + "/" + dav).encodedToUrl else { return options.queue.async { completion(account, files, nil, .urlError) } } let method = HTTPMethod(rawValue: "SEARCH") + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: method, headers: headers) @@ -405,7 +400,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -418,7 +413,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -433,18 +428,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - /// - options.contentType = "application/xml" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } - } - let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + "/" + fileName + let userId = self.nkCommonInstance.userId + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav + let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + "/" + fileName guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -456,7 +448,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -478,20 +470,18 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { - /// - options.contentType = "application/xml" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } - let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { + let user = self.nkCommonInstance.user + let userId = self.nkCommonInstance.userId + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav + let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId var files: [NKFile] = [] guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, files, nil, .urlError) } } let method = HTTPMethod(rawValue: "REPORT") + let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -502,7 +492,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -515,7 +505,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -529,15 +519,11 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ items: [NKTrash]?, _ data: Data?, _ error: NKError) -> Void) { - /// - options.contentType = "application/xml" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } - var serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/trashbin/" + nkSession.userId + "/trash/" + completion: @escaping (_ account: String, _ items: [NKTrash], _ data: Data?, _ error: NKError) -> Void) { + let userId = self.nkCommonInstance.userId + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav + var serverUrlFileName = urlBase + "/" + dav + "/trashbin/" + userId + "/trash/" if let filename { serverUrlFileName = serverUrlFileName + filename } @@ -546,6 +532,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, items, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPFIND") + var headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") headers.update(name: "Depth", value: "1") var urlRequest: URLRequest @@ -557,7 +544,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, items, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -570,7 +557,7 @@ public extension NextcloudKit { options.queue.async { completion(account, items, nil, error) } case .success: if let xmlData = response.data { - items = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataTrash(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles) + items = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataTrash(xmlData: xmlData, urlBase: urlBase, showHiddenFiles: showHiddenFiles) options.queue.async { completion(account, items, xmlData, .success) } } else { options.queue.async { completion(account, items, nil, .xmlError) } diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 4213e163..351d98e7 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -34,6 +34,22 @@ open class NextcloudKit: SessionDelegate { let instance = NextcloudKit() return instance }() + internal lazy var internalSessionManager: Alamofire.Session = { + return Alamofire.Session(configuration: nkCommonInstance.sessionConfiguration, + delegate: self, + rootQueue: nkCommonInstance.rootQueue, + startRequestsImmediately: true, + requestQueue: nkCommonInstance.requestQueue, + serializationQueue: nkCommonInstance.serializationQueue, + interceptor: nil, + serverTrustManager: nil, + redirectHandler: nil, + cachedResponseHandler: nil, + eventMonitors: [AlamofireLogger(nkCommonInstance: self.nkCommonInstance)]) + }() + public var sessionManager: Alamofire.Session { + return internalSessionManager + } #if !os(watchOS) private let reachabilityManager = Alamofire.NetworkReachabilityManager() #endif @@ -55,58 +71,50 @@ open class NextcloudKit: SessionDelegate { // MARK: - Setup - public func setup(delegate: NextcloudKitDelegate?) { - self.nkCommonInstance.delegate = delegate + public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, userAgent: String, nextcloudVersion: Int, groupIdentifier: String? = nil, delegate: NKCommonDelegate?) { + self.setup(account: account, user: user, userId: userId, password: password, urlBase: urlBase, groupIdentifier: groupIdentifier) + self.setup(userAgent: userAgent) + self.setup(nextcloudVersion: nextcloudVersion) + self.setup(delegate: delegate) } - public func appendAccount(_ account: String, - urlBase: String, - user: String, - userId: String, - password: String, - userAgent: String, - nextcloudVersion: Int, - groupIdentifier: String) { - if nkCommonInstance.nksessions.filter({ $0.account == account }).first != nil { - return updateAccount(account, urlBase: urlBase, userId: userId, password: password, userAgent: userAgent, nextcloudVersion: nextcloudVersion) + public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, groupIdentifier: String? = nil) { + self.nkCommonInstance._groupIdentifier = groupIdentifier + if (self.nkCommonInstance.account != account) || (self.nkCommonInstance.urlBase != urlBase && self.nkCommonInstance.user != user) { + if let cookieStore = sessionManager.session.configuration.httpCookieStorage { + for cookie in cookieStore.cookies ?? [] { + cookieStore.deleteCookie(cookie) + } + } + self.nkCommonInstance.internalTypeIdentifiers = [] } - let nkSession = NKSession(urlBase: urlBase, user: user, userId: userId, password: password, account: account, userAgent: userAgent, nextcloudVersion: nextcloudVersion, groupIdentifier: groupIdentifier) - nkCommonInstance.nksessions.append(nkSession) + if let account { + self.nkCommonInstance._account = account + } else { + self.nkCommonInstance._account = "" + } + self.nkCommonInstance._user = user + self.nkCommonInstance._userId = userId + self.nkCommonInstance._password = password + self.nkCommonInstance._urlBase = urlBase } - public func removeAccount(_ account: String) { - if let index = nkCommonInstance.nksessions.index(where: { $0.account == account}) { - nkCommonInstance.nksessions.remove(at: index) - } + public func setup(delegate: NKCommonDelegate?) { + self.nkCommonInstance.delegate = delegate } - public func updateAccount(_ account: String, - urlBase: String? = nil, - userId: String? = nil, - password: String? = nil, - userAgent: String? = nil, - nextcloudVersion: Int? = nil) { - guard let session = nkCommonInstance.nksessions.filter({ $0.account == account }).first else { return } - if let urlBase { - session.urlBase = urlBase - } - if let userId { - session.userId = userId - } - if let password { - session.password = password - } - if let userAgent { - session.userAgent = userAgent - } - if let nextcloudVersion { - session.nextcloudVersion = nextcloudVersion - } + public func setup(userAgent: String) { + self.nkCommonInstance._userAgent = userAgent + } + + public func setup(nextcloudVersion: Int) { + self.nkCommonInstance._nextcloudVersion = nextcloudVersion } /* internal func saveCookies(response : HTTPURLResponse?) { + if let headerFields = response?.allHeaderFields as? [String : String] { if let url = URL(string: self.nkCommonInstance.urlBase) { let HTTPCookie = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url) @@ -120,6 +128,7 @@ open class NextcloudKit: SessionDelegate { } internal func injectsCookies() { + if let cookies = cookies[self.nkCommonInstance.account] { if let url = URL(string: self.nkCommonInstance.urlBase) { sessionManager.session.configuration.httpCookieStorage?.setCookies(cookies, for: url, mainDocumentURL: nil) @@ -136,7 +145,6 @@ open class NextcloudKit: SessionDelegate { } private func startNetworkReachabilityObserver() { - /* reachabilityManager?.startListening(onUpdatePerforming: { status in switch status { case .unknown: @@ -149,7 +157,6 @@ open class NextcloudKit: SessionDelegate { self.nkCommonInstance.delegate?.networkReachabilityObserver(NKCommon.TypeReachability.reachableCellular) } }) - */ } private func stopNetworkReachabilityObserver() { @@ -157,7 +164,15 @@ open class NextcloudKit: SessionDelegate { } #endif + // MARK: - Session utility + + public func getSessionManager() -> URLSession { + return sessionManager.session + } + /* + //MARK: - + private func makeEvents() -> [EventMonitor] { let events = ClosureEventMonitor() @@ -194,10 +209,9 @@ open class NextcloudKit: SessionDelegate { } else if serverUrlFileName is String || serverUrlFileName is NSString { convertible = (serverUrlFileName as? String)?.encodedToUrl } - guard let url = convertible, - let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completionHandler(account, nil, nil, 0, nil, nil, .urlError) } + guard let url = convertible else { + options.queue.async { completionHandler(account, nil, nil, 0, nil, nil, .urlError) } + return } var destination: Alamofire.DownloadRequest.Destination? let fileNamePathLocalDestinationURL = NSURL.fileURL(withPath: fileNameLocalPath) @@ -205,8 +219,9 @@ open class NextcloudKit: SessionDelegate { return (fileNamePathLocalDestinationURL, [.removePreviousFile, .createIntermediateDirectories]) } destination = destinationFile + let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let request = nkSession.sessionData.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let request = sessionManager.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } } .downloadProgress { progress in @@ -261,12 +276,12 @@ open class NextcloudKit: SessionDelegate { } else if serverUrlFileName is String || serverUrlFileName is NSString { convertible = (serverUrlFileName as? String)?.encodedToUrl } - guard let url = convertible, - let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, nil, .urlError) } + guard let url = convertible else { + options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, nil, .urlError) } + return } let fileNameLocalPathUrl = URL(fileURLWithPath: fileNameLocalPath) + var headers = self.nkCommonInstance.getStandardHeaders(options: options) // Epoch of linux do not permitted negativ value if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { headers.update(name: "X-OC-CTime", value: "\(dateCreationFile.timeIntervalSince1970)") @@ -276,7 +291,7 @@ open class NextcloudKit: SessionDelegate { headers.update(name: "X-OC-MTime", value: "\(dateModificationFile.timeIntervalSince1970)") } - let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in + let request = sessionManager.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } }) .uploadProgress { progress in @@ -347,13 +362,12 @@ open class NextcloudKit: SessionDelegate { progressHandler: @escaping (_ totalBytesExpected: Int64, _ totalBytes: Int64, _ fractionCompleted: Double) -> Void = { _, _, _ in }, uploaded: @escaping (_ fileChunk: (fileName: String, size: Int64)) -> Void = { _ in }, completion: @escaping (_ account: String, _ filesChunk: [(fileName: String, size: Int64)]?, _ file: NKFile?, _ afError: AFError?, _ error: NKError) -> Void) { - - guard let nkSession = nkCommonInstance.getSession(account: account) else { - return completion(account, nil, nil, nil, .urlError) - } + let userId = self.nkCommonInstance.userId + let urlBase = self.nkCommonInstance.urlBase + let dav = self.nkCommonInstance.dav let fileNameLocalSize = self.nkCommonInstance.getFileSize(filePath: directory + "/" + fileName) - let serverUrlChunkFolder = nkSession.urlBase + "/" + nkSession.dav + "/uploads/" + nkSession.userId + "/" + chunkFolder - let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl, urlBase: nkSession.urlBase, userId: nkSession.userId) + "/" + fileName + let serverUrlChunkFolder = urlBase + "/" + dav + "/uploads/" + userId + "/" + chunkFolder + let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl) + "/" + fileName if options.customHeader == nil { options.customHeader = [:] } @@ -478,12 +492,14 @@ open class NextcloudKit: SessionDelegate { let assembleTimeMax: Double = 30 * 60 // 30 min options.timeout = max(assembleTimeMin, min(assembleTimePerGB * assembleSizeInGB, assembleTimeMax)) - self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account,options: options) { _, error in + self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account, options: options) { _, error in guard error == .success else { return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) } + self.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account, options: NKRequestOptions(queue: self.nkCommonInstance.backgroundQueue)) { _, files, _, error in - guard error == .success, let file = files?.first else { + + guard error == .success, let file = files.first else { return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) } return completion(account, filesChunkOutput, file, nil, error) diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 9a304eb5..3ab08a12 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -36,28 +36,26 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, public func download(serverUrlFileName: Any, fileNameLocalPath: String, taskDescription: String? = nil, - account: String) -> URLSessionDownloadTask? { + account: String, + session: URLSession) -> URLSessionDownloadTask? { var url: URL? if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { url = (serverUrlFileName as? String)?.encodedToUrl as? URL } - guard let nkSession = nkCommonInstance.getSession(account: account) else { - return nil - } guard let urlForRequest = url else { return nil } var request = URLRequest(url: urlForRequest) - let loginString = "\(nkSession.user):\(nkSession.password)" + let loginString = "\(self.nkCommonInstance.user):\(self.nkCommonInstance.password)" guard let loginData = loginString.data(using: String.Encoding.utf8) else { return nil } let base64LoginString = loginData.base64EncodedString() - request.setValue(nkSession.userAgent, forHTTPHeaderField: "User-Agent") + request.setValue(self.nkCommonInstance.userAgent, forHTTPHeaderField: "User-Agent") request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") - let task = nkSession.sessionDownloadBackground.downloadTask(with: request) + let task = session.downloadTask(with: request) task.taskDescription = taskDescription task.resume() self.nkCommonInstance.writeLog("Network start download file: \(serverUrlFileName)") @@ -73,10 +71,8 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, dateModificationFile: Date?, taskDescription: String? = nil, account: String, - sessionIdentifier: String) -> URLSessionUploadTask? { + session: URLSession) -> URLSessionUploadTask? { var url: URL? - var uploadSession: URLSession? - guard let nkSession = nkCommonInstance.getSession(account: account) else { return nil } if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { @@ -86,14 +82,14 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, return nil } var request = URLRequest(url: urlForRequest) - let loginString = "\(nkSession.user):\(nkSession.password)" + let loginString = "\(self.nkCommonInstance.user):\(self.nkCommonInstance.password)" guard let loginData = loginString.data(using: String.Encoding.utf8) else { return nil } let base64LoginString = loginData.base64EncodedString() request.httpMethod = "PUT" - request.setValue(nkSession.userAgent, forHTTPHeaderField: "User-Agent") + request.setValue(self.nkCommonInstance.userAgent, forHTTPHeaderField: "User-Agent") request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") // Epoch of linux do not permitted negativ value if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { @@ -104,18 +100,11 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, request.setValue("\(dateModificationFile.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-MTime") } - if sessionIdentifier == nkCommonInstance.identifierSessionUploadBackground { - uploadSession = nkSession.sessionUploadBackground - } else if sessionIdentifier == nkCommonInstance.identifierSessionUploadBackgroundWWan { - uploadSession = nkSession.sessionUploadBackgroundWWan - } else if sessionIdentifier == nkCommonInstance.identifierSessionUploadBackgroundExt { - uploadSession = nkSession.sessionUploadBackgroundExt - } - - let task = uploadSession?.uploadTask(with: request, fromFile: URL(fileURLWithPath: fileNameLocalPath)) - task?.taskDescription = taskDescription - task?.resume() + let task = session.uploadTask(with: request, fromFile: URL(fileURLWithPath: fileNameLocalPath)) + task.taskDescription = taskDescription + task.resume() self.nkCommonInstance.writeLog("Network start upload file: \(serverUrlFileName)") + return task } diff --git a/Sources/NextcloudKit/Utility/ThreadSafeArray.swift b/Sources/NextcloudKit/Utility/ThreadSafeArray.swift deleted file mode 100644 index acd6f736..00000000 --- a/Sources/NextcloudKit/Utility/ThreadSafeArray.swift +++ /dev/null @@ -1,353 +0,0 @@ -// -// ThreadSafeArray.swift -// Nextcloud -// -// Created by Marino Faggiana on 31/01/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// http://basememara.com/creating-thread-safe-arrays-in-swift/ -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import Foundation - -/// A thread-safe array. -public class ThreadSafeArray { - - private var array = [Element]() - private let queue = DispatchQueue(label: "com.nextcloud.ThreadSafeArray", attributes: .concurrent) - - public init() { } - - public convenience init(_ array: [Element]) { - self.init() - self.array = array - } -} - -// MARK: - Properties - -public extension ThreadSafeArray { - - /// The first element of the collection. - var first: Element? { - var result: Element? - queue.sync { result = self.array.first } - return result - } - - /// The last element of the collection. - var last: Element? { - var result: Element? - queue.sync { result = self.array.last } - return result - } - - /// The number of elements in the array. - var count: Int { - var result = 0 - queue.sync { result = self.array.count } - return result - } - - /// A Boolean value indicating whether the collection is empty. - var isEmpty: Bool { - var result = false - queue.sync { result = self.array.isEmpty } - return result - } - - /// A textual representation of the array and its elements. - var description: String { - var result = "" - queue.sync { result = self.array.description } - return result - } -} - -// MARK: - Immutable - -public extension ThreadSafeArray { - - /// Returns the first element of the sequence that satisfies the given predicate. - /// - /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. - /// - Returns: The first element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. - func first(where predicate: (Element) -> Bool) -> Element? { - var result: Element? - queue.sync { result = self.array.first(where: predicate) } - return result - } - - /// Returns the last element of the sequence that satisfies the given predicate. - /// - /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. - /// - Returns: The last element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. - func last(where predicate: (Element) -> Bool) -> Element? { - var result: Element? - queue.sync { result = self.array.last(where: predicate) } - return result - } - - /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate. - /// - /// - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array. - /// - Returns: An array of the elements that includeElement allowed. - func filter(_ isIncluded: @escaping (Element) -> Bool) -> ThreadSafeArray { - var result: ThreadSafeArray? - queue.sync { result = ThreadSafeArray(self.array.filter(isIncluded)) } - return result! - } - - /// Returns the first index in which an element of the collection satisfies the given predicate. - /// - /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match. - /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil. - func index(where predicate: (Element) -> Bool) -> Int? { - var result: Int? - queue.sync { result = self.array.firstIndex(where: predicate) } - return result - } - - /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements. - /// - /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false. - /// - Returns: A sorted array of the collection’s elements. - func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> ThreadSafeArray { - var result: ThreadSafeArray? - queue.sync { result = ThreadSafeArray(self.array.sorted(by: areInIncreasingOrder)) } - return result! - } - - /// Returns an array containing the results of mapping the given closure over the sequence’s elements. - /// - /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. - /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. - func map(_ transform: @escaping (Element) -> ElementOfResult) -> [ElementOfResult] { - var result = [ElementOfResult]() - queue.sync { result = self.array.map(transform) } - return result - } - - /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. - /// - /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. - /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. - func compactMap(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] { - var result = [ElementOfResult]() - queue.sync { result = self.array.compactMap(transform) } - return result - } - - /// Returns the result of combining the elements of the sequence using the given closure. - /// - /// - Parameters: - /// - initialResult: The value to use as the initial accumulating value. initialResult is passed to nextPartialResult the first time the closure is executed. - /// - nextPartialResult: A closure that combines an accumulating value and an element of the sequence into a new accumulating value, to be used in the next call of the nextPartialResult closure or returned to the caller. - /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. - func reduce(_ initialResult: ElementOfResult, _ nextPartialResult: @escaping (ElementOfResult, Element) -> ElementOfResult) -> ElementOfResult { - var result: ElementOfResult? - queue.sync { result = self.array.reduce(initialResult, nextPartialResult) } - return result ?? initialResult - } - - /// Returns the result of combining the elements of the sequence using the given closure. - /// - /// - Parameters: - /// - initialResult: The value to use as the initial accumulating value. - /// - updateAccumulatingResult: A closure that updates the accumulating value with an element of the sequence. - /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. - func reduce(into initialResult: ElementOfResult, _ updateAccumulatingResult: @escaping (inout ElementOfResult, Element) -> Void) -> ElementOfResult { - var result: ElementOfResult? - queue.sync { result = self.array.reduce(into: initialResult, updateAccumulatingResult) } - return result ?? initialResult - } - - /// Calls the given closure on each element in the sequence in the same order as a for-in loop. - /// - /// - Parameter body: A closure that takes an element of the sequence as a parameter. - func forEach(_ body: (Element) -> Void) { - queue.sync { self.array.forEach(body) } - } - - /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate. - /// - /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match. - /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false. - func contains(where predicate: (Element) -> Bool) -> Bool { - var result = false - queue.sync { result = self.array.contains(where: predicate) } - return result - } - - /// Returns a Boolean value indicating whether every element of a sequence satisfies a given predicate. - /// - /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element satisfies a condition. - /// - Returns: true if the sequence contains only elements that satisfy predicate; otherwise, false. - func allSatisfy(_ predicate: (Element) -> Bool) -> Bool { - var result = false - queue.sync { result = self.array.allSatisfy(predicate) } - return result - } - - /// Returns the array - /// - /// - Returns: the array part. - func getArray() -> [Element]? { - var results: [Element]? - queue.sync { results = self.array } - return results - } -} - -// MARK: - Mutable - -public extension ThreadSafeArray { - - /// Adds a new element at the end of the array. - /// - /// - Parameter element: The element to append to the array. - func append(_ element: Element) { - queue.async(flags: .barrier) { - self.array.append(element) - } - } - - /// Adds new elements at the end of the array. - /// - /// - Parameter element: The elements to append to the array. - func append(_ elements: [Element]) { - queue.async(flags: .barrier) { - self.array += elements - } - } - - /// Inserts a new element at the specified position. - /// - /// - Parameters: - /// - element: The new element to insert into the array. - /// - index: The position at which to insert the new element. - func insert(_ element: Element, at index: Int) { - queue.async(flags: .barrier) { - self.array.insert(element, at: index) - } - } - - /// Removes and returns the element at the specified position. - /// - /// - Parameters: - /// - index: The position of the element to remove. - /// - completion: The handler with the removed element. - func remove(at index: Int, completion: ((Element) -> Void)? = nil) { - queue.async(flags: .barrier) { - let element = self.array.remove(at: index) - DispatchQueue.main.async { completion?(element) } - } - } - - /// Removes and returns the elements that meet the criteria. - /// - /// - Parameters: - /// - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. - /// - completion: The handler with the removed elements. - func remove(where predicate: @escaping (Element) -> Bool, completion: (([Element]) -> Void)? = nil) { - queue.async(flags: .barrier) { - var elements = [Element]() - - while let index = self.array.firstIndex(where: predicate) { - elements.append(self.array.remove(at: index)) - } - - DispatchQueue.main.async { completion?(elements) } - } - } - - /// Removes all elements from the array. - /// - /// - Parameter completion: The handler with the removed elements. - func removeAll(completion: (([Element]) -> Void)? = nil) { - queue.async(flags: .barrier) { - let elements = self.array - self.array.removeAll() - DispatchQueue.main.async { completion?(elements) } - } - } -} - -public extension ThreadSafeArray { - - /// Accesses the element at the specified position if it exists. - /// - /// - Parameter index: The position of the element to access. - /// - Returns: optional element if it exists. - subscript(index: Int) -> Element? { - get { - var result: Element? - - queue.sync { - guard self.array.startIndex.. Bool { - var result = false - queue.sync { result = self.array.contains(element) } - return result - } -} - -// MARK: - Infix operators - -public extension ThreadSafeArray { - - /// Adds a new element at the end of the array. - /// - /// - Parameters: - /// - left: The collection to append to. - /// - right: The element to append to the array. - static func += (left: inout ThreadSafeArray, right: Element) { - left.append(right) - } - - /// Adds new elements at the end of the array. - /// - /// - Parameters: - /// - left: The collection to append to. - /// - right: The elements to append to the array. - static func += (left: inout ThreadSafeArray, right: [Element]) { - left.append(right) - } -} diff --git a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift index d30420ae..00b57c56 100644 --- a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift @@ -29,7 +29,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { let serverUrl = "\(baseUrl)/remote.php/dav/files/\(userId)" let serverUrlFileName = "\(serverUrl)/\(folderName)" - NextcloudKit.shared.appendAccount(account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") + NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: baseUrl) // Test creating folder NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in @@ -45,7 +45,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { XCTAssertEqual(self.account, account) XCTAssertEqual(NKError.success.errorCode, error.errorCode) XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) - XCTAssertEqual(files?[0].fileName, folderName) + XCTAssertEqual(files[0].fileName, folderName) Thread.sleep(forTimeInterval: 0.2) @@ -63,7 +63,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { XCTAssertEqual(404, error.errorCode) XCTAssertEqual(self.account, account) - XCTAssertTrue(files?.isEmpty ?? false) + XCTAssertTrue(files.isEmpty) } } } diff --git a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift index 78f64141..1da28e7e 100644 --- a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift @@ -21,7 +21,6 @@ // import XCTest -import Alamofire @testable import NextcloudKit final class ShareIntegrationTests: BaseIntegrationXCTestCase { @@ -32,7 +31,7 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { let serverUrl = "\(baseUrl)/remote.php/dav/files/\(userId)" let serverUrlFileName = "\(serverUrl)/\(folderName)" - NextcloudKit.shared.appendAccount(account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") + NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: baseUrl) NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in XCTAssertEqual(self.account, account) @@ -44,7 +43,7 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { let note = "Test note" - NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: "") { account, share, data, error in + NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: account) { account, share, data, error in defer { expectation.fulfill() } XCTAssertEqual(self.account, account) diff --git a/Tests/NextcloudKitUnitTests/LoginUnitTests.swift b/Tests/NextcloudKitUnitTests/LoginUnitTests.swift index 524ab820..a6940d35 100644 --- a/Tests/NextcloudKitUnitTests/LoginUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/LoginUnitTests.swift @@ -40,7 +40,7 @@ class LoginUnitTests: XCTestCase { override func setUp() { // Set our mock session manager as the one the API is going to use - // NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() + NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() } // Format of function names should be: func test_functionName_withCircumstances_shouldExpectation() {} @@ -59,7 +59,7 @@ class LoginUnitTests: XCTestCase { } mock.register() - //NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() + NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() // Now we call the function we want to test; it will use the mock session and request and return the mock data NextcloudKit.shared.getLoginFlowV2(serverUrl: serverUrl) { token, endpoint, login, data, error in @@ -109,7 +109,7 @@ class LoginUnitTests: XCTestCase { mock.register() // Set our mock session manager as the one the API is going to use - // NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() + NextcloudKit.shared.nkCommonInstance.sessionConfiguration = mockSessionManager() // Now we call the function we want to test; it will use the mock session and request and return the mock data NextcloudKit.shared.getLoginFlowV2(serverUrl: "badUrl") { token, endpoint, login, data, error in From f8324b8eb10a5e19f1f78ca1107725ea924b7e01 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 6 Aug 2024 09:53:51 +0200 Subject: [PATCH 053/135] Filename validator (#83) * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * cleanup Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * Make singleton Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev --------- Signed-off-by: Milen Pivchev --- Package.swift | 2 +- .../Extensions/String+Extension.swift | 8 ++ Sources/NextcloudKit/NKCommon.swift | 2 +- Sources/NextcloudKit/NKModel.swift | 4 +- .../NextcloudKit/NextcloudKit+WebDAV.swift | 6 + Sources/NextcloudKit/NextcloudKit.swift | 23 ---- .../Utils/FileNameValidator.swift | 122 ++++++++++++++++++ .../FileNameValidatorTests.swift | 69 ++++++++++ 8 files changed, 209 insertions(+), 27 deletions(-) create mode 100644 Sources/NextcloudKit/Utils/FileNameValidator.swift create mode 100644 Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift diff --git a/Package.swift b/Package.swift index 4ef04b99..a69e3a09 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "NextcloudKit", platforms: [ .macOS(.v10_15), - .iOS(.v10), + .iOS(.v12), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1) diff --git a/Sources/NextcloudKit/Extensions/String+Extension.swift b/Sources/NextcloudKit/Extensions/String+Extension.swift index a205a436..805babb7 100644 --- a/Sources/NextcloudKit/Extensions/String+Extension.swift +++ b/Sources/NextcloudKit/Extensions/String+Extension.swift @@ -38,4 +38,12 @@ extension String { public var asUrl: URLConvertible? { return try? asURL() } + + public var withRemovedFileExtension: String { + return String(NSString(string: self).deletingPathExtension) + } + + public var fileExtension: String { + return String(NSString(string: self).pathExtension) + } } diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 2b9795a8..f737c996 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -277,7 +277,7 @@ public class NKCommon: NSObject { if let inUTI = inUTI { typeIdentifier = inUTI as String - fileNameWithoutExt = (fileName as NSString).deletingPathExtension + fileNameWithoutExt = fileName.withRemovedFileExtension // contentType detect if mimeType.isEmpty { diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index c1f4168c..71e05185 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -873,14 +873,14 @@ class NKDataFileXML: NSObject { // Live photo detect files = files.sorted { - return ($0.serverUrl, ($0.fileName as NSString).deletingPathExtension, $0.classFile) < ($1.serverUrl, ($1.fileName as NSString).deletingPathExtension, $1.classFile) + return ($0.serverUrl, $0.fileName.withRemovedFileExtension, $0.classFile) < ($1.serverUrl, $1.fileName.withRemovedFileExtension, $1.classFile) } for index in files.indices { if !files[index].livePhotoFile.isEmpty || files[index].directory { continue } if index < files.count - 1, - (files[index].fileName as NSString).deletingPathExtension == (files[index + 1].fileName as NSString) .deletingPathExtension, + files[index].fileName.withRemovedFileExtension == files[index + 1].fileName.withRemovedFileExtension, files[index].classFile == NKCommon.TypeClassFile.image.rawValue, files[index + 1].classFile == NKCommon.TypeClassFile.video.rawValue { files[index].livePhotoFile = files[index + 1].fileId diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 61d79eb4..31104440 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -31,6 +31,8 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ error: NKError) -> Void) { + let account = self.nkCommonInstance.account + guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -113,6 +115,8 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let account = self.nkCommonInstance.account + guard let url = serverUrlFileNameSource.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } @@ -157,6 +161,8 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { + let account = self.nkCommonInstance.account + guard let url = serverUrlFileNameSource.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 351d98e7..e936cecd 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -170,29 +170,6 @@ open class NextcloudKit: SessionDelegate { return sessionManager.session } - /* - //MARK: - - - private func makeEvents() -> [EventMonitor] { - - let events = ClosureEventMonitor() - events.requestDidFinish = { request in - print("Request finished \(request)") - } - events.taskDidComplete = { session, task, error in - print("Request failed \(session) \(task) \(String(describing: error))") - /* - if let urlString = (error as NSError?)?.userInfo["NSErrorFailingURLStringKey"] as? String, - let resumedata = (error as NSError?)?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data { - print("Found resume data for url \(urlString)") - //self.startDownload(urlString, resumeData: resumedata) - } - */ - } - return [events] - } - */ - // MARK: - download / upload public func download(serverUrlFileName: Any, diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift new file mode 100644 index 00000000..70bfff90 --- /dev/null +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -0,0 +1,122 @@ +// +// FileNameValidator.swift +// +// +// Created by Milen Pivchev on 12.07.24. +// Copyright © 2024 Milen Pivchev. All rights reserved. +// +// Author: Milen Pivchev +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation + +public class FileNameValidator { + public static let shared: FileNameValidator = { + let instance = FileNameValidator() + return instance + }() + + public var forbiddenFileNames: [String] = [] { + didSet { + forbiddenFileNames = forbiddenFileNames.map({$0.uppercased()}) + } + } + public var forbiddenFileNameBasenames: [String] = [] { + didSet { + forbiddenFileNameBasenames = forbiddenFileNameBasenames.map({$0.uppercased()}) + } + } + + private var forbiddenFileNameCharactersRegex: NSRegularExpression? + + public var forbiddenFileNameCharacters: [String] = [] { + didSet { + forbiddenFileNameCharactersRegex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]") + } + } + + public var forbiddenFileNameExtensions: [String] = [] { + didSet { + forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.uppercased()}) + } + } + + public let fileEndsWithSpacePeriodError = NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: NSLocalizedString("_file_name_validator_error_ends_with_space_period_", value: "File name ends with a space or a period", comment: "")) + + public var fileReservedNameError: NKError { + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_reserved_name_", value: "%@ is a reserved name", comment: "") + let errorMessage = String(format: errorMessageTemplate, templateString) + return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) + } + + public var fileForbiddenFileExtensionError: NKError { + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: ".%@ is a forbidden file extension", comment: "") + let errorMessage = String(format: errorMessageTemplate, templateString) + return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) + } + + public var fileInvalidCharacterError: NKError { + let errorMessageTemplate = NSLocalizedString("file_name_validator_error_invalid_character_", value: "File name contains an invalid character: %@", comment: "") + let errorMessage = String(format: errorMessageTemplate, templateString) + return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) + } + + private var templateString = "" + + private init() {} + + public func setup(forbiddenFileNames: [String], forbiddenFileNameBasenames: [String], forbiddenFileNameCharacters: [String], forbiddenFileNameExtensions: [String]) { + self.forbiddenFileNames = forbiddenFileNames + self.forbiddenFileNameBasenames = forbiddenFileNameBasenames + self.forbiddenFileNameCharacters = forbiddenFileNameCharacters + self.forbiddenFileNameExtensions = forbiddenFileNameExtensions + } + + public func checkFileName(_ filename: String, existedFileNames: Set? = nil) -> NKError? { + if filename.hasSuffix(" ") || filename.hasSuffix(".") { + return fileEndsWithSpacePeriodError + } + + if let invalidCharacterError = checkInvalidCharacters(string: filename) { + return invalidCharacterError + } + + if forbiddenFileNames.contains(filename.uppercased()) || forbiddenFileNames.contains(filename.withRemovedFileExtension.uppercased()) || + forbiddenFileNameBasenames.contains(filename.uppercased()) || forbiddenFileNameBasenames.contains(filename.withRemovedFileExtension.uppercased()) { + templateString = filename + return fileReservedNameError + } + + if forbiddenFileNameExtensions.contains(where: { filename.uppercased().hasSuffix($0.uppercased()) }) { + templateString = filename.fileExtension + return fileForbiddenFileExtensionError + } + + return nil + } + + private func checkInvalidCharacters(string: String) -> NKError? { + for char in string { + let charAsString = String(char) + let range = NSRange(location: 0, length: charAsString.utf16.count) + + if forbiddenFileNameCharactersRegex?.firstMatch(in: charAsString, options: [], range: range) != nil { + templateString = charAsString + return fileInvalidCharacterError + } + } + return nil + } +} diff --git a/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift b/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift new file mode 100644 index 00000000..f49b2cbd --- /dev/null +++ b/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift @@ -0,0 +1,69 @@ +// +// fileNameValidatorTests.swift +// +// +// Created by Milen Pivchev on 12.07.24. +// Copyright © 2024 Milen Pivchev. All rights reserved. +// +// Author: Milen Pivchev +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import XCTest +@testable import NextcloudKit + +class FileNameValidatorTests: XCTestCase { + let fileNameValidator = FileNameValidator.shared + + override func setUp() { + fileNameValidator.setup( + forbiddenFileNames: [".htaccess",".htaccess"], + forbiddenFileNameBasenames: ["con", "prn", "aux", "nul", "com0", "com1", "com2", "com3", "com4", + "com5", "com6", "com7", "com8", "com9", "com¹", "com²", "com³", + "lpt0", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", + "lpt8", "lpt9", "lpt¹", "lpt²", "lpt³"], + forbiddenFileNameCharacters: ["<", ">", ":", "\\\\", "/", "|", "?", "*", "&"], + forbiddenFileNameExtensions: [".filepart",".part"] + ) + super.setUp() + } + + func testInvalidCharacter() { + let result = fileNameValidator.checkFileName("file Date: Tue, 13 Aug 2024 13:47:28 +0200 Subject: [PATCH 054/135] fix session delegate Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+API.swift | 2 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index f7f3382e..25605def 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -57,7 +57,7 @@ public extension NextcloudKit { return options.queue.async { completion(.urlError) } } - AF.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 1bb056b1..280ab626 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -51,7 +51,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, nil, NKError(error: error)) } } - AF.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -130,7 +130,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - AF.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -167,7 +167,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - AF.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in From 0673cebbeb4d3b1f730ec750ec38e5b1bf75e3e5 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 13 Aug 2024 14:16:17 +0200 Subject: [PATCH 055/135] fix Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+API.swift | 2 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index f7f3382e..25605def 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -57,7 +57,7 @@ public extension NextcloudKit { return options.queue.async { completion(.urlError) } } - AF.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 1bb056b1..280ab626 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -51,7 +51,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, nil, NKError(error: error)) } } - AF.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -130,7 +130,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - AF.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -167,7 +167,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - AF.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in From e9c40dd9a86f68b9cb6dde36649f1f36cc1647dd Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 23 Aug 2024 10:14:16 +0200 Subject: [PATCH 056/135] Filename validator (#92) * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * cleanup Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * Make singleton Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev --------- Signed-off-by: Milen Pivchev Co-authored-by: Milen Pivchev --- .../Utils/FileNameValidator.swift | 24 ++++++---- .../FileNameValidatorTests.swift | 46 ++++++++++++++++++- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index 70bfff90..ed4437cb 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -28,12 +28,13 @@ public class FileNameValidator { return instance }() - public var forbiddenFileNames: [String] = [] { + public private(set) var forbiddenFileNames: [String] = [] { didSet { forbiddenFileNames = forbiddenFileNames.map({$0.uppercased()}) } } - public var forbiddenFileNameBasenames: [String] = [] { + + public private(set) var forbiddenFileNameBasenames: [String] = [] { didSet { forbiddenFileNameBasenames = forbiddenFileNameBasenames.map({$0.uppercased()}) } @@ -41,34 +42,34 @@ public class FileNameValidator { private var forbiddenFileNameCharactersRegex: NSRegularExpression? - public var forbiddenFileNameCharacters: [String] = [] { + public private(set) var forbiddenFileNameCharacters: [String] = [] { didSet { forbiddenFileNameCharactersRegex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]") } } - public var forbiddenFileNameExtensions: [String] = [] { + public private(set) var forbiddenFileNameExtensions: [String] = [] { didSet { forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.uppercased()}) } } - public let fileEndsWithSpacePeriodError = NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: NSLocalizedString("_file_name_validator_error_ends_with_space_period_", value: "File name ends with a space or a period", comment: "")) + public let fileEndsWithSpacePeriodError = NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: NSLocalizedString("_file_name_validator_error_ends_with_space_period_", value: "File name ends with a space or a period.", comment: "")) public var fileReservedNameError: NKError { - let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_reserved_name_", value: "%@ is a reserved name", comment: "") + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_reserved_name_", value: "\"%@\" is a forbidden name.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } public var fileForbiddenFileExtensionError: NKError { - let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: ".%@ is a forbidden file extension", comment: "") + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: ".\"%@\" is a forbidden file extension.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } public var fileInvalidCharacterError: NKError { - let errorMessageTemplate = NSLocalizedString("file_name_validator_error_invalid_character_", value: "File name contains an invalid character: %@", comment: "") + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_invalid_character_", value: "Name contains an invalid character: \"%@\".", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } @@ -84,7 +85,7 @@ public class FileNameValidator { self.forbiddenFileNameExtensions = forbiddenFileNameExtensions } - public func checkFileName(_ filename: String, existedFileNames: Set? = nil) -> NKError? { + public func checkFileName(_ filename: String) -> NKError? { if filename.hasSuffix(" ") || filename.hasSuffix(".") { return fileEndsWithSpacePeriodError } @@ -107,6 +108,11 @@ public class FileNameValidator { return nil } + public func checkFolderPath(folderPath: String) -> Bool { + return folderPath.split { $0 == "/" || $0 == "\\" } + .allSatisfy { checkFileName(String($0)) == nil } + } + private func checkInvalidCharacters(string: String) -> NKError? { for char in string { let charAsString = String(char) diff --git a/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift b/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift index f49b2cbd..8e2299ba 100644 --- a/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift +++ b/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift @@ -1,7 +1,7 @@ // // fileNameValidatorTests.swift -// -// +// +// // Created by Milen Pivchev on 12.07.24. // Copyright © 2024 Milen Pivchev. All rights reserved. // @@ -66,4 +66,46 @@ class FileNameValidatorTests: XCTestCase { let result = fileNameValidator.checkFileName("validFileName") XCTAssertNil(result?.errorDescription) } + + func testValidFolderAndFilePaths() { + let folderPath = "validFolder" + + let result = fileNameValidator.checkFolderPath(folderPath: folderPath) + XCTAssertTrue(result) + } + + func testFolderPathWithReservedName() { + let folderPath = "CON" + + let result = fileNameValidator.checkFolderPath(folderPath: folderPath) + XCTAssertFalse(result) + } + + func testFolderPathWithInvalidCharacter() { + let folderPath = "invalid Date: Mon, 26 Aug 2024 17:55:29 +0200 Subject: [PATCH 057/135] WIP Signed-off-by: Milen Pivchev --- .../Utils/FileNameValidator.swift | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index ed4437cb..f6ae2f9e 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -40,13 +40,9 @@ public class FileNameValidator { } } - private var forbiddenFileNameCharactersRegex: NSRegularExpression? +// private var forbiddenFileNameCharactersRegex: NSRegularExpression? - public private(set) var forbiddenFileNameCharacters: [String] = [] { - didSet { - forbiddenFileNameCharactersRegex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]") - } - } + public private(set) var forbiddenFileNameCharacters: [String] = [] public private(set) var forbiddenFileNameExtensions: [String] = [] { didSet { @@ -90,7 +86,30 @@ public class FileNameValidator { return fileEndsWithSpacePeriodError } - if let invalidCharacterError = checkInvalidCharacters(string: filename) { + if let regex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]"), let invalidCharacterError = checkInvalidCharacters(string: filename, regex: regex) { + return invalidCharacterError + } + + if forbiddenFileNames.contains(filename.uppercased()) || forbiddenFileNames.contains(filename.withRemovedFileExtension.uppercased()) || + forbiddenFileNameBasenames.contains(filename.uppercased()) || forbiddenFileNameBasenames.contains(filename.withRemovedFileExtension.uppercased()) { + templateString = filename + return fileReservedNameError + } + + if forbiddenFileNameExtensions.contains(where: { filename.uppercased().hasSuffix($0.uppercased()) }) { + templateString = filename.fileExtension + return fileForbiddenFileExtensionError + } + + return nil + } + + public func checkFileName(_ filename: String, forbiddenFileNames: [String]) -> NKError? { + if filename.hasSuffix(" ") || filename.hasSuffix(".") { + return fileEndsWithSpacePeriodError + } + + if let regex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]"), let invalidCharacterError = checkInvalidCharacters(string: filename, regex: regex) { return invalidCharacterError } @@ -113,12 +132,12 @@ public class FileNameValidator { .allSatisfy { checkFileName(String($0)) == nil } } - private func checkInvalidCharacters(string: String) -> NKError? { + private func checkInvalidCharacters(string: String, regex: NSRegularExpression) -> NKError? { for char in string { let charAsString = String(char) let range = NSRange(location: 0, length: charAsString.utf16.count) - if forbiddenFileNameCharactersRegex?.firstMatch(in: charAsString, options: [], range: range) != nil { + if regex.firstMatch(in: charAsString, options: [], range: range) != nil { templateString = charAsString return fileInvalidCharacterError } From d4219947aafe929829ab4af0c728ffa606c4af36 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 16:14:49 +0200 Subject: [PATCH 058/135] NextcloudKit Version 5 - Multisession (#94) --------- Signed-off-by: Marino Faggiana Signed-off-by: Milen Pivchev Co-authored-by: Milen Pivchev --- Package.swift | 2 +- Sources/NextcloudKit/NKCommon.swift | 213 +++---- Sources/NextcloudKit/NKError.swift | 1 + Sources/NextcloudKit/NKLogger.swift | 68 +++ Sources/NextcloudKit/NKModel.swift | 31 +- Sources/NextcloudKit/NKSession.swift | 117 ++++ Sources/NextcloudKit/NextcloudKit+API.swift | 263 ++++----- .../NextcloudKit/NextcloudKit+Download.swift | 92 +++ Sources/NextcloudKit/NextcloudKit+Login.swift | 15 +- .../NextcloudKit/NextcloudKit+Upload.swift | 282 ++++++++++ Sources/NextcloudKit/NextcloudKit.swift | 529 +++--------------- .../NextcloudKit/NextcloudKitBackground.swift | 4 + .../NextcloudKitSessionDelegate.swift | 60 ++ .../Utils/FileNameValidator.swift | 27 +- .../NextcloudKit/Utils/ThreadSafeArray.swift | 353 ++++++++++++ .../FilesIntegrationTests.swift | 6 +- .../ShareIntegrationTests.swift | 5 +- .../FileNameValidatorTests.swift | 12 +- 18 files changed, 1267 insertions(+), 813 deletions(-) create mode 100644 Sources/NextcloudKit/NKLogger.swift create mode 100644 Sources/NextcloudKit/NKSession.swift create mode 100644 Sources/NextcloudKit/NextcloudKit+Download.swift create mode 100644 Sources/NextcloudKit/NextcloudKit+Upload.swift create mode 100644 Sources/NextcloudKit/NextcloudKitSessionDelegate.swift create mode 100644 Sources/NextcloudKit/Utils/ThreadSafeArray.swift diff --git a/Package.swift b/Package.swift index a69e3a09..b9ba90ff 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "2.3.0")), - .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.4.1")), + .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.9.1")), .package(url: "https://github.com/SwiftyJSON/SwiftyJSON", .upToNextMajor(from: "5.0.0")), .package(url: "https://github.com/yahoojapan/SwiftyXMLParser", .upToNextMajor(from: "5.3.0")), ], diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index f737c996..d71e0974 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -30,7 +30,7 @@ import MobileCoreServices import CoreServices #endif -public protocol NKCommonDelegate { +public protocol NextcloudKitDelegate { func authenticationChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) @@ -40,15 +40,29 @@ public protocol NKCommonDelegate { func uploadProgress(_ progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String, session: URLSession, task: URLSessionTask) func downloadingFinish(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) - + func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: Date?, dateLastModified: Date?, length: Int64, task: URLSessionTask, error: NKError) func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: Date?, size: Int64, task: URLSessionTask, error: NKError) } public class NKCommon: NSObject { - public let dav: String = "remote.php/dav" - public let sessionIdentifierDownload: String = "com.nextcloud.nextcloudkit.session.download" - public let sessionIdentifierUpload: String = "com.nextcloud.nextcloudkit.session.upload" + public var nksessions = ThreadSafeArray() + public var delegate: NextcloudKitDelegate? + + public let identifierSessionDownload: String = "com.nextcloud.nextcloudkit.session.download" + public let identifierSessionUpload: String = "com.nextcloud.nextcloudkit.session.upload" + public let identifierSessionDownloadBackground: String = "com.nextcloud.session.downloadbackground" + public let identifierSessionUploadBackground: String = "com.nextcloud.session.uploadbackground" + public let identifierSessionUploadBackgroundWWan: String = "com.nextcloud.session.uploadbackgroundWWan" + public let identifierSessionUploadBackgroundExt: String = "com.nextcloud.session.uploadextension" + + public let rootQueue = DispatchQueue(label: "com.nextcloud.session.rootQueue") + public let requestQueue = DispatchQueue(label: "com.nextcloud.session.requestQueue") + public let serializationQueue = DispatchQueue(label: "com.nextcloud.session.serializationQueue") + public let backgroundQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.backgroundqueue", qos: .background, attributes: .concurrent) + private let logQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.queuelog", attributes: .concurrent ) + + public let notificationCenterChunkedFileStop = NSNotification.Name(rawValue: "NextcloudKit.chunkedFile.stop") public enum TypeReachability: Int { case unknown = 0 @@ -90,97 +104,36 @@ public class NKCommon: NSObject { var editor: String var iconName: String var name: String + var account: String } - public let notificationCenterChunkedFileStop = NSNotification.Name(rawValue: "NextcloudKit.chunkedFile.stop") - - internal lazy var sessionConfiguration: URLSessionConfiguration = { - let configuration = URLSessionConfiguration.af.default - configuration.requestCachePolicy = .reloadIgnoringLocalCacheData - if let groupIdentifier { - let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: groupIdentifier) - configuration.httpCookieStorage = cookieStorage - } else { - configuration.httpCookieStorage = nil - } - return configuration - }() - internal var rootQueue: DispatchQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.sessionManagerData.rootQueue") - internal var requestQueue: DispatchQueue? - internal var serializationQueue: DispatchQueue? - - internal var _user = "" - internal var _userId = "" - internal var _password = "" - internal var _account = "" - internal var _urlBase = "" - internal var _userAgent: String? - internal var _nextcloudVersion: Int = 0 - internal var _groupIdentifier: String? - - internal var internalTypeIdentifiers: [UTTypeConformsToServer] = [] internal var utiCache = NSCache() internal var mimeTypeCache = NSCache() internal var filePropertiesCache = NSCache() - internal var delegate: NKCommonDelegate? - - private var _filenameLog: String = "communication.log" - private var _pathLog: String = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! - private var _filenamePathLog: String = "" - private var _levelLog: Int = 0 - private var _printLog: Bool = true - private var _copyLogToDocumentDirectory: Bool = false - private let queueLog = DispatchQueue(label: "com.nextcloud.nextcloudkit.queuelog", attributes: .concurrent ) - - public var user: String { - return _user - } - - public var userId: String { - return _userId - } - - public var password: String { - return _password - } - - public var account: String { - return _account - } - - public var urlBase: String { - return _urlBase - } - - public var userAgent: String? { - return _userAgent - } - - public var nextcloudVersion: Int { - return _nextcloudVersion - } + internal var internalTypeIdentifiers: [UTTypeConformsToServer] = [] - public var groupIdentifier: String? { - return _groupIdentifier - } - - public let backgroundQueue = DispatchQueue(label: "com.nextcloud.nextcloudkit.backgroundqueue", qos: .background, attributes: .concurrent) + public var filenamePathLog: String = "" + public var levelLog: Int = 0 + public var copyLogToDocumentDirectory: Bool = false + public var printLog: Bool = true + private var internalFilenameLog: String = "communication.log" public var filenameLog: String { get { - return _filenameLog + return internalFilenameLog } set(newVal) { if !newVal.isEmpty { - _filenameLog = newVal - _filenamePathLog = _pathLog + "/" + _filenameLog + internalFilenameLog = newVal + internalFilenameLog = internalPathLog + "/" + internalFilenameLog } } } + private var internalPathLog: String = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! public var pathLog: String { get { - return _pathLog + return internalPathLog } set(newVal) { var tempVal = newVal @@ -188,77 +141,34 @@ public class NKCommon: NSObject { tempVal = String(tempVal.dropLast()) } if !tempVal.isEmpty { - _pathLog = tempVal - _filenamePathLog = _pathLog + "/" + _filenameLog + internalPathLog = tempVal + filenamePathLog = internalPathLog + "/" + internalFilenameLog } } } - public var filenamePathLog: String { - return _filenamePathLog - } - - public var levelLog: Int { - get { - return _levelLog - } - set(newVal) { - _levelLog = newVal - } - } - - public var printLog: Bool { - get { - return _printLog - } - set(newVal) { - _printLog = newVal - } - } - - public var copyLogToDocumentDirectory: Bool { - get { - return _copyLogToDocumentDirectory - } - set(newVal) { - _copyLogToDocumentDirectory = newVal - } - } - // MARK: - Init override init() { super.init() - _filenamePathLog = _pathLog + "/" + _filenameLog + filenamePathLog = internalPathLog + "/" + internalFilenameLog } // MARK: - Type Identifier - public func getInternalTypeIdentifier(typeIdentifier: String) -> [UTTypeConformsToServer] { - var results: [UTTypeConformsToServer] = [] - - for internalTypeIdentifier in internalTypeIdentifiers { - if internalTypeIdentifier.typeIdentifier == typeIdentifier { - results.append(internalTypeIdentifier) - } - } - return results + public func clearInternalTypeIdentifier(account: String) { + internalTypeIdentifiers = internalTypeIdentifiers.filter({ $0.account != account }) } - public func addInternalTypeIdentifier(typeIdentifier: String, classFile: String, editor: String, iconName: String, name: String) { - if !internalTypeIdentifiers.contains(where: { $0.typeIdentifier == typeIdentifier && $0.editor == editor}) { - let newUTI = UTTypeConformsToServer(typeIdentifier: typeIdentifier, classFile: classFile, editor: editor, iconName: iconName, name: name) + public func addInternalTypeIdentifier(typeIdentifier: String, classFile: String, editor: String, iconName: String, name: String, account: String) { + if !internalTypeIdentifiers.contains(where: { $0.typeIdentifier == typeIdentifier && $0.editor == editor && $0.account == account}) { + let newUTI = UTTypeConformsToServer(typeIdentifier: typeIdentifier, classFile: classFile, editor: editor, iconName: iconName, name: name, account: account) internalTypeIdentifiers.append(newUTI) } } - public func objcGetInternalType(fileName: String, mimeType: String, directory: Bool) -> [String: String] { - let results = getInternalType(fileName: fileName, mimeType: mimeType, directory: directory) - return ["mimeType": results.mimeType, "classFile": results.classFile, "iconName": results.iconName, "typeIdentifier": results.typeIdentifier, "fileNameWithoutExt": results.fileNameWithoutExt, "ext": results.ext] - } - - public func getInternalType(fileName: String, mimeType: String, directory: Bool) -> (mimeType: String, classFile: String, iconName: String, typeIdentifier: String, fileNameWithoutExt: String, ext: String) { + public func getInternalType(fileName: String, mimeType: String, directory: Bool, account: String) -> (mimeType: String, classFile: String, iconName: String, typeIdentifier: String, fileNameWithoutExt: String, ext: String) { var ext = (fileName as NSString).pathExtension.lowercased() var mimeType = mimeType var classFile = "", iconName = "", typeIdentifier = "", fileNameWithoutExt = "" @@ -277,7 +187,7 @@ public class NKCommon: NSObject { if let inUTI = inUTI { typeIdentifier = inUTI as String - fileNameWithoutExt = fileName.withRemovedFileExtension + fileNameWithoutExt = (fileName as NSString).deletingPathExtension // contentType detect if mimeType.isEmpty { @@ -486,41 +396,44 @@ public class NKCommon: NSObject { // MARK: - Common - public func getStandardHeaders(options: NKRequestOptions) -> HTTPHeaders { - return getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent, contentType: options.contentType) - } - - public func getStandardHeaders(_ appendHeaders: [String: String]? = nil, customUserAgent: String? = nil, contentType: String? = nil) -> HTTPHeaders { - return getStandardHeaders(user: user, password: password, appendHeaders: appendHeaders, customUserAgent: customUserAgent, contentType: contentType) + public func getSession(account: String) -> NKSession? { + var session: NKSession? + nksessions.forEach { result in + if result.account == account { + session = result + } + } + return session } - public func getStandardHeaders(user: String?, password: String?, appendHeaders: [String: String]?, customUserAgent: String?, contentType: String? = nil) -> HTTPHeaders { + public func getStandardHeaders(account: String, options: NKRequestOptions? = nil) -> HTTPHeaders? { + guard let session = nksessions.filter({ $0.account == account }).first else { return nil} var headers: HTTPHeaders = [] - if let user, let password { - headers.update(.authorization(username: user, password: password)) - } - if let customUserAgent { + headers.update(.authorization(username: session.user, password: session.password)) + headers.update(.userAgent(session.userAgent)) + if let customUserAgent = options?.customUserAgent { headers.update(.userAgent(customUserAgent)) - } else if let userAgent = userAgent { - headers.update(.userAgent(userAgent)) } - if let contentType { + if let contentType = options?.contentType { headers.update(.contentType(contentType)) } else { headers.update(.contentType("application/x-www-form-urlencoded")) } - if contentType != "application/xml" { + if options?.contentType != "application/xml" { headers.update(name: "Accept", value: "application/json") } headers.update(name: "OCS-APIRequest", value: "true") - for (key, value) in appendHeaders ?? [:] { + for (key, value) in options?.customHeader ?? [:] { headers.update(name: key, value: value) } return headers } - public func createStandardUrl(serverUrl: String, endpoint: String) -> URLConvertible? { + public func createStandardUrl(serverUrl: String, endpoint: String, options: NKRequestOptions) -> URLConvertible? { + if let endpoint = options.endpoint { + return URL(string: endpoint) + } guard var serverUrl = serverUrl.urlEncoded else { return nil } if serverUrl.last != "/" { serverUrl = serverUrl + "/" } @@ -586,7 +499,7 @@ public class NKCommon: NSObject { return 0 } - public func returnPathfromServerUrl(_ serverUrl: String) -> String { + public func returnPathfromServerUrl(_ serverUrl: String, urlBase: String, userId: String) -> String { let home = urlBase + "/remote.php/dav/files/" + userId return serverUrl.replacingOccurrences(of: home, with: "") } @@ -620,7 +533,7 @@ public class NKCommon: NSObject { if printLog { print(textToWrite) } if levelLog > 0 { - queueLog.async(flags: .barrier) { + logQueue.async(flags: .barrier) { self.writeLogToDisk(filename: self.filenamePathLog, text: textToWrite) if self.copyLogToDocumentDirectory, let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first { let filenameCopyToDocumentDirectory = path + "/" + self.filenameLog diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index 579b3000..f815bb9b 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -67,6 +67,7 @@ public class NKError: NSObject { public let responseData: Data? public static let urlError = NKError(errorCode: NSURLErrorBadURL, errorDescription: NSLocalizedString("_invalid_url_", value: "Invalid server url", comment: "")) + public static let invalidResponseError = NKError(errorCode: NSURLErrorBadServerResponse, errorDescription: NSLocalizedString("_error_response_", value: "Invalid response", comment: "")) public static let xmlError = NKError(errorCode: NSURLErrorBadServerResponse, errorDescription: NSLocalizedString("_error_decode_xml_", value: "Invalid response, error decoding XML", comment: "")) public static let invalidDate = NKError(errorCode: NSURLErrorBadServerResponse, errorDescription: NSLocalizedString("_invalid_date_format_", value: "Invalid date format", comment: "")) public static let invalidData = NKError(errorCode: NSURLErrorCannotDecodeContentData, errorDescription: NSLocalizedString("_invalid_data_format_", value: "Invalid data format", comment: "")) diff --git a/Sources/NextcloudKit/NKLogger.swift b/Sources/NextcloudKit/NKLogger.swift new file mode 100644 index 00000000..43fa09b1 --- /dev/null +++ b/Sources/NextcloudKit/NKLogger.swift @@ -0,0 +1,68 @@ +// +// NKLogger.swift +// NextcloudKit +// +// Created by Marino Faggiana on 16/08/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + + +import Foundation +import Alamofire + +final class NKLogger: EventMonitor { + let nkCommonInstance: NKCommon + + init(nkCommonInstance: NKCommon) { + self.nkCommonInstance = nkCommonInstance + } + + func requestDidResume(_ request: Request) { + if self.nkCommonInstance.levelLog > 0 { + self.nkCommonInstance.writeLog("Network request started: \(request)") + if self.nkCommonInstance.levelLog > 1 { + let allHeaders = request.request.flatMap { $0.allHTTPHeaderFields.map { $0.description } } ?? "None" + let body = request.request.flatMap { $0.httpBody.map { String(decoding: $0, as: UTF8.self) } } ?? "None" + + self.nkCommonInstance.writeLog("Network request headers: \(allHeaders)") + self.nkCommonInstance.writeLog("Network request body: \(body)") + } + } + } + + func request(_ request: DataRequest, didParseResponse response: AFDataResponse) { + guard let date = self.nkCommonInstance.convertDate(Date(), format: "yyyy-MM-dd' 'HH:mm:ss") else { return } + let responseResultString = String("\(response.result)") + let responseDebugDescription = String("\(response.debugDescription)") + let responseAllHeaderFields = String("\(String(describing: response.response?.allHeaderFields))") + + if self.nkCommonInstance.levelLog > 0 { + if self.nkCommonInstance.levelLog == 1 { + if let request = response.request { + let requestString = "\(request)" + self.nkCommonInstance.writeLog("Network response request: " + requestString + ", result: " + responseResultString) + } else { + self.nkCommonInstance.writeLog("Network response result: " + responseResultString) + } + } else { + self.nkCommonInstance.writeLog("Network response result: \(date) " + responseDebugDescription) + self.nkCommonInstance.writeLog("Network response all headers: \(date) " + responseAllHeaderFields) + } + } + } +} diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 71e05185..948a4c84 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -74,8 +74,11 @@ public enum NKProperties: String, CaseIterable { case systemtags = "" case filemetadatasize = "" case filemetadatagps = "" - case metadataphotossize = "" + case metadataphotosexif = "" case metadataphotosgps = "" + case metadataphotosoriginaldatetime = "" + case metadataphotoplace = "" + case metadataphotossize = "" case metadatafileslivephoto = "" case hidden = "" /// open-collaboration-services.org @@ -228,6 +231,15 @@ public class NKFile: NSObject { public var livePhotoFile = "" /// Indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side public var isFlaggedAsLivePhotoByServer = false + /// + public var datePhotosOriginal: Date? + /// + public struct ChildElement { + let name: String + let text: String? + } + public var exifPhotos = [[String: String?]]() + public var placePhotos: String? } public class NKFileProperty: NSObject { @@ -858,7 +870,20 @@ class NKDataFileXML: NSObject { file.hidden = (hidden as NSString).boolValue } - let results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory) + if let datePhotosOriginal = propstat["d:prop", "nc:metadata-photos-original_date_time"].double, datePhotosOriginal > 0 { + file.datePhotosOriginal = Date(timeIntervalSince1970: datePhotosOriginal) + } + + let exifPhotosElements = propstat["d:prop", "nc:metadata-photos-exif"] + if let element = exifPhotosElements.element { + for child in element.childElements { + file.exifPhotos.append([child.name: child.text]) + } + } + + file.placePhotos = propstat["d:prop", "nc:metadata-photos-place"].text + + let results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) file.contentType = results.mimeType file.iconName = results.iconName @@ -963,7 +988,7 @@ class NKDataFileXML: NSObject { file.trashbinDeletionTime = Date(timeIntervalSince1970: trashbinDeletionTimeDouble) } - let results = self.nkCommonInstance.getInternalType(fileName: file.trashbinFileName, mimeType: file.contentType, directory: file.directory) + let results = self.nkCommonInstance.getInternalType(fileName: file.trashbinFileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) file.contentType = results.mimeType file.classFile = results.classFile diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift new file mode 100644 index 00000000..b528eb16 --- /dev/null +++ b/Sources/NextcloudKit/NKSession.swift @@ -0,0 +1,117 @@ +// +// NKSession.swift +// +// +// Created by Marino Faggiana on 20/07/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation +import Alamofire + +public class NKSession { + public var urlBase: String + public var user: String + public var userId: String + public var password: String + public var account: String + public var userAgent: String + public var nextcloudVersion: Int + public let groupIdentifier: String + public let dav: String = "remote.php/dav" + public var internalTypeIdentifiers: [NKCommon.UTTypeConformsToServer] = [] + public let sessionData: Alamofire.Session + public let sessionDownloadBackground: URLSession + public let sessionUploadBackground: URLSession + public let sessionUploadBackgroundWWan: URLSession + public let sessionUploadBackgroundExt: URLSession + + init(urlBase: String, + user: String, + userId: String, + password: String, + account: String, + userAgent: String, + nextcloudVersion: Int, + groupIdentifier: String) { + self.urlBase = urlBase + self.user = user + self.userId = userId + self.password = password + self.account = account + self.userAgent = userAgent + self.nextcloudVersion = nextcloudVersion + self.groupIdentifier = groupIdentifier + + let backgroundSessionDelegate = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance) + /// Strange but works ?!?! + let sharedCookieStorage = user + "@" + urlBase + + /// Session Alamofire + let configuration = URLSessionConfiguration.af.default + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + configuration.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionData = Alamofire.Session(configuration: configuration, + delegate: NextcloudKitSessionDelegate(nkCommonInstance: NextcloudKit.shared.nkCommonInstance), + rootQueue: NextcloudKit.shared.nkCommonInstance.rootQueue, + requestQueue: NextcloudKit.shared.nkCommonInstance.requestQueue, + serializationQueue: NextcloudKit.shared.nkCommonInstance.serializationQueue, + eventMonitors: [NKLogger(nkCommonInstance: NextcloudKit.shared.nkCommonInstance)]) + + /// Session Download Background + let configurationDownloadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionDownloadBackground) + configurationDownloadBackground.allowsCellularAccess = true + configurationDownloadBackground.sessionSendsLaunchEvents = true + configurationDownloadBackground.isDiscretionary = false + configurationDownloadBackground.httpMaximumConnectionsPerHost = 5 + configurationDownloadBackground.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationDownloadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionDownloadBackground = URLSession(configuration: configurationDownloadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) + + /// Session Upload Background + let configurationUploadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackground) + configurationUploadBackground.allowsCellularAccess = true + configurationUploadBackground.sessionSendsLaunchEvents = true + configurationUploadBackground.isDiscretionary = false + configurationUploadBackground.httpMaximumConnectionsPerHost = 5 + configurationUploadBackground.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationUploadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionUploadBackground = URLSession(configuration: configurationUploadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) + + /// Session Upload Background WWan + let configurationUploadBackgroundWWan = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackgroundWWan) + configurationUploadBackgroundWWan.allowsCellularAccess = false + configurationUploadBackgroundWWan.sessionSendsLaunchEvents = true + configurationUploadBackgroundWWan.isDiscretionary = false + configurationUploadBackgroundWWan.httpMaximumConnectionsPerHost = 5 + configurationUploadBackgroundWWan.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationUploadBackgroundWWan.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionUploadBackgroundWWan = URLSession(configuration: configurationUploadBackgroundWWan, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) + + /// Session Upload Background Extension + let configurationUploadBackgroundExt = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackgroundExt + UUID().uuidString) + configurationUploadBackgroundExt.allowsCellularAccess = true + configurationUploadBackgroundExt.sessionSendsLaunchEvents = true + configurationUploadBackgroundExt.isDiscretionary = false + configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = 5 + configurationUploadBackgroundExt.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationUploadBackgroundExt.sharedContainerIdentifier = groupIdentifier + configurationUploadBackgroundExt.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) + sessionUploadBackgroundExt = URLSession(configuration: configurationUploadBackgroundExt, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) + } +} diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 25605def..baaa2ea7 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -57,7 +57,7 @@ public extension NextcloudKit { return options.queue.async { completion(.urlError) } } - sessionManager.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in + internalSession.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -77,19 +77,19 @@ public extension NextcloudKit { // MARK: - func generalWithEndpoint(_ endpoint: String, - method: String, account: String, + method: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: method.uppercased()) - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -112,15 +112,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var externalSites: [NKExternalSite] = [] let endpoint = "ocs/v2.php/apps/external/api/v1" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, externalSites, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -174,12 +174,15 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (ServerInfoResult) -> Void) { let endpoint = "status.php" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { return options.queue.async { completion(ServerInfoResult.failure(.urlError)) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) + var headers: HTTPHeaders? + if let userAgent = options.customUserAgent { + headers = [HTTPHeader.userAgent(userAgent)] + } - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + internalSession.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -228,13 +231,16 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - var headers = self.nkCommonInstance.getStandardHeaders(options: options) + guard let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, .urlError) } + } if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -257,8 +263,8 @@ public extension NextcloudKit { } func downloadPreview(fileId: String, - widthPreview: Int = 512, - heightPreview: Int = 512, + width: Int = 1024, + height: Int = 1024, etag: String? = nil, crop: Int = 1, cropMode: String = "cover", @@ -267,20 +273,20 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - guard let urlRequest = url else { - return options.queue.async { completion(account, nil, .urlError) } + completion: @escaping (_ account: String, _ data: Data?, _ width: Int, _ height: Int, _ etag: String?, _ error: NKError) -> Void) { + let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, width, height, nil, .urlError) } } - var headers = self.nkCommonInstance.getStandardHeaders(options: options) + if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -290,51 +296,21 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, error) } + options.queue.async { completion(account, nil, width, height, nil, error) } case .success: if let data = response.data { - options.queue.async { completion(account, data, .success) } + let etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields)?.replacingOccurrences(of: "\"", with: "") + options.queue.async { completion(account, data, width, height, etag, .success) } } else { - options.queue.async { completion(account, nil, .invalidData) } + options.queue.async { completion(account, nil, width, height, nil, .invalidData) } } } } } - func downloadPreview(fileId: String, - fileNamePreviewLocalPath: String, - fileNameIconLocalPath: String? = nil, - widthPreview: Int = 512, - heightPreview: Int = 512, - sizeIcon: Int = 512, - compressionQualityPreview: CGFloat = 0.5, - compressionQualityIcon: CGFloat = 0.5, - etag: String? = nil, - crop: Int = 1, - cropMode: String = "cover", - forceIcon: Int = 0, - mimeFallback: Int = 0, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - - downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: etag, endpoint: endpoint, account: account, options: options) { task in - taskHandler(task) - } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in - completion(account, imagePreview, imageIcon, imageOriginal,etag,error) - } - } - func downloadTrashPreview(fileId: String, - fileNamePreviewLocalPath: String, - fileNameIconLocalPath: String, - widthPreview: Int = 512, - heightPreview: Int = 512, - sizeIcon: Int = 512, - compressionQualityPreview: CGFloat = 0.5, - compressionQualityIcon: CGFloat = 0.5, + width: Int = 512, + height: Int = 512, crop: Int = 1, cropMode: String = "cover", forceIcon: Int = 0, @@ -342,42 +318,16 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - - downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: nil, endpoint: endpoint, account: account, options: options) { task in - taskHandler(task) - } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in - completion(account, imagePreview, imageIcon, imageOriginal,etag,error) - } - } + completion: @escaping (_ account: String, _ data: Data?, _ width: Int, _ height: Int, _ error: NKError) -> Void) { + let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - private func downloadPreview(fileNamePreviewLocalPath: String, - fileNameIconLocalPath: String? = nil, - sizeIcon: Int = 0, - compressionQualityPreview: CGFloat, - compressionQualityIcon: CGFloat, - etag: String? = nil, - endpoint: String?, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var url: URLConvertible? - if let endpoint { - url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - } - guard let urlRequest = url else { - return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } - } - var headers = self.nkCommonInstance.getStandardHeaders(options: options) - if var etag = etag { - etag = "\"" + etag + "\"" - headers.update(name: "If-None-Match", value: etag) + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, width, height, .urlError) } } - sessionManager.request(urlRequest, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -387,28 +337,12 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, nil, nil, error) } + options.queue.async { completion(account, nil, width, height, error) } case .success: - guard let data = response.data, let imageOriginal = UIImage(data: data) else { - return options.queue.async { completion(account, nil, nil, nil, nil, .invalidData) } - } - let etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields)?.replacingOccurrences(of: "\"", with: "") - var imagePreview, imageIcon: UIImage? - do { - if let data = imageOriginal.jpegData(compressionQuality: compressionQualityPreview) { - try data.write(to: URL(fileURLWithPath: fileNamePreviewLocalPath), options: .atomic) - imagePreview = UIImage(data: data) - } - if let fileNameIconLocalPath, sizeIcon > 0 { - imageIcon = imageOriginal.resizeImage(size: CGSize(width: sizeIcon, height: sizeIcon)) - if let data = imageIcon?.jpegData(compressionQuality: compressionQualityIcon) { - try data.write(to: URL(fileURLWithPath: fileNameIconLocalPath), options: .atomic) - imageIcon = UIImage(data: data) - } - } - options.queue.async { completion(account, imagePreview, imageIcon, imageOriginal, etag, .success) } - } catch { - options.queue.async { completion(account, nil, nil, nil, nil, NKError(error: error)) } + if let data = response.data { + options.queue.async { completion(account, data, width, height, .success) } + } else { + options.queue.async { completion(account, nil, width, height, .invalidData) } } } } @@ -423,18 +357,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/avatar/\(user)/\(sizeImage)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - var headers = self.nkCommonInstance.getStandardHeaders(options: options) + if var etag = etag { etag = "\"" + etag + "\"" headers.update(name: "If-None-Match", value: etag) } - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -510,12 +445,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - guard let url = serverUrl.asUrl else { + guard let url = serverUrl.asUrl, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -542,14 +478,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/cloud/user" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -611,14 +547,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v1.php/cloud/capabilities" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -649,12 +585,16 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ wipe: Bool, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "index.php/core/wipe/check" let parameters: [String: Any] = ["token": token] - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + /// + options.contentType = "application/json" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, false, nil, .urlError) } } - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -681,12 +621,16 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "index.php/core/wipe/success" let parameters: [String: Any] = ["token": token] - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + /// + options.contentType = "application/json" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -714,7 +658,6 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var activities: [NKActivity] = [] var activityFirstKnown = 0 var activityLastGiven = 0 @@ -737,12 +680,13 @@ public extension NextcloudKit { parameters["previews"] = "true" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -811,15 +755,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications" - var notifications: [NKNotifications] = [] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -834,6 +777,7 @@ public extension NextcloudKit { let json = JSON(jsonData) if json["ocs"]["meta"]["statuscode"].int == 200 { let ocsdata = json["ocs"]["data"] + var notifications: [NKNotifications] = [] for (_, subJson): (String, JSON) in ocsdata { let notification = NKNotifications() @@ -885,11 +829,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } var url: URLConvertible? if serverUrl == nil { let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications/\(idNotification)" - url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + url = self.nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options) } else { url = serverUrl?.asUrl } @@ -897,9 +844,8 @@ public extension NextcloudKit { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: method) - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -923,18 +869,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/dav/api/v1/direct" let parameters: [String: Any] = [ "fileId": fileId, "format": "json" ] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -961,12 +907,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/security_guard/diagnostics" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + /// + options.contentType = "application/json" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: .put, headers: headers) @@ -975,7 +924,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Download.swift b/Sources/NextcloudKit/NextcloudKit+Download.swift new file mode 100644 index 00000000..e5725600 --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+Download.swift @@ -0,0 +1,92 @@ +// +// NextcloudKit+Download.swift +// NextcloudKit +// +// Created by Marino Faggiana on 16/08/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation +import Alamofire +import SwiftyJSON + +public extension NextcloudKit { + func download(serverUrlFileName: Any, + fileNameLocalPath: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + requestHandler: @escaping (_ request: DownloadRequest) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, + completionHandler: @escaping (_ account: String, _ etag: String?, _ date: Date?, _ lenght: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nKError: NKError) -> Void) { + var convertible: URLConvertible? + if serverUrlFileName is URL { + convertible = serverUrlFileName as? URLConvertible + } else if serverUrlFileName is String || serverUrlFileName is NSString { + convertible = (serverUrlFileName as? String)?.encodedToUrl + } + guard let url = convertible, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completionHandler(account, nil, nil, 0, nil, nil, .urlError) } + } + var destination: Alamofire.DownloadRequest.Destination? + let fileNamePathLocalDestinationURL = NSURL.fileURL(withPath: fileNameLocalPath) + let destinationFile: DownloadRequest.Destination = { _, _ in + return (fileNamePathLocalDestinationURL, [.removePreviousFile, .createIntermediateDirectories]) + } + destination = destinationFile + + let request = nkSession.sessionData.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + options.queue.async { taskHandler(task) } + } .downloadProgress { progress in + options.queue.async { progressHandler(progress) } + } .response(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let resultError = NKError(error: error, afResponse: response, responseData: nil) + options.queue.async { completionHandler(account, nil, nil, 0, nil, error, resultError) } + case .success: + var date: Date? + var etag: String? + var length: Int64 = 0 + let allHeaderFields = response.response?.allHeaderFields + + if let result = response.response?.allHeaderFields["Content-Length"] as? String { + length = Int64(result) ?? 0 + } + if self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) != nil { + etag = self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) + } else if self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) != nil { + etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) + } + if etag != nil { + etag = etag?.replacingOccurrences(of: "\"", with: "") + } + if let dateString = self.nkCommonInstance.findHeader("Date", allHeaderFields: response.response?.allHeaderFields) { + date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") + } + + options.queue.async { completionHandler(account, etag, date, length, allHeaderFields, nil, .success) } + } + } + + options.queue.async { requestHandler(request) } + } +} diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 280ab626..19a7a48a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -35,7 +35,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/getapppassword" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint) else { + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint, options: options) else { return options.queue.async { completion(nil, nil, .urlError) } } var headers: HTTPHeaders = [.authorization(username: user, password: password)] @@ -51,7 +51,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + internalSession.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -82,7 +82,8 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/apppassword" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { return options.queue.async { completion(nil, .urlError) } } var headers: HTTPHeaders = [.authorization(username: username, password: password)] @@ -98,7 +99,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -122,7 +123,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ token: String?, _ endpoint: String?, _ login: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "index.php/login/v2" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { return options.queue.async { completion(nil, nil, nil, nil, .urlError) } } var headers: HTTPHeaders? @@ -130,7 +131,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + internalSession.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -167,7 +168,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + internalSession.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift new file mode 100644 index 00000000..ca98a185 --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -0,0 +1,282 @@ +// +// NextcloudKit+Upload.swift +// NextcloudKit +// +// Created by Marino Faggiana on 16/08/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation +import Alamofire +import SwiftyJSON + +public extension NextcloudKit { + func upload(serverUrlFileName: Any, + fileNameLocalPath: String, + dateCreationFile: Date? = nil, + dateModificationFile: Date? = nil, + overwrite: Bool = false, + account: String, + options: NKRequestOptions = NKRequestOptions(), + requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, + completionHandler: @escaping (_ account: String, _ ocId: String?, _ etag: String?, _ date: Date?, _ size: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nkError: NKError) -> Void) { + var convertible: URLConvertible? + var size: Int64 = 0 + if serverUrlFileName is URL { + convertible = serverUrlFileName as? URLConvertible + } else if serverUrlFileName is String || serverUrlFileName is NSString { + convertible = (serverUrlFileName as? String)?.encodedToUrl + } + guard let url = convertible, + let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, nil, .urlError) } + } + let fileNameLocalPathUrl = URL(fileURLWithPath: fileNameLocalPath) + // Epoch of linux do not permitted negativ value + if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { + headers.update(name: "X-OC-CTime", value: "\(dateCreationFile.timeIntervalSince1970)") + } + // Epoch of linux do not permitted negativ value + if let dateModificationFile, dateModificationFile.timeIntervalSince1970 > 0 { + headers.update(name: "X-OC-MTime", value: "\(dateModificationFile.timeIntervalSince1970)") + } + if overwrite { + headers.update(name: "Overwrite", value: "true") + } + + let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in + task.taskDescription = options.taskDescription + options.queue.async { taskHandler(task) } + }) .uploadProgress { progress in + options.queue.async { progressHandler(progress) } + size = progress.totalUnitCount + } .response(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let resultError = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, error, resultError) } + case .success: + var ocId: String?, etag: String? + let allHeaderFields = response.response?.allHeaderFields + if self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: response.response?.allHeaderFields) != nil { + ocId = self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: response.response?.allHeaderFields) + } else if self.nkCommonInstance.findHeader("fileid", allHeaderFields: response.response?.allHeaderFields) != nil { + ocId = self.nkCommonInstance.findHeader("fileid", allHeaderFields: response.response?.allHeaderFields) + } + if self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) != nil { + etag = self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) + } else if self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) != nil { + etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) + } + if etag != nil { + etag = etag?.replacingOccurrences(of: "\"", with: "") + } + if let dateString = self.nkCommonInstance.findHeader("date", allHeaderFields: response.response?.allHeaderFields) { + if let date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") { + options.queue.async { completionHandler(account, ocId, etag, date, size, allHeaderFields, nil, .success) } + } else { + options.queue.async { completionHandler(account, nil, nil, nil, 0, allHeaderFields, nil, .invalidDate) } + } + } else { + options.queue.async { completionHandler(account, nil, nil, nil, 0, allHeaderFields, nil, .invalidDate) } + } + } + } + + options.queue.async { requestHandler(request) } + } + + /// - Parameters: + /// - directory: The local directory where is the file to be split + /// - fileName: The name of the file to be splites + /// - date: If exist the date of file + /// - creationDate: If exist the creation date of file + /// - serverUrl: The serverURL where the file will be deposited once reassembled + /// - chunkFolder: The name of temp folder, usually NSUUID().uuidString + /// - filesChunk: The struct it will contain all file names with the increment size still to be sent. + /// Example filename: "3","4","5" .... size: 30000000, 40000000, 43000000 + /// - chunkSizeInMB: Size in MB of chunk + + func uploadChunk(directory: String, + fileName: String, + date: Date?, + creationDate: Date?, + serverUrl: String, + chunkFolder: String, + filesChunk: [(fileName: String, size: Int64)], + chunkSize: Int, + account: String, + options: NKRequestOptions = NKRequestOptions(), + numChunks: @escaping (_ num: Int) -> Void = { _ in }, + counterChunk: @escaping (_ counter: Int) -> Void = { _ in }, + start: @escaping (_ filesChunk: [(fileName: String, size: Int64)]) -> Void = { _ in }, + requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + progressHandler: @escaping (_ totalBytesExpected: Int64, _ totalBytes: Int64, _ fractionCompleted: Double) -> Void = { _, _, _ in }, + uploaded: @escaping (_ fileChunk: (fileName: String, size: Int64)) -> Void = { _ in }, + completion: @escaping (_ account: String, _ filesChunk: [(fileName: String, size: Int64)]?, _ file: NKFile?, _ afError: AFError?, _ error: NKError) -> Void) { + + guard let nkSession = nkCommonInstance.getSession(account: account) else { + return completion(account, nil, nil, nil, .urlError) + } + let fileNameLocalSize = self.nkCommonInstance.getFileSize(filePath: directory + "/" + fileName) + let serverUrlChunkFolder = nkSession.urlBase + "/" + nkSession.dav + "/uploads/" + nkSession.userId + "/" + chunkFolder + let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl, urlBase: nkSession.urlBase, userId: nkSession.userId) + "/" + fileName + if options.customHeader == nil { + options.customHeader = [:] + } + options.customHeader?["Destination"] = serverUrlFileName.urlEncoded + options.customHeader?["OC-Total-Length"] = String(fileNameLocalSize) + + // check space + #if os(macOS) + var fsAttributes: [FileAttributeKey: Any] + do { + fsAttributes = try FileManager.default.attributesOfFileSystem(forPath: "/") + } catch { + return completion(account, nil, nil, nil, NKError(errorCode: NKError.chunkNoEnoughMemory)) + } + let freeDisk = ((fsAttributes[FileAttributeKey.systemFreeSize] ?? 0) as? Int64) ?? 0 + #elseif os(visionOS) || os(iOS) + var freeDisk: Int64 = 0 + let fileURL = URL(fileURLWithPath: directory as String) + do { + let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey]) + if let capacity = values.volumeAvailableCapacityForImportantUsage { + freeDisk = capacity + } + } catch { } + #endif + + #if os(visionOS) || os(iOS) + if freeDisk < fileNameLocalSize * 4 { + // It seems there is not enough space to send the file + let error = NKError(errorCode: NKError.chunkNoEnoughMemory, errorDescription: "_chunk_enough_memory_") + return completion(account, nil, nil, nil, error) + } + #endif + + func createFolder(completion: @escaping (_ errorCode: NKError) -> Void) { + readFileOrFolder(serverUrlFileName: serverUrlChunkFolder, depth: "0", account: account, options: options) { _, _, _, error in + if error == .success { + completion(NKError()) + } else if error.errorCode == 404 { + NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, account: account, options: options) { _, _, _, error in + completion(error) + } + } else { + completion(error) + } + } + } + + createFolder { error in + guard error == .success else { + return completion(account, nil, nil, nil, NKError(errorCode: NKError.chunkCreateFolder, errorDescription: error.errorDescription)) + } + var uploadNKError = NKError() + var uploadAFError: AFError? + + self.nkCommonInstance.chunkedFile(inputDirectory: directory, outputDirectory: directory, fileName: fileName, chunkSize: chunkSize, filesChunk: filesChunk) { num in + numChunks(num) + } counterChunk: { counter in + counterChunk(counter) + } completion: { filesChunk in + if filesChunk.isEmpty { + // The file for sending could not be created + let error = NKError(errorCode: NKError.chunkFilesNull, errorDescription: "_chunk_files_null_") + return completion(account, nil, nil, nil, error) + } + var filesChunkOutput = filesChunk + start(filesChunkOutput) + + for fileChunk in filesChunk { + let serverUrlFileName = serverUrlChunkFolder + "/" + fileChunk.fileName + let fileNameLocalPath = directory + "/" + fileChunk.fileName + let fileSize = self.nkCommonInstance.getFileSize(filePath: fileNameLocalPath) + if fileSize == 0 { + // The file could not be sent + let error = NKError(errorCode: NKError.chunkFileNull, errorDescription: "_chunk_file_null_") + return completion(account, nil, nil, .explicitlyCancelled, error) + } + let semaphore = DispatchSemaphore(value: 0) + self.upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: account, options: options, requestHandler: { request in + requestHandler(request) + }, taskHandler: { task in + taskHandler(task) + }, progressHandler: { _ in + let totalBytesExpected = fileNameLocalSize + let totalBytes = fileChunk.size + let fractionCompleted = Double(totalBytes) / Double(totalBytesExpected) + progressHandler(totalBytesExpected, totalBytes, fractionCompleted) + }) { _, _, _, _, _, _, afError, error in + if error == .success { + filesChunkOutput.removeFirst() + uploaded(fileChunk) + } + uploadAFError = afError + uploadNKError = error + semaphore.signal() + } + semaphore.wait() + + if uploadNKError != .success { + break + } + } + + guard uploadNKError == .success else { + return completion(account, filesChunkOutput, nil, uploadAFError, NKError(errorCode: NKError.chunkFileUpload, errorDescription: uploadNKError.errorDescription)) + } + + // Assemble the chunks + let serverUrlFileNameSource = serverUrlChunkFolder + "/.file" + // Epoch of linux do not permitted negativ value + if let creationDate, creationDate.timeIntervalSince1970 > 0 { + options.customHeader?["X-OC-CTime"] = "\(creationDate.timeIntervalSince1970)" + } + // Epoch of linux do not permitted negativ value + if let date, date.timeIntervalSince1970 > 0 { + options.customHeader?["X-OC-MTime"] = "\(date.timeIntervalSince1970)" + } + // Calculate Assemble Timeout + let assembleSizeInGB = Double(fileNameLocalSize) / 1e9 + let assembleTimePerGB: Double = 3 * 60 // 3 min + let assembleTimeMin: Double = 60 // 60 sec + let assembleTimeMax: Double = 30 * 60 // 30 min + options.timeout = max(assembleTimeMin, min(assembleTimePerGB * assembleSizeInGB, assembleTimeMax)) + + self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account, options: options) { _, error in + guard error == .success else { + return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) + } + self.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account, options: NKRequestOptions(queue: self.nkCommonInstance.backgroundQueue)) { _, files, _, error in + guard error == .success, let file = files?.first else { + return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) + } + return completion(account, filesChunkOutput, file, nil, error) + } + } + } + } + } +} + diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index e936cecd..553361d1 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -29,117 +29,110 @@ import UIKit import Alamofire import SwiftyJSON -open class NextcloudKit: SessionDelegate { +open class NextcloudKit { public static let shared: NextcloudKit = { let instance = NextcloudKit() return instance }() - internal lazy var internalSessionManager: Alamofire.Session = { - return Alamofire.Session(configuration: nkCommonInstance.sessionConfiguration, - delegate: self, - rootQueue: nkCommonInstance.rootQueue, - startRequestsImmediately: true, - requestQueue: nkCommonInstance.requestQueue, - serializationQueue: nkCommonInstance.serializationQueue, - interceptor: nil, - serverTrustManager: nil, - redirectHandler: nil, - cachedResponseHandler: nil, - eventMonitors: [AlamofireLogger(nkCommonInstance: self.nkCommonInstance)]) - }() - public var sessionManager: Alamofire.Session { - return internalSessionManager - } - #if !os(watchOS) +#if !os(watchOS) private let reachabilityManager = Alamofire.NetworkReachabilityManager() - #endif - // private var cookies: [String:[HTTPCookie]] = [:] +#endif public let nkCommonInstance = NKCommon() + internal lazy var internalSession: Alamofire.Session = { + return Alamofire.Session(configuration: URLSessionConfiguration.af.default, + delegate: NextcloudKitSessionDelegate(nkCommonInstance: nkCommonInstance), + eventMonitors: [NKLogger(nkCommonInstance: self.nkCommonInstance)]) + }() - override public init(fileManager: FileManager = .default) { - super.init(fileManager: fileManager) - #if !os(watchOS) + init() { +#if !os(watchOS) startNetworkReachabilityObserver() - #endif +#endif } deinit { - #if !os(watchOS) +#if !os(watchOS) stopNetworkReachabilityObserver() - #endif +#endif } - // MARK: - Setup + // MARK: - Session setup - public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, userAgent: String, nextcloudVersion: Int, groupIdentifier: String? = nil, delegate: NKCommonDelegate?) { - self.setup(account: account, user: user, userId: userId, password: password, urlBase: urlBase, groupIdentifier: groupIdentifier) - self.setup(userAgent: userAgent) - self.setup(nextcloudVersion: nextcloudVersion) - self.setup(delegate: delegate) + public func setup(delegate: NextcloudKitDelegate?) { + self.nkCommonInstance.delegate = delegate } - public func setup(account: String? = nil, user: String, userId: String, password: String, urlBase: String, groupIdentifier: String? = nil) { - self.nkCommonInstance._groupIdentifier = groupIdentifier - if (self.nkCommonInstance.account != account) || (self.nkCommonInstance.urlBase != urlBase && self.nkCommonInstance.user != user) { - if let cookieStore = sessionManager.session.configuration.httpCookieStorage { - for cookie in cookieStore.cookies ?? [] { - cookieStore.deleteCookie(cookie) - } - } - self.nkCommonInstance.internalTypeIdentifiers = [] - } - - if let account { - self.nkCommonInstance._account = account - } else { - self.nkCommonInstance._account = "" + public func appendSession(account: String, + urlBase: String, + user: String, + userId: String, + password: String, + userAgent: String, + nextcloudVersion: Int, + groupIdentifier: String) { + if nkCommonInstance.nksessions.filter({ $0.account == account }).first != nil { + return updateSession(account: account, urlBase: urlBase, userId: userId, password: password, userAgent: userAgent, nextcloudVersion: nextcloudVersion) } - self.nkCommonInstance._user = user - self.nkCommonInstance._userId = userId - self.nkCommonInstance._password = password - self.nkCommonInstance._urlBase = urlBase - } + let nkSession = NKSession(urlBase: urlBase, user: user, userId: userId, password: password, account: account, userAgent: userAgent, nextcloudVersion: nextcloudVersion, groupIdentifier: groupIdentifier) - public func setup(delegate: NKCommonDelegate?) { - self.nkCommonInstance.delegate = delegate + nkCommonInstance.nksessions.append(nkSession) } - public func setup(userAgent: String) { - self.nkCommonInstance._userAgent = userAgent + public func updateSession(account: String, + urlBase: String? = nil, + user: String? = nil, + userId: String? = nil, + password: String? = nil, + userAgent: String? = nil, + nextcloudVersion: Int? = nil, + replaceWithAccount: String? = nil) { + guard let nkSession = nkCommonInstance.nksessions.filter({ $0.account == account }).first else { return } + if let urlBase { + nkSession.urlBase = urlBase + } + if let user { + nkSession.user = user + } + if let userId { + nkSession.userId = userId + } + if let password { + nkSession.password = password + } + if let userAgent { + nkSession.userAgent = userAgent + } + if let nextcloudVersion { + nkSession.nextcloudVersion = nextcloudVersion + } + if let replaceWithAccount { + nkSession.account = replaceWithAccount + } } - public func setup(nextcloudVersion: Int) { - self.nkCommonInstance._nextcloudVersion = nextcloudVersion + public func removeSession(account: String) { + if let index = nkCommonInstance.nksessions.index(where: { $0.account == account}) { + nkCommonInstance.nksessions.remove(at: index) + } } - /* - internal func saveCookies(response : HTTPURLResponse?) { - - if let headerFields = response?.allHeaderFields as? [String : String] { - if let url = URL(string: self.nkCommonInstance.urlBase) { - let HTTPCookie = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url) - if HTTPCookie.count > 0 { - cookies[self.nkCommonInstance.account] = HTTPCookie - } else { - cookies[self.nkCommonInstance.account] = nil - } - } - } + public func getSession(account: String) -> NKSession? { + return nkCommonInstance.nksessions.filter({ $0.account == account }).first } - internal func injectsCookies() { + public func deleteCookieStorageForAccount(_ account: String) { + guard let nkSession = nkCommonInstance.nksessions.filter({ $0.account == account }).first else { return } - if let cookies = cookies[self.nkCommonInstance.account] { - if let url = URL(string: self.nkCommonInstance.urlBase) { - sessionManager.session.configuration.httpCookieStorage?.setCookies(cookies, for: url, mainDocumentURL: nil) + if let cookieStore = nkSession.sessionData.session.configuration.httpCookieStorage { + for cookie in cookieStore.cookies ?? [] { + cookieStore.deleteCookie(cookie) } } } - */ // MARK: - Reachability - #if !os(watchOS) +#if !os(watchOS) public func isNetworkReachable() -> Bool { return reachabilityManager?.isReachable ?? false } @@ -162,385 +155,5 @@ open class NextcloudKit: SessionDelegate { private func stopNetworkReachabilityObserver() { reachabilityManager?.stopListening() } - #endif - - // MARK: - Session utility - - public func getSessionManager() -> URLSession { - return sessionManager.session - } - - // MARK: - download / upload - - public func download(serverUrlFileName: Any, - fileNameLocalPath: String, - account: String, - options: NKRequestOptions = NKRequestOptions(), - requestHandler: @escaping (_ request: DownloadRequest) -> Void = { _ in }, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, - completionHandler: @escaping (_ account: String, _ etag: String?, _ date: Date?, _ lenght: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nKError: NKError) -> Void) { - var convertible: URLConvertible? - if serverUrlFileName is URL { - convertible = serverUrlFileName as? URLConvertible - } else if serverUrlFileName is String || serverUrlFileName is NSString { - convertible = (serverUrlFileName as? String)?.encodedToUrl - } - guard let url = convertible else { - options.queue.async { completionHandler(account, nil, nil, 0, nil, nil, .urlError) } - return - } - var destination: Alamofire.DownloadRequest.Destination? - let fileNamePathLocalDestinationURL = NSURL.fileURL(withPath: fileNameLocalPath) - let destinationFile: DownloadRequest.Destination = { _, _ in - return (fileNamePathLocalDestinationURL, [.removePreviousFile, .createIntermediateDirectories]) - } - destination = destinationFile - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - - let request = sessionManager.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - options.queue.async { taskHandler(task) } - } .downloadProgress { progress in - options.queue.async { progressHandler(progress) } - } .response(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { - case .failure(let error): - let resultError = NKError(error: error, afResponse: response, responseData: nil) - options.queue.async { completionHandler(account, nil, nil, 0, nil, error, resultError) } - case .success: - var date: Date? - var etag: String? - var length: Int64 = 0 - let allHeaderFields = response.response?.allHeaderFields - - if let result = response.response?.allHeaderFields["Content-Length"] as? String { - length = Int64(result) ?? 0 - } - if self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) != nil { - etag = self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) - } else if self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) != nil { - etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) - } - if etag != nil { - etag = etag?.replacingOccurrences(of: "\"", with: "") - } - if let dateString = self.nkCommonInstance.findHeader("Date", allHeaderFields: response.response?.allHeaderFields) { - date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") - } - - options.queue.async { completionHandler(account, etag, date, length, allHeaderFields, nil, .success) } - } - } - - options.queue.async { requestHandler(request) } - } - - public func upload(serverUrlFileName: Any, - fileNameLocalPath: String, - dateCreationFile: Date? = nil, - dateModificationFile: Date? = nil, - account: String, - options: NKRequestOptions = NKRequestOptions(), - requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in }, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, - completionHandler: @escaping (_ account: String, _ ocId: String?, _ etag: String?, _ date: Date?, _ size: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nkError: NKError) -> Void) { - var convertible: URLConvertible? - var size: Int64 = 0 - if serverUrlFileName is URL { - convertible = serverUrlFileName as? URLConvertible - } else if serverUrlFileName is String || serverUrlFileName is NSString { - convertible = (serverUrlFileName as? String)?.encodedToUrl - } - guard let url = convertible else { - options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, nil, .urlError) } - return - } - let fileNameLocalPathUrl = URL(fileURLWithPath: fileNameLocalPath) - var headers = self.nkCommonInstance.getStandardHeaders(options: options) - // Epoch of linux do not permitted negativ value - if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { - headers.update(name: "X-OC-CTime", value: "\(dateCreationFile.timeIntervalSince1970)") - } - // Epoch of linux do not permitted negativ value - if let dateModificationFile, dateModificationFile.timeIntervalSince1970 > 0 { - headers.update(name: "X-OC-MTime", value: "\(dateModificationFile.timeIntervalSince1970)") - } - - let request = sessionManager.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in - task.taskDescription = options.taskDescription - options.queue.async { taskHandler(task) } - }) .uploadProgress { progress in - options.queue.async { progressHandler(progress) } - size = progress.totalUnitCount - } .response(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { - case .failure(let error): - let resultError = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, error, resultError) } - case .success: - var ocId: String?, etag: String? - let allHeaderFields = response.response?.allHeaderFields - if self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: response.response?.allHeaderFields) != nil { - ocId = self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: response.response?.allHeaderFields) - } else if self.nkCommonInstance.findHeader("fileid", allHeaderFields: response.response?.allHeaderFields) != nil { - ocId = self.nkCommonInstance.findHeader("fileid", allHeaderFields: response.response?.allHeaderFields) - } - if self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) != nil { - etag = self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: response.response?.allHeaderFields) - } else if self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) != nil { - etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields) - } - if etag != nil { - etag = etag?.replacingOccurrences(of: "\"", with: "") - } - if let dateString = self.nkCommonInstance.findHeader("date", allHeaderFields: response.response?.allHeaderFields) { - if let date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") { - options.queue.async { completionHandler(account, ocId, etag, date, size, allHeaderFields, nil, .success) } - } else { - options.queue.async { completionHandler(account, nil, nil, nil, 0, allHeaderFields, nil, .invalidDate) } - } - } else { - options.queue.async { completionHandler(account, nil, nil, nil, 0, allHeaderFields, nil, .invalidDate) } - } - } - } - - options.queue.async { requestHandler(request) } - } - - /// - Parameters: - /// - directory: The local directory where is the file to be split - /// - fileName: The name of the file to be splites - /// - date: If exist the date of file - /// - creationDate: If exist the creation date of file - /// - serverUrl: The serverURL where the file will be deposited once reassembled - /// - chunkFolder: The name of temp folder, usually NSUUID().uuidString - /// - filesChunk: The struct it will contain all file names with the increment size still to be sent. - /// Example filename: "3","4","5" .... size: 30000000, 40000000, 43000000 - /// - chunkSizeInMB: Size in MB of chunk - - public func uploadChunk(directory: String, - fileName: String, - date: Date?, - creationDate: Date?, - serverUrl: String, - chunkFolder: String, - filesChunk: [(fileName: String, size: Int64)], - chunkSize: Int, - account: String, - options: NKRequestOptions = NKRequestOptions(), - numChunks: @escaping (_ num: Int) -> Void = { _ in }, - counterChunk: @escaping (_ counter: Int) -> Void = { _ in }, - start: @escaping (_ filesChunk: [(fileName: String, size: Int64)]) -> Void = { _ in }, - requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in }, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - progressHandler: @escaping (_ totalBytesExpected: Int64, _ totalBytes: Int64, _ fractionCompleted: Double) -> Void = { _, _, _ in }, - uploaded: @escaping (_ fileChunk: (fileName: String, size: Int64)) -> Void = { _ in }, - completion: @escaping (_ account: String, _ filesChunk: [(fileName: String, size: Int64)]?, _ file: NKFile?, _ afError: AFError?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let fileNameLocalSize = self.nkCommonInstance.getFileSize(filePath: directory + "/" + fileName) - let serverUrlChunkFolder = urlBase + "/" + dav + "/uploads/" + userId + "/" + chunkFolder - let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl) + "/" + fileName - if options.customHeader == nil { - options.customHeader = [:] - } - options.customHeader?["Destination"] = serverUrlFileName - options.customHeader?["OC-Total-Length"] = String(fileNameLocalSize) - - // check space - #if os(macOS) - var fsAttributes: [FileAttributeKey: Any] - do { - fsAttributes = try FileManager.default.attributesOfFileSystem(forPath: "/") - } catch { - return completion(account, nil, nil, nil, NKError(errorCode: NKError.chunkNoEnoughMemory)) - } - let freeDisk = ((fsAttributes[FileAttributeKey.systemFreeSize] ?? 0) as? Int64) ?? 0 - #elseif os(visionOS) || os(iOS) - var freeDisk: Int64 = 0 - let fileURL = URL(fileURLWithPath: directory as String) - do { - let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey]) - if let capacity = values.volumeAvailableCapacityForImportantUsage { - freeDisk = capacity - } - } catch { } - #endif - - #if os(visionOS) || os(iOS) - if freeDisk < fileNameLocalSize * 4 { - // It seems there is not enough space to send the file - let error = NKError(errorCode: NKError.chunkNoEnoughMemory, errorDescription: "_chunk_enough_memory_") - return completion(account, nil, nil, nil, error) - } - #endif - - func createFolder(completion: @escaping (_ errorCode: NKError) -> Void) { - readFileOrFolder(serverUrlFileName: serverUrlChunkFolder, depth: "0", account: account, options: options) { _, _, _, error in - if error == .success { - completion(NKError()) - } else if error.errorCode == 404 { - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, account: account, options: options) { _, _, _, error in - completion(error) - } - } else { - completion(error) - } - } - } - - createFolder { error in - guard error == .success else { - return completion(account, nil, nil, nil, NKError(errorCode: NKError.chunkCreateFolder, errorDescription: error.errorDescription)) - } - var uploadNKError = NKError() - var uploadAFError: AFError? - - self.nkCommonInstance.chunkedFile(inputDirectory: directory, outputDirectory: directory, fileName: fileName, chunkSize: chunkSize, filesChunk: filesChunk) { num in - numChunks(num) - } counterChunk: { counter in - counterChunk(counter) - } completion: { filesChunk in - if filesChunk.isEmpty { - // The file for sending could not be created - let error = NKError(errorCode: NKError.chunkFilesNull, errorDescription: "_chunk_files_null_") - return completion(account, nil, nil, nil, error) - } - var filesChunkOutput = filesChunk - start(filesChunkOutput) - - for fileChunk in filesChunk { - let serverUrlFileName = serverUrlChunkFolder + "/" + fileChunk.fileName - let fileNameLocalPath = directory + "/" + fileChunk.fileName - let fileSize = self.nkCommonInstance.getFileSize(filePath: fileNameLocalPath) - if fileSize == 0 { - // The file could not be sent - let error = NKError(errorCode: NKError.chunkFileNull, errorDescription: "_chunk_file_null_") - return completion(account, nil, nil, .explicitlyCancelled, error) - } - let semaphore = DispatchSemaphore(value: 0) - self.upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: account, options: options, requestHandler: { request in - requestHandler(request) - }, taskHandler: { task in - taskHandler(task) - }, progressHandler: { _ in - let totalBytesExpected = fileNameLocalSize - let totalBytes = fileChunk.size - let fractionCompleted = Double(totalBytes) / Double(totalBytesExpected) - progressHandler(totalBytesExpected, totalBytes, fractionCompleted) - }) { _, _, _, _, _, _, afError, error in - if error == .success { - filesChunkOutput.removeFirst() - uploaded(fileChunk) - } - uploadAFError = afError - uploadNKError = error - semaphore.signal() - } - semaphore.wait() - - if uploadNKError != .success { - break - } - } - - guard uploadNKError == .success else { - return completion(account, filesChunkOutput, nil, uploadAFError, NKError(errorCode: NKError.chunkFileUpload, errorDescription: uploadNKError.errorDescription)) - } - - // Assemble the chunks - let serverUrlFileNameSource = serverUrlChunkFolder + "/.file" - // Epoch of linux do not permitted negativ value - if let creationDate, creationDate.timeIntervalSince1970 > 0 { - options.customHeader?["X-OC-CTime"] = "\(creationDate.timeIntervalSince1970)" - } - // Epoch of linux do not permitted negativ value - if let date, date.timeIntervalSince1970 > 0 { - options.customHeader?["X-OC-MTime"] = "\(date.timeIntervalSince1970)" - } - // Calculate Assemble Timeout - let assembleSizeInGB = Double(fileNameLocalSize) / 1e9 - let assembleTimePerGB: Double = 3 * 60 // 3 min - let assembleTimeMin: Double = 60 // 60 sec - let assembleTimeMax: Double = 30 * 60 // 30 min - options.timeout = max(assembleTimeMin, min(assembleTimePerGB * assembleSizeInGB, assembleTimeMax)) - - self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account, options: options) { _, error in - guard error == .success else { - return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) - } - - self.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account, options: NKRequestOptions(queue: self.nkCommonInstance.backgroundQueue)) { _, files, _, error in - - guard error == .success, let file = files.first else { - return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) - } - return completion(account, filesChunkOutput, file, nil, error) - } - } - } - } - } - - // MARK: - SessionDelegate - - public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - if self.nkCommonInstance.delegate == nil { - self.nkCommonInstance.writeLog("[WARNING] URLAuthenticationChallenge, no delegate found, perform with default handling") - completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) - } else { - self.nkCommonInstance.delegate?.authenticationChallenge(session, didReceive: challenge, completionHandler: { authChallengeDisposition, credential in - if self.nkCommonInstance.levelLog > 1 { - self.nkCommonInstance.writeLog("[INFO AUTH] Challenge Disposition: \(authChallengeDisposition.rawValue)") - } - completionHandler(authChallengeDisposition, credential) - }) - } - } -} - -final class AlamofireLogger: EventMonitor { - let nkCommonInstance: NKCommon - - init(nkCommonInstance: NKCommon) { - self.nkCommonInstance = nkCommonInstance - } - - func requestDidResume(_ request: Request) { - if self.nkCommonInstance.levelLog > 0 { - self.nkCommonInstance.writeLog("Network request started: \(request)") - if self.nkCommonInstance.levelLog > 1 { - let allHeaders = request.request.flatMap { $0.allHTTPHeaderFields.map { $0.description } } ?? "None" - let body = request.request.flatMap { $0.httpBody.map { String(decoding: $0, as: UTF8.self) } } ?? "None" - - self.nkCommonInstance.writeLog("Network request headers: \(allHeaders)") - self.nkCommonInstance.writeLog("Network request body: \(body)") - } - } - } - - func request(_ request: DataRequest, didParseResponse response: AFDataResponse) { - guard let date = self.nkCommonInstance.convertDate(Date(), format: "yyyy-MM-dd' 'HH:mm:ss") else { return } - let responseResultString = String("\(response.result)") - let responseDebugDescription = String("\(response.debugDescription)") - let responseAllHeaderFields = String("\(String(describing: response.response?.allHeaderFields))") - - if self.nkCommonInstance.levelLog > 0 { - if self.nkCommonInstance.levelLog == 1 { - if let request = response.request { - let requestString = "\(request)" - self.nkCommonInstance.writeLog("Network response request: " + requestString + ", result: " + responseResultString) - } else { - self.nkCommonInstance.writeLog("Network response result: " + responseResultString) - } - } else { - self.nkCommonInstance.writeLog("Network response result: \(date) " + responseDebugDescription) - self.nkCommonInstance.writeLog("Network response all headers: \(date) " + responseAllHeaderFields) - } - } - } +#endif } diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 3ab08a12..ffcbce9a 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -70,6 +70,7 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, dateCreationFile: Date?, dateModificationFile: Date?, taskDescription: String? = nil, + overwrite: Bool = false, account: String, session: URLSession) -> URLSessionUploadTask? { var url: URL? @@ -91,6 +92,9 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, request.httpMethod = "PUT" request.setValue(self.nkCommonInstance.userAgent, forHTTPHeaderField: "User-Agent") request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") + if overwrite { + request.setValue("true", forHTTPHeaderField: "Overwrite") + } // Epoch of linux do not permitted negativ value if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { request.setValue("\(dateCreationFile.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-CTime") diff --git a/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift new file mode 100644 index 00000000..32e38857 --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift @@ -0,0 +1,60 @@ +// +// NextcloudKitSessionDelegate.swift +// NextcloudKit +// +// Created by Marino Faggiana on 07/08/2024. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation + +#if os(macOS) +import Foundation +#else +import UIKit +#endif +import Alamofire +import SwiftyJSON + +open class NextcloudKitSessionDelegate: SessionDelegate { + public var nkCommonInstance: NKCommon? = nil + + override public init(fileManager: FileManager = .default) { + super.init(fileManager: fileManager) + } + + convenience init(nkCommonInstance: NKCommon?) { + self.init() + self.nkCommonInstance = nkCommonInstance + } + + public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + if let nkCommon = self.nkCommonInstance, + let delegate = nkCommon.delegate { + delegate.authenticationChallenge(session, didReceive: challenge) { authChallengeDisposition, credential in + if nkCommon.levelLog > 1 { + nkCommon.writeLog("[INFO AUTH] Challenge Disposition: \(authChallengeDisposition.rawValue)") + } + completionHandler(authChallengeDisposition, credential) + } + } else { + self.nkCommonInstance?.writeLog("[WARNING] URLAuthenticationChallenge, no delegate found, perform with default handling") + completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) + } + } +} diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index f6ae2f9e..f76d1efa 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -40,8 +40,6 @@ public class FileNameValidator { } } -// private var forbiddenFileNameCharactersRegex: NSRegularExpression? - public private(set) var forbiddenFileNameCharacters: [String] = [] public private(set) var forbiddenFileNameExtensions: [String] = [] { @@ -104,30 +102,7 @@ public class FileNameValidator { return nil } - public func checkFileName(_ filename: String, forbiddenFileNames: [String]) -> NKError? { - if filename.hasSuffix(" ") || filename.hasSuffix(".") { - return fileEndsWithSpacePeriodError - } - - if let regex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]"), let invalidCharacterError = checkInvalidCharacters(string: filename, regex: regex) { - return invalidCharacterError - } - - if forbiddenFileNames.contains(filename.uppercased()) || forbiddenFileNames.contains(filename.withRemovedFileExtension.uppercased()) || - forbiddenFileNameBasenames.contains(filename.uppercased()) || forbiddenFileNameBasenames.contains(filename.withRemovedFileExtension.uppercased()) { - templateString = filename - return fileReservedNameError - } - - if forbiddenFileNameExtensions.contains(where: { filename.uppercased().hasSuffix($0.uppercased()) }) { - templateString = filename.fileExtension - return fileForbiddenFileExtensionError - } - - return nil - } - - public func checkFolderPath(folderPath: String) -> Bool { + public func checkFolderPath(_ folderPath: String) -> Bool { return folderPath.split { $0 == "/" || $0 == "\\" } .allSatisfy { checkFileName(String($0)) == nil } } diff --git a/Sources/NextcloudKit/Utils/ThreadSafeArray.swift b/Sources/NextcloudKit/Utils/ThreadSafeArray.swift new file mode 100644 index 00000000..acd6f736 --- /dev/null +++ b/Sources/NextcloudKit/Utils/ThreadSafeArray.swift @@ -0,0 +1,353 @@ +// +// ThreadSafeArray.swift +// Nextcloud +// +// Created by Marino Faggiana on 31/01/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// http://basememara.com/creating-thread-safe-arrays-in-swift/ +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation + +/// A thread-safe array. +public class ThreadSafeArray { + + private var array = [Element]() + private let queue = DispatchQueue(label: "com.nextcloud.ThreadSafeArray", attributes: .concurrent) + + public init() { } + + public convenience init(_ array: [Element]) { + self.init() + self.array = array + } +} + +// MARK: - Properties + +public extension ThreadSafeArray { + + /// The first element of the collection. + var first: Element? { + var result: Element? + queue.sync { result = self.array.first } + return result + } + + /// The last element of the collection. + var last: Element? { + var result: Element? + queue.sync { result = self.array.last } + return result + } + + /// The number of elements in the array. + var count: Int { + var result = 0 + queue.sync { result = self.array.count } + return result + } + + /// A Boolean value indicating whether the collection is empty. + var isEmpty: Bool { + var result = false + queue.sync { result = self.array.isEmpty } + return result + } + + /// A textual representation of the array and its elements. + var description: String { + var result = "" + queue.sync { result = self.array.description } + return result + } +} + +// MARK: - Immutable + +public extension ThreadSafeArray { + + /// Returns the first element of the sequence that satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - Returns: The first element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. + func first(where predicate: (Element) -> Bool) -> Element? { + var result: Element? + queue.sync { result = self.array.first(where: predicate) } + return result + } + + /// Returns the last element of the sequence that satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - Returns: The last element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. + func last(where predicate: (Element) -> Bool) -> Element? { + var result: Element? + queue.sync { result = self.array.last(where: predicate) } + return result + } + + /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate. + /// + /// - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array. + /// - Returns: An array of the elements that includeElement allowed. + func filter(_ isIncluded: @escaping (Element) -> Bool) -> ThreadSafeArray { + var result: ThreadSafeArray? + queue.sync { result = ThreadSafeArray(self.array.filter(isIncluded)) } + return result! + } + + /// Returns the first index in which an element of the collection satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match. + /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil. + func index(where predicate: (Element) -> Bool) -> Int? { + var result: Int? + queue.sync { result = self.array.firstIndex(where: predicate) } + return result + } + + /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements. + /// + /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false. + /// - Returns: A sorted array of the collection’s elements. + func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> ThreadSafeArray { + var result: ThreadSafeArray? + queue.sync { result = ThreadSafeArray(self.array.sorted(by: areInIncreasingOrder)) } + return result! + } + + /// Returns an array containing the results of mapping the given closure over the sequence’s elements. + /// + /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. + /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. + func map(_ transform: @escaping (Element) -> ElementOfResult) -> [ElementOfResult] { + var result = [ElementOfResult]() + queue.sync { result = self.array.map(transform) } + return result + } + + /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. + /// + /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. + /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. + func compactMap(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] { + var result = [ElementOfResult]() + queue.sync { result = self.array.compactMap(transform) } + return result + } + + /// Returns the result of combining the elements of the sequence using the given closure. + /// + /// - Parameters: + /// - initialResult: The value to use as the initial accumulating value. initialResult is passed to nextPartialResult the first time the closure is executed. + /// - nextPartialResult: A closure that combines an accumulating value and an element of the sequence into a new accumulating value, to be used in the next call of the nextPartialResult closure or returned to the caller. + /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. + func reduce(_ initialResult: ElementOfResult, _ nextPartialResult: @escaping (ElementOfResult, Element) -> ElementOfResult) -> ElementOfResult { + var result: ElementOfResult? + queue.sync { result = self.array.reduce(initialResult, nextPartialResult) } + return result ?? initialResult + } + + /// Returns the result of combining the elements of the sequence using the given closure. + /// + /// - Parameters: + /// - initialResult: The value to use as the initial accumulating value. + /// - updateAccumulatingResult: A closure that updates the accumulating value with an element of the sequence. + /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. + func reduce(into initialResult: ElementOfResult, _ updateAccumulatingResult: @escaping (inout ElementOfResult, Element) -> Void) -> ElementOfResult { + var result: ElementOfResult? + queue.sync { result = self.array.reduce(into: initialResult, updateAccumulatingResult) } + return result ?? initialResult + } + + /// Calls the given closure on each element in the sequence in the same order as a for-in loop. + /// + /// - Parameter body: A closure that takes an element of the sequence as a parameter. + func forEach(_ body: (Element) -> Void) { + queue.sync { self.array.forEach(body) } + } + + /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match. + /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false. + func contains(where predicate: (Element) -> Bool) -> Bool { + var result = false + queue.sync { result = self.array.contains(where: predicate) } + return result + } + + /// Returns a Boolean value indicating whether every element of a sequence satisfies a given predicate. + /// + /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element satisfies a condition. + /// - Returns: true if the sequence contains only elements that satisfy predicate; otherwise, false. + func allSatisfy(_ predicate: (Element) -> Bool) -> Bool { + var result = false + queue.sync { result = self.array.allSatisfy(predicate) } + return result + } + + /// Returns the array + /// + /// - Returns: the array part. + func getArray() -> [Element]? { + var results: [Element]? + queue.sync { results = self.array } + return results + } +} + +// MARK: - Mutable + +public extension ThreadSafeArray { + + /// Adds a new element at the end of the array. + /// + /// - Parameter element: The element to append to the array. + func append(_ element: Element) { + queue.async(flags: .barrier) { + self.array.append(element) + } + } + + /// Adds new elements at the end of the array. + /// + /// - Parameter element: The elements to append to the array. + func append(_ elements: [Element]) { + queue.async(flags: .barrier) { + self.array += elements + } + } + + /// Inserts a new element at the specified position. + /// + /// - Parameters: + /// - element: The new element to insert into the array. + /// - index: The position at which to insert the new element. + func insert(_ element: Element, at index: Int) { + queue.async(flags: .barrier) { + self.array.insert(element, at: index) + } + } + + /// Removes and returns the element at the specified position. + /// + /// - Parameters: + /// - index: The position of the element to remove. + /// - completion: The handler with the removed element. + func remove(at index: Int, completion: ((Element) -> Void)? = nil) { + queue.async(flags: .barrier) { + let element = self.array.remove(at: index) + DispatchQueue.main.async { completion?(element) } + } + } + + /// Removes and returns the elements that meet the criteria. + /// + /// - Parameters: + /// - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. + /// - completion: The handler with the removed elements. + func remove(where predicate: @escaping (Element) -> Bool, completion: (([Element]) -> Void)? = nil) { + queue.async(flags: .barrier) { + var elements = [Element]() + + while let index = self.array.firstIndex(where: predicate) { + elements.append(self.array.remove(at: index)) + } + + DispatchQueue.main.async { completion?(elements) } + } + } + + /// Removes all elements from the array. + /// + /// - Parameter completion: The handler with the removed elements. + func removeAll(completion: (([Element]) -> Void)? = nil) { + queue.async(flags: .barrier) { + let elements = self.array + self.array.removeAll() + DispatchQueue.main.async { completion?(elements) } + } + } +} + +public extension ThreadSafeArray { + + /// Accesses the element at the specified position if it exists. + /// + /// - Parameter index: The position of the element to access. + /// - Returns: optional element if it exists. + subscript(index: Int) -> Element? { + get { + var result: Element? + + queue.sync { + guard self.array.startIndex.. Bool { + var result = false + queue.sync { result = self.array.contains(element) } + return result + } +} + +// MARK: - Infix operators + +public extension ThreadSafeArray { + + /// Adds a new element at the end of the array. + /// + /// - Parameters: + /// - left: The collection to append to. + /// - right: The element to append to the array. + static func += (left: inout ThreadSafeArray, right: Element) { + left.append(right) + } + + /// Adds new elements at the end of the array. + /// + /// - Parameters: + /// - left: The collection to append to. + /// - right: The elements to append to the array. + static func += (left: inout ThreadSafeArray, right: [Element]) { + left.append(right) + } +} diff --git a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift index 00b57c56..e32a8ab3 100644 --- a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift @@ -29,7 +29,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { let serverUrl = "\(baseUrl)/remote.php/dav/files/\(userId)" let serverUrlFileName = "\(serverUrl)/\(folderName)" - NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: baseUrl) + NextcloudKit.shared.appendSession(account: account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") // Test creating folder NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in @@ -45,7 +45,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { XCTAssertEqual(self.account, account) XCTAssertEqual(NKError.success.errorCode, error.errorCode) XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) - XCTAssertEqual(files[0].fileName, folderName) + XCTAssertEqual(files?[0].fileName, folderName) Thread.sleep(forTimeInterval: 0.2) @@ -63,7 +63,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { XCTAssertEqual(404, error.errorCode) XCTAssertEqual(self.account, account) - XCTAssertTrue(files.isEmpty) + XCTAssertTrue(files?.isEmpty ?? false) } } } diff --git a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift index 1da28e7e..68c8339f 100644 --- a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift @@ -21,6 +21,7 @@ // import XCTest +import Alamofire @testable import NextcloudKit final class ShareIntegrationTests: BaseIntegrationXCTestCase { @@ -31,7 +32,7 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { let serverUrl = "\(baseUrl)/remote.php/dav/files/\(userId)" let serverUrlFileName = "\(serverUrl)/\(folderName)" - NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: baseUrl) + NextcloudKit.shared.appendSession(account: account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in XCTAssertEqual(self.account, account) @@ -43,7 +44,7 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { let note = "Test note" - NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: account) { account, share, data, error in + NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: "") { account, share, data, error in defer { expectation.fulfill() } XCTAssertEqual(self.account, account) diff --git a/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift b/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift index 8e2299ba..2b0f4257 100644 --- a/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift +++ b/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift @@ -70,42 +70,42 @@ class FileNameValidatorTests: XCTestCase { func testValidFolderAndFilePaths() { let folderPath = "validFolder" - let result = fileNameValidator.checkFolderPath(folderPath: folderPath) + let result = fileNameValidator.checkFolderPath(folderPath) XCTAssertTrue(result) } func testFolderPathWithReservedName() { let folderPath = "CON" - let result = fileNameValidator.checkFolderPath(folderPath: folderPath) + let result = fileNameValidator.checkFolderPath(folderPath) XCTAssertFalse(result) } func testFolderPathWithInvalidCharacter() { let folderPath = "invalid Date: Tue, 24 Sep 2024 16:23:58 +0200 Subject: [PATCH 059/135] NextcloudKit V 5 Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 51 ++---- Sources/NextcloudKit/NextcloudKit+API.swift | 108 +++++++++--- .../NextcloudKit/NextcloudKit+Assistant.swift | 50 +++--- .../NextcloudKit/NextcloudKit+Comments.swift | 67 ++++--- .../NextcloudKit/NextcloudKit+Dashboard.swift | 36 ++-- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 96 +++++----- .../NextcloudKit/NextcloudKit+FilesLock.swift | 7 +- .../NextcloudKit+Groupfolders.swift | 11 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 11 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 13 +- .../NextcloudKit/NextcloudKit+NCText.swift | 44 ++--- .../NextcloudKit+PushNotification.swift | 28 +-- .../NextcloudKit+Richdocuments.swift | 48 ++--- .../NextcloudKit/NextcloudKit+Search.swift | 22 ++- Sources/NextcloudKit/NextcloudKit+Share.swift | 50 +++--- .../NextcloudKit/NextcloudKit+Upload.swift | 6 +- .../NextcloudKit+UserStatus.swift | 63 ++++--- .../NextcloudKit/NextcloudKit+WebDAV.swift | 165 +++++++++--------- Sources/NextcloudKit/NextcloudKit.swift | 2 +- .../NextcloudKit/NextcloudKitBackground.swift | 39 +++-- .../Utils/FileNameValidator.swift | 31 ++-- 21 files changed, 504 insertions(+), 444 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 948a4c84..83615561 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -74,11 +74,8 @@ public enum NKProperties: String, CaseIterable { case systemtags = "" case filemetadatasize = "" case filemetadatagps = "" - case metadataphotosexif = "" - case metadataphotosgps = "" - case metadataphotosoriginaldatetime = "" - case metadataphotoplace = "" case metadataphotossize = "" + case metadataphotosgps = "" case metadatafileslivephoto = "" case hidden = "" /// open-collaboration-services.org @@ -231,15 +228,6 @@ public class NKFile: NSObject { public var livePhotoFile = "" /// Indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side public var isFlaggedAsLivePhotoByServer = false - /// - public var datePhotosOriginal: Date? - /// - public struct ChildElement { - let name: String - let text: String? - } - public var exifPhotos = [[String: String?]]() - public var placePhotos: String? } public class NKFileProperty: NSObject { @@ -619,10 +607,10 @@ class NKDataFileXML: NSObject { return xml["ocs", "data", "apppassword"].text } - func convertDataFile(xmlData: Data, dav: String, urlBase: String, user: String, userId: String, account: String, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] { + func convertDataFile(xmlData: Data, nkSession: NKSession, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] { var files: [NKFile] = [] - let rootFiles = "/" + dav + "/files/" - guard let baseUrl = self.nkCommonInstance.getHostName(urlString: urlBase) else { + let rootFiles = "/" + nkSession.dav + "/files/" + guard let baseUrl = self.nkCommonInstance.getHostName(urlString: nkSession.urlBase) else { return files } let xml = XML.parse(xmlData) @@ -653,7 +641,7 @@ class NKDataFileXML: NSObject { } // account - file.account = account + file.account = nkSession.account // path file.path = (fileNamePath as NSString).deletingLastPathComponent + "/" @@ -664,7 +652,7 @@ class NKDataFileXML: NSObject { file.fileName = file.fileName.removingPercentEncoding ?? "" // ServerUrl - if href == rootFiles + user + "/" { + if href == rootFiles + nkSession.user + "/" { file.fileName = "." file.serverUrl = ".." } else { @@ -870,42 +858,29 @@ class NKDataFileXML: NSObject { file.hidden = (hidden as NSString).boolValue } - if let datePhotosOriginal = propstat["d:prop", "nc:metadata-photos-original_date_time"].double, datePhotosOriginal > 0 { - file.datePhotosOriginal = Date(timeIntervalSince1970: datePhotosOriginal) - } - - let exifPhotosElements = propstat["d:prop", "nc:metadata-photos-exif"] - if let element = exifPhotosElements.element { - for child in element.childElements { - file.exifPhotos.append([child.name: child.text]) - } - } - - file.placePhotos = propstat["d:prop", "nc:metadata-photos-place"].text - let results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) file.contentType = results.mimeType file.iconName = results.iconName file.name = "files" file.classFile = results.classFile - file.urlBase = urlBase - file.user = user - file.userId = userId + file.urlBase = nkSession.urlBase + file.user = nkSession.user + file.userId = nkSession.userId files.append(file) } // Live photo detect files = files.sorted { - return ($0.serverUrl, $0.fileName.withRemovedFileExtension, $0.classFile) < ($1.serverUrl, $1.fileName.withRemovedFileExtension, $1.classFile) + return ($0.serverUrl, ($0.fileName as NSString).deletingPathExtension, $0.classFile) < ($1.serverUrl, ($1.fileName as NSString).deletingPathExtension, $1.classFile) } for index in files.indices { if !files[index].livePhotoFile.isEmpty || files[index].directory { continue } if index < files.count - 1, - files[index].fileName.withRemovedFileExtension == files[index + 1].fileName.withRemovedFileExtension, + (files[index].fileName as NSString).deletingPathExtension == (files[index + 1].fileName as NSString) .deletingPathExtension, files[index].classFile == NKCommon.TypeClassFile.image.rawValue, files[index + 1].classFile == NKCommon.TypeClassFile.video.rawValue { files[index].livePhotoFile = files[index + 1].fileId @@ -916,10 +891,10 @@ class NKDataFileXML: NSObject { return files } - func convertDataTrash(xmlData: Data, urlBase: String, showHiddenFiles: Bool) -> [NKTrash] { + func convertDataTrash(xmlData: Data, nkSession: NKSession, showHiddenFiles: Bool) -> [NKTrash] { var files: [NKTrash] = [] var first: Bool = true - guard let baseUrl = self.nkCommonInstance.getHostName(urlString: urlBase) else { + guard let baseUrl = self.nkCommonInstance.getHostName(urlString: nkSession.urlBase) else { return files } let xml = XML.parse(xmlData) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index baaa2ea7..476ef6ce 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -263,8 +263,8 @@ public extension NextcloudKit { } func downloadPreview(fileId: String, - width: Int = 1024, - height: Int = 1024, + widthPreview: Int = 512, + heightPreview: Int = 512, etag: String? = nil, crop: Int = 1, cropMode: String = "cover", @@ -273,12 +273,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ width: Int, _ height: Int, _ etag: String?, _ error: NKError) -> Void) { - let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, width, height, nil, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } if var etag = etag { @@ -296,21 +296,51 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, width, height, nil, error) } + options.queue.async { completion(account, nil, error) } case .success: if let data = response.data { - let etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields)?.replacingOccurrences(of: "\"", with: "") - options.queue.async { completion(account, data, width, height, etag, .success) } + options.queue.async { completion(account, data, .success) } } else { - options.queue.async { completion(account, nil, width, height, nil, .invalidData) } + options.queue.async { completion(account, nil, .invalidData) } } } } } + func downloadPreview(fileId: String, + fileNamePreviewLocalPath: String, + fileNameIconLocalPath: String? = nil, + widthPreview: Int = 512, + heightPreview: Int = 512, + sizeIcon: Int = 512, + compressionQualityPreview: CGFloat = 0.5, + compressionQualityIcon: CGFloat = 0.5, + etag: String? = nil, + crop: Int = 1, + cropMode: String = "cover", + forceIcon: Int = 0, + mimeFallback: Int = 0, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { + let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + + downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: etag, endpoint: endpoint, account: account, options: options) { task in + taskHandler(task) + } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in + completion(account, imagePreview, imageIcon, imageOriginal,etag,error) + } + } + func downloadTrashPreview(fileId: String, - width: Int = 512, - height: Int = 512, + fileNamePreviewLocalPath: String, + fileNameIconLocalPath: String, + widthPreview: Int = 512, + heightPreview: Int = 512, + sizeIcon: Int = 512, + compressionQualityPreview: CGFloat = 0.5, + compressionQualityIcon: CGFloat = 0.5, crop: Int = 1, cropMode: String = "cover", forceIcon: Int = 0, @@ -318,13 +348,35 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ width: Int, _ height: Int, _ error: NKError) -> Void) { - let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { + let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: nil, endpoint: endpoint, account: account, options: options) { task in + taskHandler(task) + } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in + completion(account, imagePreview, imageIcon, imageOriginal,etag,error) + } + } + + private func downloadPreview(fileNamePreviewLocalPath: String, + fileNameIconLocalPath: String? = nil, + sizeIcon: Int = 0, + compressionQualityPreview: CGFloat, + compressionQualityIcon: CGFloat, + etag: String? = nil, + endpoint: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, width, height, .urlError) } + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } + } + if var etag = etag { + etag = "\"" + etag + "\"" + headers.update(name: "If-None-Match", value: etag) } nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -337,12 +389,28 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, width, height, error) } + options.queue.async { completion(account, nil, nil, nil, nil, error) } case .success: - if let data = response.data { - options.queue.async { completion(account, data, width, height, .success) } - } else { - options.queue.async { completion(account, nil, width, height, .invalidData) } + guard let data = response.data, let imageOriginal = UIImage(data: data) else { + return options.queue.async { completion(account, nil, nil, nil, nil, .invalidData) } + } + let etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields)?.replacingOccurrences(of: "\"", with: "") + var imagePreview, imageIcon: UIImage? + do { + if let data = imageOriginal.jpegData(compressionQuality: compressionQualityPreview) { + try data.write(to: URL(fileURLWithPath: fileNamePreviewLocalPath), options: .atomic) + imagePreview = UIImage(data: data) + } + if let fileNameIconLocalPath, sizeIcon > 0 { + imageIcon = imageOriginal.resizeImage(size: CGSize(width: sizeIcon, height: sizeIcon)) + if let data = imageIcon?.jpegData(compressionQuality: compressionQualityIcon) { + try data.write(to: URL(fileURLWithPath: fileNameIconLocalPath), options: .atomic) + imageIcon = UIImage(data: data) + } + } + options.queue.async { completion(account, imagePreview, imageIcon, imageOriginal, etag, .success) } + } catch { + options.queue.async { completion(account, nil, nil, nil, nil, NKError(error: error)) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 15e75380..6b02e271 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -30,14 +30,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ types: [NKTextProcessingTaskType]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/textprocessing/tasktypes" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -70,15 +70,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/schedule" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -108,14 +108,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -145,14 +145,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -182,14 +182,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ task: [NKTextProcessingTask]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "/ocs/v2.php/textprocessing/tasks/app/\(appId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 61bd2174..3667b298 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -30,14 +30,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ items: [NKComments]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPFIND") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -47,7 +51,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -76,13 +80,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" + /// + options.contentType = "application/json" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/json") var urlRequest: URLRequest do { @@ -93,7 +101,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -118,14 +126,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)/\(messageId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -136,7 +148,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -160,15 +172,16 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)/\(messageId)" + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)/\(messageId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -191,14 +204,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlEndpoint = urlBase + "/" + dav + "/comments/files/\(fileId)" + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -209,7 +226,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 4c4651eb..4c4efe1a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -31,20 +31,14 @@ public extension NextcloudKit { request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardWidgets: [NCCDashboardWidget]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var url: URLConvertible? - if let endpoint = options.endpoint { - url = URL(string: endpoint) - } else { - let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widgets" - url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) + let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widgets" + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - guard let url = url else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -76,20 +70,14 @@ public extension NextcloudKit { request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ dashboardApplications: [NCCDashboardApplication]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var url: URLConvertible? - if let endpoint = options.endpoint { - url = URL(string: endpoint) - } else { - let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widget-items?widgets[]=\(items)" - url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - } - guard let url = url else { - return options.queue.async { completion(account, nil, nil, .urlError) } + let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widget-items?widgets[]=\(items)" + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let dashboardRequest = sessionManager.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index be39affc..2dee0358 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -32,19 +32,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/encrypted/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } let method: HTTPMethod = delete ? .delete : .put - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -75,17 +75,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eToken: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/lock/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: method) - var headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] if let e2eToken { @@ -96,7 +96,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-COUNTER", value: e2eCounter) } - sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -126,23 +126,22 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ e2eMetadata: String?, _ signature: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] - if let e2eToken { parameters["e2e-token"] = e2eToken } - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -176,22 +175,20 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ metadata: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/meta-data/\(fileId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: method) - var headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters: [String: Any] = [:] - parameters["e2e-token"] = e2eToken headers.update(name: "e2e-token", value: e2eToken) - if let e2eMetadata { parameters["metaData"] = e2eMetadata } @@ -199,7 +196,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-SIGNATURE", value: signature) } - sessionManager.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -230,8 +227,7 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ certificateUser: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - let userId = self.nkCommonInstance.userId + var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -245,12 +241,13 @@ public extension NextcloudKit { } else { endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -265,7 +262,7 @@ public extension NextcloudKit { let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { - let key = json["ocs"]["data"]["public-keys"][userId].stringValue + let key = json["ocs"]["data"]["public-keys"][nkSession.userId].stringValue if let user = user { let keyUser = json["ocs"]["data"]["public-keys"][user].string options.queue.async { completion(account, key, keyUser, jsonData, .success) } @@ -283,18 +280,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -322,18 +319,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/server-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -362,19 +359,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ certificate: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = ["csr": certificate] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -404,19 +401,19 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = ["privateKey": privateKey] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -444,18 +441,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/public-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -476,18 +472,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var version = "v1" if let optionsVesion = options.version { version = optionsVesion } let endpoint = "ocs/v2.php/apps/end_to_end_encryption/api/\(version)/private-key" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index 851915df..2cdfa92a 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -38,10 +38,13 @@ public extension NextcloudKit { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: shouldLock ? "LOCK" : "UNLOCK") - var headers = self.nkCommonInstance.getStandardHeaders(options: options) + guard let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } headers.update(name: "X-User-Lock", value: "1") - sessionManager.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 5c2b1e85..3c54f4bb 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -30,15 +30,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ results: [NKGroupfolders]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/apps/groupfolders/folders?applicable=1" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index e1964925..93331745 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -32,15 +32,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ result: NKHovercard?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/hovercard/v1/\(userId)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) - else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 8259d105..091047d5 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -31,13 +31,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlfileNamePath.encodedToUrl else { + guard let url = serverUrlfileNamePath.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account) else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") + /// + options.contentType = "application/xml" + /// + guard let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } var urlRequest: URLRequest - do { try urlRequest = URLRequest(url: url, method: method, headers: headers) let parameters = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyLivephoto, livePhotoFile) @@ -46,7 +51,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index f6d7cfc2..a23ab7c1 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -26,20 +26,20 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func NCTextObtainEditorDetails(account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase + func NCTextObtainEditorDetails(account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing" var editors: [NKEditorDetailsEditors] = [] var creators: [NKEditorDetailsCreators] = [] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, editors, creators, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -96,7 +96,6 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -104,12 +103,13 @@ public extension NextcloudKit { if let fileId = fileId { endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/open?path=/\(fileNamePath)&fileId=\(fileId)&editorId=\(editor)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -131,16 +131,16 @@ public extension NextcloudKit { func NCTextGetListOfTemplates(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ templates: [NKEditorTemplates], _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase + completion: @escaping (_ account: String, _ templates: [NKEditorTemplates]?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/templates/text/textdocumenttemplate" var templates: [NKEditorTemplates] = [] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, templates, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -178,7 +178,6 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -188,12 +187,13 @@ public extension NextcloudKit { } else { endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/create?path=/\(fileNamePath)&editorId=\(editorId)&creatorId=\(creatorId)&templateId=\(templateId)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index 656704ca..2c1cc184 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -27,8 +27,6 @@ import SwiftyJSON public extension NextcloudKit { func subscribingPushNotification(serverUrl: String, - user: String, - password: String, pushTokenHash: String, devicePublicKey: String, proxyServerUrl: String, @@ -37,7 +35,9 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } let parameters = [ @@ -45,9 +45,8 @@ public extension NextcloudKit { "devicePublicKey": devicePublicKey, "proxyServer": proxyServerUrl ] - let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -74,19 +73,18 @@ public extension NextcloudKit { } func unsubscribingPushNotification(serverUrl: String, - user: String, - password: String, account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(user: user, password: password, appendHeaders: options.customHeader, customUserAgent: options.customUserAgent) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -113,7 +111,8 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "devices?format=json" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint, options: options), let userAgent = options.customUserAgent else { return options.queue.async { completion(account, .urlError) } } @@ -125,7 +124,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -151,7 +150,8 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { let endpoint = "devices" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint), + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint, options: options), let userAgent = options.customUserAgent else { return options.queue.async { completion(account, .urlError) } } @@ -162,7 +162,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - sessionManager.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index a4a55f4d..5cb9d6e9 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -26,20 +26,20 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - func createUrlRichdocuments(fileID: String, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase + func createUrlRichdocuments(fileID: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/document" - let parameters: [String: Any] = ["fileId": fileID] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) + let parameters: [String: Any] = ["fileId": fileID] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -67,14 +67,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ templates: [NKRichdocumentsTemplate]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/\(typeTemplate)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -115,15 +115,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/new" - let parameters: [String: Any] = ["path": path, "template": templateId] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) + let parameters: [String: Any] = ["path": path, "template": templateId] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -151,15 +151,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "index.php/apps/richdocuments/assets" - let parameters: [String: Any] = ["path": path] - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) + let parameters: [String: Any] = ["path": path] - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index e501727f..d89f8a3d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -52,14 +52,14 @@ public extension NextcloudKit { providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, update: @escaping (_ account: String, _ searchResult: NKSearchResult?, _ provider: NKSearchProvider, _ error: NKError) -> Void, completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/search/providers" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return completion(account, nil, .urlError) + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - let requestUnifiedSearch = sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestUnifiedSearch = nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -122,8 +122,9 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, NKSearchResult?, _ data: Data?, _ error: NKError) -> Void) -> DataRequest? { - let urlBase = self.nkCommonInstance.urlBase - guard let term = term.urlEncoded else { + guard let term = term.urlEncoded, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { completion(account, nil, nil, .urlError) return nil } @@ -134,14 +135,11 @@ public extension NextcloudKit { if let cursor = cursor { endpoint += "&cursor=\(cursor)" } - guard let url = self.nkCommonInstance.createStandardUrl( - serverUrl: urlBase, - endpoint: endpoint) + guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options) else { completion(account, nil, nil, .urlError) return nil } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var urlRequest: URLRequest do { @@ -152,7 +150,7 @@ public extension NextcloudKit { return nil } - let requestSearchProvider = sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestSearchProvider = nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 70186482..961ae284 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -80,14 +80,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ shares: [NKShare]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: parameters.endpoint) - else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: parameters.endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -132,16 +131,16 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ sharees: [NKSharee]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/sharees" var lookupString = "false" if lookup { lookupString = "true" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = [ "search": search, "page": String(page), @@ -150,7 +149,7 @@ public extension NextcloudKit { "lookup": lookupString ] - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -290,12 +289,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "path": path, "shareType": String(shareType), @@ -320,7 +319,7 @@ public extension NextcloudKit { parameters["attributes"] = attributes } - sessionManager.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -376,12 +375,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil,nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "permissions": String(permissions) ] @@ -405,7 +404,7 @@ public extension NextcloudKit { parameters["attributes"] = "[]" } - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -429,21 +428,21 @@ public extension NextcloudKit { } /* - * @param idShare Identifier of the share to update + * @param idShare: Identifier of the share to update */ func deleteShare(idShare: Int, account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -465,6 +464,7 @@ public extension NextcloudKit { private func convertResponseShare(json: JSON, account: String) -> NKShare { let share = NKShare() + share.account = account share.canDelete = json["can_delete"].boolValue share.canEdit = json["can_edit"].boolValue share.displaynameFileOwner = json["displayname_file_owner"].stringValue diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index ca98a185..b7250445 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -30,7 +30,6 @@ public extension NextcloudKit { fileNameLocalPath: String, dateCreationFile: Date? = nil, dateModificationFile: Date? = nil, - overwrite: Bool = false, account: String, options: NKRequestOptions = NKRequestOptions(), requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in }, @@ -58,9 +57,6 @@ public extension NextcloudKit { if let dateModificationFile, dateModificationFile.timeIntervalSince1970 > 0 { headers.update(name: "X-OC-MTime", value: "\(dateModificationFile.timeIntervalSince1970)") } - if overwrite { - headers.update(name: "Overwrite", value: "true") - } let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in task.taskDescription = options.taskDescription @@ -143,7 +139,7 @@ public extension NextcloudKit { if options.customHeader == nil { options.customHeader = [:] } - options.customHeader?["Destination"] = serverUrlFileName.urlEncoded + options.customHeader?["Destination"] = serverUrlFileName options.customHeader?["OC-Total-Length"] = String(fileNameLocalSize) // check space diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index ce488f90..cb18b7c0 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -31,17 +31,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ clearAt: Date?, _ icon: String?, _ message: String?, _ messageId: String?, _ messageIsPredefined: Bool, _ status: String?, _ statusIsUserDefined: Bool, _ userId: String?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase var endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status" if let userId = userId { endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/\(userId)" } - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -82,17 +82,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/status" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = [ "statusType": String(status) ] - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -121,12 +121,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/predefined" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "messageId": String(messageId) ] @@ -134,7 +134,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -165,12 +165,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/custom" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var parameters = [ "message": String(message) ] @@ -181,7 +181,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - sessionManager.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -209,14 +209,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -244,15 +244,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var userStatuses: [NKUserStatus] = [] let endpoint = "ocs/v2.php/apps/user_status/api/v1/predefined_statuses" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) - sessionManager.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -264,11 +263,11 @@ public extension NextcloudKit { let error = NKError(error: error, afResponse: response, responseData: response.data) options.queue.async { completion(account, nil, nil, error) } case .success(let jsonData): + var userStatuses: [NKUserStatus] = [] let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { let ocsdata = json["ocs"]["data"] - for (_, subJson): (String, JSON) in ocsdata { let userStatus = NKUserStatus() @@ -298,19 +297,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { - let urlBase = self.nkCommonInstance.urlBase - var userStatuses: [NKUserStatus] = [] let endpoint = "ocs/v2.php/apps/user_status/api/v1/statuses" - guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: urlBase, endpoint: endpoint) else { + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) let parameters = [ "limit": String(limit), "offset": String(offset) ] - sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -324,12 +322,11 @@ public extension NextcloudKit { case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError + var userStatuses: [NKUserStatus] = [] if statusCode == 200 { let ocsdata = json["ocs"]["data"] - for (_, subJson): (String, JSON) in ocsdata { let userStatus = NKUserStatus() - if let value = subJson["clearAt"].double { if value > 0 { userStatus.clearAt = Date(timeIntervalSince1970: value) diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 31104440..81ad2d47 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -31,15 +31,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account - - guard let url = serverUrlFileName.encodedToUrl else { + guard let url = serverUrlFileName.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: "MKCOL") - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var urlRequest: URLRequest - do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -47,7 +45,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -78,12 +76,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - guard let url = serverUrlFileName.encodedToUrl else { + guard let url = serverUrlFileName.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } - let headers = self.nkCommonInstance.getStandardHeaders(options: options) var urlRequest: URLRequest - do { try urlRequest = URLRequest(url: url, method: .delete, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -91,7 +89,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -115,13 +113,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account - - guard let url = serverUrlFileNameSource.encodedToUrl else { + guard let url = serverUrlFileNameSource.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "MOVE") - var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") if overwrite { headers.update(name: "Overwrite", value: "T") @@ -129,7 +126,6 @@ public extension NextcloudKit { headers.update(name: "Overwrite", value: "F") } var urlRequest: URLRequest - do { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout @@ -137,7 +133,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -161,13 +157,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let account = self.nkCommonInstance.account - - guard let url = serverUrlFileNameSource.encodedToUrl else { + guard let url = serverUrlFileNameSource.encodedToUrl, + let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "COPY") - var headers = self.nkCommonInstance.getStandardHeaders(options: options) headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") if overwrite { headers.update(name: "Overwrite", value: "T") @@ -183,7 +178,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -208,15 +203,16 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let user = self.nkCommonInstance.user - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { var files: [NKFile] = [] var serverUrlFileName = serverUrlFileName - guard let url = serverUrlFileName.encodedToUrl else { - return options.queue.async { completion(account, files, nil, .urlError) } + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = serverUrlFileName.encodedToUrl, + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } } if depth == "0", serverUrlFileName.last == "/" { serverUrlFileName = String(serverUrlFileName.dropLast()) @@ -224,7 +220,6 @@ public extension NextcloudKit { serverUrlFileName = serverUrlFileName + "/" } let method = HTTPMethod(rawValue: "PROPFIND") - var headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") headers.update(name: "Depth", value: depth) var urlRequest: URLRequest @@ -240,7 +235,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -253,7 +248,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -268,26 +263,27 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ file: NKFile?, _ data: Data?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase + guard let nkSession = nkCommonInstance.getSession(account: account) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } var httpBody: Data? if let fileId = fileId { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), nkSession.userId, fileId).data(using: .utf8) } else if let link = link { let linkArray = link.components(separatedBy: "/") if let fileId = linkArray.last { - httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), userId, fileId).data(using: .utf8) + httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileId(createProperties: options.createProperties, removeProperties: options.removeProperties), nkSession.userId, fileId).data(using: .utf8) } } guard let httpBody = httpBody else { return options.queue.async { completion(account, nil, nil, .urlError) } } - search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], account: account, options: options) { task in + search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], account: account, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) } completion: { account, files, data, error in - options.queue.async { completion(account, files.first, data, error) } + options.queue.async { completion(account, files?.first, data, error) } } } @@ -298,7 +294,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { if let httpBody = requestBody.data(using: .utf8) { search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) @@ -316,10 +312,10 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - guard let href = ("/files/" + userId).urlEncoded else { - return options.queue.async { completion(account, [], nil, .urlError) } + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + guard let nkSession = nkCommonInstance.getSession(account: account), + let href = ("/files/" + nkSession.userId).urlEncoded else { + return options.queue.async { completion(account, nil, nil, .urlError) } } let requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchFileName(createProperties: options.createProperties, removeProperties: options.removeProperties), href, depth, "%" + literal + "%") if let httpBody = requestBody.data(using: .utf8) { @@ -341,12 +337,13 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + guard let nkSession = nkCommonInstance.getSession(account: account) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } let files: [NKFile] = [] var greaterDateString: String?, lessDateString: String? - let href = "/files/" + userId + path + let href = "/files/" + nkSession.userId + path if let lessDate = lessDate as? Date { lessDateString = self.nkCommonInstance.convertDate(lessDate, format: "yyyy-MM-dd'T'HH:mm:ssZZZZZ") } else if let lessDate = lessDate as? Int { @@ -371,7 +368,7 @@ public extension NextcloudKit { } if let httpBody = requestBody.data(using: .utf8) { - search(serverUrl: urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in + search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) } completion: { account, files, data, error in options.queue.async { completion(account, files, data, error) } @@ -386,17 +383,19 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let user = self.nkCommonInstance.user - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } var files: [NKFile] = [] - guard let url = (serverUrl + "/" + dav).encodedToUrl else { + guard let url = (serverUrl + "/" + nkSession.dav).encodedToUrl else { return options.queue.async { completion(account, files, nil, .urlError) } } let method = HTTPMethod(rawValue: "SEARCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: method, headers: headers) @@ -406,7 +405,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -419,7 +418,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -434,15 +433,18 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + "/" + fileName + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + "/" + fileName guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -454,7 +456,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -476,18 +478,20 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile], _ data: Data?, _ error: NKError) -> Void) { - let user = self.nkCommonInstance.user - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - let serverUrlFileName = urlBase + "/" + dav + "/files/" + userId + completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId var files: [NKFile] = [] guard let url = serverUrlFileName.encodedToUrl else { return options.queue.async { completion(account, files, nil, .urlError) } } let method = HTTPMethod(rawValue: "REPORT") - let headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") var urlRequest: URLRequest do { @@ -498,7 +502,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -511,7 +515,7 @@ public extension NextcloudKit { options.queue.async { completion(account, files, nil, error) } case .success: if let xmlData = response.data { - files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, dav: dav, urlBase: urlBase, user: user, userId: userId, account: account, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) + files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) options.queue.async { completion(account, files, xmlData, .success) } } else { options.queue.async { completion(account, files, nil, .xmlError) } @@ -525,11 +529,15 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ items: [NKTrash], _ data: Data?, _ error: NKError) -> Void) { - let userId = self.nkCommonInstance.userId - let urlBase = self.nkCommonInstance.urlBase - let dav = self.nkCommonInstance.dav - var serverUrlFileName = urlBase + "/" + dav + "/trashbin/" + userId + "/trash/" + completion: @escaping (_ account: String, _ items: [NKTrash]?, _ data: Data?, _ error: NKError) -> Void) { + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + var serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/trashbin/" + nkSession.userId + "/trash/" if let filename { serverUrlFileName = serverUrlFileName + filename } @@ -538,7 +546,6 @@ public extension NextcloudKit { return options.queue.async { completion(account, items, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPFIND") - var headers = self.nkCommonInstance.getStandardHeaders(options.customHeader, customUserAgent: options.customUserAgent, contentType: "application/xml") headers.update(name: "Depth", value: "1") var urlRequest: URLRequest @@ -550,7 +557,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, items, nil, NKError(error: error)) } } - sessionManager.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -563,7 +570,7 @@ public extension NextcloudKit { options.queue.async { completion(account, items, nil, error) } case .success: if let xmlData = response.data { - items = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataTrash(xmlData: xmlData, urlBase: urlBase, showHiddenFiles: showHiddenFiles) + items = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataTrash(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles) options.queue.async { completion(account, items, xmlData, .success) } } else { options.queue.async { completion(account, items, nil, .xmlError) } diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 553361d1..bd85711e 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -116,7 +116,7 @@ open class NextcloudKit { } } - public func getSession(account: String) -> NKSession? { + public func getSessionhttpResponse(account: String) -> NKSession? { return nkCommonInstance.nksessions.filter({ $0.account == account }).first } diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index ffcbce9a..9a304eb5 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -36,26 +36,28 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, public func download(serverUrlFileName: Any, fileNameLocalPath: String, taskDescription: String? = nil, - account: String, - session: URLSession) -> URLSessionDownloadTask? { + account: String) -> URLSessionDownloadTask? { var url: URL? if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { url = (serverUrlFileName as? String)?.encodedToUrl as? URL } + guard let nkSession = nkCommonInstance.getSession(account: account) else { + return nil + } guard let urlForRequest = url else { return nil } var request = URLRequest(url: urlForRequest) - let loginString = "\(self.nkCommonInstance.user):\(self.nkCommonInstance.password)" + let loginString = "\(nkSession.user):\(nkSession.password)" guard let loginData = loginString.data(using: String.Encoding.utf8) else { return nil } let base64LoginString = loginData.base64EncodedString() - request.setValue(self.nkCommonInstance.userAgent, forHTTPHeaderField: "User-Agent") + request.setValue(nkSession.userAgent, forHTTPHeaderField: "User-Agent") request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") - let task = session.downloadTask(with: request) + let task = nkSession.sessionDownloadBackground.downloadTask(with: request) task.taskDescription = taskDescription task.resume() self.nkCommonInstance.writeLog("Network start download file: \(serverUrlFileName)") @@ -70,10 +72,11 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, dateCreationFile: Date?, dateModificationFile: Date?, taskDescription: String? = nil, - overwrite: Bool = false, account: String, - session: URLSession) -> URLSessionUploadTask? { + sessionIdentifier: String) -> URLSessionUploadTask? { var url: URL? + var uploadSession: URLSession? + guard let nkSession = nkCommonInstance.getSession(account: account) else { return nil } if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { @@ -83,18 +86,15 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, return nil } var request = URLRequest(url: urlForRequest) - let loginString = "\(self.nkCommonInstance.user):\(self.nkCommonInstance.password)" + let loginString = "\(nkSession.user):\(nkSession.password)" guard let loginData = loginString.data(using: String.Encoding.utf8) else { return nil } let base64LoginString = loginData.base64EncodedString() request.httpMethod = "PUT" - request.setValue(self.nkCommonInstance.userAgent, forHTTPHeaderField: "User-Agent") + request.setValue(nkSession.userAgent, forHTTPHeaderField: "User-Agent") request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") - if overwrite { - request.setValue("true", forHTTPHeaderField: "Overwrite") - } // Epoch of linux do not permitted negativ value if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { request.setValue("\(dateCreationFile.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-CTime") @@ -104,11 +104,18 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, request.setValue("\(dateModificationFile.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-MTime") } - let task = session.uploadTask(with: request, fromFile: URL(fileURLWithPath: fileNameLocalPath)) - task.taskDescription = taskDescription - task.resume() - self.nkCommonInstance.writeLog("Network start upload file: \(serverUrlFileName)") + if sessionIdentifier == nkCommonInstance.identifierSessionUploadBackground { + uploadSession = nkSession.sessionUploadBackground + } else if sessionIdentifier == nkCommonInstance.identifierSessionUploadBackgroundWWan { + uploadSession = nkSession.sessionUploadBackgroundWWan + } else if sessionIdentifier == nkCommonInstance.identifierSessionUploadBackgroundExt { + uploadSession = nkSession.sessionUploadBackgroundExt + } + let task = uploadSession?.uploadTask(with: request, fromFile: URL(fileURLWithPath: fileNameLocalPath)) + task?.taskDescription = taskDescription + task?.resume() + self.nkCommonInstance.writeLog("Network start upload file: \(serverUrlFileName)") return task } diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index f76d1efa..44797879 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -28,21 +28,26 @@ public class FileNameValidator { return instance }() - public private(set) var forbiddenFileNames: [String] = [] { + public var forbiddenFileNames: [String] = [] { didSet { forbiddenFileNames = forbiddenFileNames.map({$0.uppercased()}) } } - - public private(set) var forbiddenFileNameBasenames: [String] = [] { + public var forbiddenFileNameBasenames: [String] = [] { didSet { forbiddenFileNameBasenames = forbiddenFileNameBasenames.map({$0.uppercased()}) } } - public private(set) var forbiddenFileNameCharacters: [String] = [] + private var forbiddenFileNameCharactersRegex: NSRegularExpression? + + public var forbiddenFileNameCharacters: [String] = [] { + didSet { + forbiddenFileNameCharactersRegex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]") + } + } - public private(set) var forbiddenFileNameExtensions: [String] = [] { + public var forbiddenFileNameExtensions: [String] = [] { didSet { forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.uppercased()}) } @@ -51,19 +56,19 @@ public class FileNameValidator { public let fileEndsWithSpacePeriodError = NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: NSLocalizedString("_file_name_validator_error_ends_with_space_period_", value: "File name ends with a space or a period.", comment: "")) public var fileReservedNameError: NKError { - let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_reserved_name_", value: "\"%@\" is a forbidden name.", comment: "") + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_reserved_name_", value: "%@ is a forbidden name.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } public var fileForbiddenFileExtensionError: NKError { - let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: ".\"%@\" is a forbidden file extension.", comment: "") + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: ".%@ is a forbidden file extension.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } public var fileInvalidCharacterError: NKError { - let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_invalid_character_", value: "Name contains an invalid character: \"%@\".", comment: "") + let errorMessageTemplate = NSLocalizedString("file_name_validator_error_invalid_character_", value: "Name contains an invalid character: %@.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } @@ -79,12 +84,12 @@ public class FileNameValidator { self.forbiddenFileNameExtensions = forbiddenFileNameExtensions } - public func checkFileName(_ filename: String) -> NKError? { + public func checkFileName(_ filename: String, existedFileNames: Set? = nil) -> NKError? { if filename.hasSuffix(" ") || filename.hasSuffix(".") { return fileEndsWithSpacePeriodError } - if let regex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]"), let invalidCharacterError = checkInvalidCharacters(string: filename, regex: regex) { + if let invalidCharacterError = checkInvalidCharacters(string: filename) { return invalidCharacterError } @@ -102,17 +107,17 @@ public class FileNameValidator { return nil } - public func checkFolderPath(_ folderPath: String) -> Bool { + public func checkFolderPath(folderPath: String) -> Bool { return folderPath.split { $0 == "/" || $0 == "\\" } .allSatisfy { checkFileName(String($0)) == nil } } - private func checkInvalidCharacters(string: String, regex: NSRegularExpression) -> NKError? { + private func checkInvalidCharacters(string: String) -> NKError? { for char in string { let charAsString = String(char) let range = NSRange(location: 0, length: charAsString.utf16.count) - if regex.firstMatch(in: charAsString, options: [], range: range) != nil { + if forbiddenFileNameCharactersRegex?.firstMatch(in: charAsString, options: [], range: range) != nil { templateString = charAsString return fileInvalidCharacterError } From caee63d9cd7e798598101688b0f85e34cc401674 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 24 Sep 2024 16:34:12 +0200 Subject: [PATCH 060/135] fix Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKModel.swift | 27 ++++- Sources/NextcloudKit/NextcloudKit+API.swift | 108 ++++-------------- .../NextcloudKit/NextcloudKit+Upload.swift | 6 +- Sources/NextcloudKit/NextcloudKit.swift | 2 +- .../NextcloudKit/NextcloudKitBackground.swift | 4 + .../Utils/FileNameValidator.swift | 31 +++-- 6 files changed, 69 insertions(+), 109 deletions(-) diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 83615561..78a668c3 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -74,8 +74,11 @@ public enum NKProperties: String, CaseIterable { case systemtags = "" case filemetadatasize = "" case filemetadatagps = "" - case metadataphotossize = "" + case metadataphotosexif = "" case metadataphotosgps = "" + case metadataphotosoriginaldatetime = "" + case metadataphotoplace = "" + case metadataphotossize = "" case metadatafileslivephoto = "" case hidden = "" /// open-collaboration-services.org @@ -228,6 +231,15 @@ public class NKFile: NSObject { public var livePhotoFile = "" /// Indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side public var isFlaggedAsLivePhotoByServer = false + /// + public var datePhotosOriginal: Date? + /// + public struct ChildElement { + let name: String + let text: String? + } + public var exifPhotos = [[String: String?]]() + public var placePhotos: String? } public class NKFileProperty: NSObject { @@ -858,6 +870,19 @@ class NKDataFileXML: NSObject { file.hidden = (hidden as NSString).boolValue } + if let datePhotosOriginal = propstat["d:prop", "nc:metadata-photos-original_date_time"].double, datePhotosOriginal > 0 { + file.datePhotosOriginal = Date(timeIntervalSince1970: datePhotosOriginal) + } + + let exifPhotosElements = propstat["d:prop", "nc:metadata-photos-exif"] + if let element = exifPhotosElements.element { + for child in element.childElements { + file.exifPhotos.append([child.name: child.text]) + } + } + + file.placePhotos = propstat["d:prop", "nc:metadata-photos-place"].text + let results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) file.contentType = results.mimeType diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 476ef6ce..baaa2ea7 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -263,8 +263,8 @@ public extension NextcloudKit { } func downloadPreview(fileId: String, - widthPreview: Int = 512, - heightPreview: Int = 512, + width: Int = 1024, + height: Int = 1024, etag: String? = nil, crop: Int = 1, cropMode: String = "cover", @@ -273,12 +273,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { - let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + completion: @escaping (_ account: String, _ data: Data?, _ width: Int, _ height: Int, _ etag: String?, _ error: NKError) -> Void) { + let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, .urlError) } + return options.queue.async { completion(account, nil, width, height, nil, .urlError) } } if var etag = etag { @@ -296,51 +296,21 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, error) } + options.queue.async { completion(account, nil, width, height, nil, error) } case .success: if let data = response.data { - options.queue.async { completion(account, data, .success) } + let etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields)?.replacingOccurrences(of: "\"", with: "") + options.queue.async { completion(account, data, width, height, etag, .success) } } else { - options.queue.async { completion(account, nil, .invalidData) } + options.queue.async { completion(account, nil, width, height, nil, .invalidData) } } } } } - func downloadPreview(fileId: String, - fileNamePreviewLocalPath: String, - fileNameIconLocalPath: String? = nil, - widthPreview: Int = 512, - heightPreview: Int = 512, - sizeIcon: Int = 512, - compressionQualityPreview: CGFloat = 0.5, - compressionQualityIcon: CGFloat = 0.5, - etag: String? = nil, - crop: Int = 1, - cropMode: String = "cover", - forceIcon: Int = 0, - mimeFallback: Int = 0, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - - downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: etag, endpoint: endpoint, account: account, options: options) { task in - taskHandler(task) - } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in - completion(account, imagePreview, imageIcon, imageOriginal,etag,error) - } - } - func downloadTrashPreview(fileId: String, - fileNamePreviewLocalPath: String, - fileNameIconLocalPath: String, - widthPreview: Int = 512, - heightPreview: Int = 512, - sizeIcon: Int = 512, - compressionQualityPreview: CGFloat = 0.5, - compressionQualityIcon: CGFloat = 0.5, + width: Int = 512, + height: Int = 512, crop: Int = 1, cropMode: String = "cover", forceIcon: Int = 0, @@ -348,35 +318,13 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { - let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(widthPreview)&y=\(heightPreview)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" + completion: @escaping (_ account: String, _ data: Data?, _ width: Int, _ height: Int, _ error: NKError) -> Void) { + let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" - downloadPreview(fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: sizeIcon, compressionQualityPreview: compressionQualityPreview, compressionQualityIcon: compressionQualityIcon, etag: nil, endpoint: endpoint, account: account, options: options) { task in - taskHandler(task) - } completion: { account, imagePreview, imageIcon, imageOriginal, etag, error in - completion(account, imagePreview, imageIcon, imageOriginal,etag,error) - } - } - - private func downloadPreview(fileNamePreviewLocalPath: String, - fileNameIconLocalPath: String? = nil, - sizeIcon: Int = 0, - compressionQualityPreview: CGFloat, - compressionQualityIcon: CGFloat, - etag: String? = nil, - endpoint: String, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imagePreview: UIImage?, _ imageIcon: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } - } - if var etag = etag { - etag = "\"" + etag + "\"" - headers.update(name: "If-None-Match", value: etag) + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, width, height, .urlError) } } nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -389,28 +337,12 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, nil, nil, error) } + options.queue.async { completion(account, nil, width, height, error) } case .success: - guard let data = response.data, let imageOriginal = UIImage(data: data) else { - return options.queue.async { completion(account, nil, nil, nil, nil, .invalidData) } - } - let etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields)?.replacingOccurrences(of: "\"", with: "") - var imagePreview, imageIcon: UIImage? - do { - if let data = imageOriginal.jpegData(compressionQuality: compressionQualityPreview) { - try data.write(to: URL(fileURLWithPath: fileNamePreviewLocalPath), options: .atomic) - imagePreview = UIImage(data: data) - } - if let fileNameIconLocalPath, sizeIcon > 0 { - imageIcon = imageOriginal.resizeImage(size: CGSize(width: sizeIcon, height: sizeIcon)) - if let data = imageIcon?.jpegData(compressionQuality: compressionQualityIcon) { - try data.write(to: URL(fileURLWithPath: fileNameIconLocalPath), options: .atomic) - imageIcon = UIImage(data: data) - } - } - options.queue.async { completion(account, imagePreview, imageIcon, imageOriginal, etag, .success) } - } catch { - options.queue.async { completion(account, nil, nil, nil, nil, NKError(error: error)) } + if let data = response.data { + options.queue.async { completion(account, data, width, height, .success) } + } else { + options.queue.async { completion(account, nil, width, height, .invalidData) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index b7250445..ca98a185 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -30,6 +30,7 @@ public extension NextcloudKit { fileNameLocalPath: String, dateCreationFile: Date? = nil, dateModificationFile: Date? = nil, + overwrite: Bool = false, account: String, options: NKRequestOptions = NKRequestOptions(), requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in }, @@ -57,6 +58,9 @@ public extension NextcloudKit { if let dateModificationFile, dateModificationFile.timeIntervalSince1970 > 0 { headers.update(name: "X-OC-MTime", value: "\(dateModificationFile.timeIntervalSince1970)") } + if overwrite { + headers.update(name: "Overwrite", value: "true") + } let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in task.taskDescription = options.taskDescription @@ -139,7 +143,7 @@ public extension NextcloudKit { if options.customHeader == nil { options.customHeader = [:] } - options.customHeader?["Destination"] = serverUrlFileName + options.customHeader?["Destination"] = serverUrlFileName.urlEncoded options.customHeader?["OC-Total-Length"] = String(fileNameLocalSize) // check space diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index bd85711e..553361d1 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -116,7 +116,7 @@ open class NextcloudKit { } } - public func getSessionhttpResponse(account: String) -> NKSession? { + public func getSession(account: String) -> NKSession? { return nkCommonInstance.nksessions.filter({ $0.account == account }).first } diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 9a304eb5..98188d9b 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -72,6 +72,7 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, dateCreationFile: Date?, dateModificationFile: Date?, taskDescription: String? = nil, + overwrite: Bool = false, account: String, sessionIdentifier: String) -> URLSessionUploadTask? { var url: URL? @@ -95,6 +96,9 @@ public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, request.httpMethod = "PUT" request.setValue(nkSession.userAgent, forHTTPHeaderField: "User-Agent") request.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") + if overwrite { + request.setValue("true", forHTTPHeaderField: "Overwrite") + } // Epoch of linux do not permitted negativ value if let dateCreationFile, dateCreationFile.timeIntervalSince1970 > 0 { request.setValue("\(dateCreationFile.timeIntervalSince1970)", forHTTPHeaderField: "X-OC-CTime") diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index 44797879..f76d1efa 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -28,26 +28,21 @@ public class FileNameValidator { return instance }() - public var forbiddenFileNames: [String] = [] { + public private(set) var forbiddenFileNames: [String] = [] { didSet { forbiddenFileNames = forbiddenFileNames.map({$0.uppercased()}) } } - public var forbiddenFileNameBasenames: [String] = [] { + + public private(set) var forbiddenFileNameBasenames: [String] = [] { didSet { forbiddenFileNameBasenames = forbiddenFileNameBasenames.map({$0.uppercased()}) } } - private var forbiddenFileNameCharactersRegex: NSRegularExpression? - - public var forbiddenFileNameCharacters: [String] = [] { - didSet { - forbiddenFileNameCharactersRegex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]") - } - } + public private(set) var forbiddenFileNameCharacters: [String] = [] - public var forbiddenFileNameExtensions: [String] = [] { + public private(set) var forbiddenFileNameExtensions: [String] = [] { didSet { forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.uppercased()}) } @@ -56,19 +51,19 @@ public class FileNameValidator { public let fileEndsWithSpacePeriodError = NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: NSLocalizedString("_file_name_validator_error_ends_with_space_period_", value: "File name ends with a space or a period.", comment: "")) public var fileReservedNameError: NKError { - let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_reserved_name_", value: "%@ is a forbidden name.", comment: "") + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_reserved_name_", value: "\"%@\" is a forbidden name.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } public var fileForbiddenFileExtensionError: NKError { - let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: ".%@ is a forbidden file extension.", comment: "") + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: ".\"%@\" is a forbidden file extension.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } public var fileInvalidCharacterError: NKError { - let errorMessageTemplate = NSLocalizedString("file_name_validator_error_invalid_character_", value: "Name contains an invalid character: %@.", comment: "") + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_invalid_character_", value: "Name contains an invalid character: \"%@\".", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } @@ -84,12 +79,12 @@ public class FileNameValidator { self.forbiddenFileNameExtensions = forbiddenFileNameExtensions } - public func checkFileName(_ filename: String, existedFileNames: Set? = nil) -> NKError? { + public func checkFileName(_ filename: String) -> NKError? { if filename.hasSuffix(" ") || filename.hasSuffix(".") { return fileEndsWithSpacePeriodError } - if let invalidCharacterError = checkInvalidCharacters(string: filename) { + if let regex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]"), let invalidCharacterError = checkInvalidCharacters(string: filename, regex: regex) { return invalidCharacterError } @@ -107,17 +102,17 @@ public class FileNameValidator { return nil } - public func checkFolderPath(folderPath: String) -> Bool { + public func checkFolderPath(_ folderPath: String) -> Bool { return folderPath.split { $0 == "/" || $0 == "\\" } .allSatisfy { checkFileName(String($0)) == nil } } - private func checkInvalidCharacters(string: String) -> NKError? { + private func checkInvalidCharacters(string: String, regex: NSRegularExpression) -> NKError? { for char in string { let charAsString = String(char) let range = NSRange(location: 0, length: charAsString.utf16.count) - if forbiddenFileNameCharactersRegex?.firstMatch(in: charAsString, options: [], range: range) != nil { + if regex.firstMatch(in: charAsString, options: [], range: range) != nil { templateString = charAsString return fileInvalidCharacterError } From 263420ef65b645ba126ab817379dc0310814d451 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 8 Oct 2024 10:09:48 +0200 Subject: [PATCH 061/135] requestCachePolicy Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 15 +++++++++------ Sources/NextcloudKit/NextcloudKit.swift | 9 ++++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index b528eb16..cbd575f7 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -33,6 +33,7 @@ public class NKSession { public var userAgent: String public var nextcloudVersion: Int public let groupIdentifier: String + public let requestCachePolicy: URLRequest.CachePolicy public let dav: String = "remote.php/dav" public var internalTypeIdentifiers: [NKCommon.UTTypeConformsToServer] = [] public let sessionData: Alamofire.Session @@ -48,7 +49,8 @@ public class NKSession { account: String, userAgent: String, nextcloudVersion: Int, - groupIdentifier: String) { + groupIdentifier: String, + requestCachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy) { self.urlBase = urlBase self.user = user self.userId = userId @@ -57,6 +59,7 @@ public class NKSession { self.userAgent = userAgent self.nextcloudVersion = nextcloudVersion self.groupIdentifier = groupIdentifier + self.requestCachePolicy = requestCachePolicy let backgroundSessionDelegate = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance) /// Strange but works ?!?! @@ -64,7 +67,7 @@ public class NKSession { /// Session Alamofire let configuration = URLSessionConfiguration.af.default - configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + configuration.requestCachePolicy = requestCachePolicy configuration.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionData = Alamofire.Session(configuration: configuration, delegate: NextcloudKitSessionDelegate(nkCommonInstance: NextcloudKit.shared.nkCommonInstance), @@ -79,7 +82,7 @@ public class NKSession { configurationDownloadBackground.sessionSendsLaunchEvents = true configurationDownloadBackground.isDiscretionary = false configurationDownloadBackground.httpMaximumConnectionsPerHost = 5 - configurationDownloadBackground.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationDownloadBackground.requestCachePolicy = requestCachePolicy configurationDownloadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionDownloadBackground = URLSession(configuration: configurationDownloadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) @@ -89,7 +92,7 @@ public class NKSession { configurationUploadBackground.sessionSendsLaunchEvents = true configurationUploadBackground.isDiscretionary = false configurationUploadBackground.httpMaximumConnectionsPerHost = 5 - configurationUploadBackground.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationUploadBackground.requestCachePolicy = requestCachePolicy configurationUploadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackground = URLSession(configuration: configurationUploadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) @@ -99,7 +102,7 @@ public class NKSession { configurationUploadBackgroundWWan.sessionSendsLaunchEvents = true configurationUploadBackgroundWWan.isDiscretionary = false configurationUploadBackgroundWWan.httpMaximumConnectionsPerHost = 5 - configurationUploadBackgroundWWan.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationUploadBackgroundWWan.requestCachePolicy = requestCachePolicy configurationUploadBackgroundWWan.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackgroundWWan = URLSession(configuration: configurationUploadBackgroundWWan, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) @@ -109,7 +112,7 @@ public class NKSession { configurationUploadBackgroundExt.sessionSendsLaunchEvents = true configurationUploadBackgroundExt.isDiscretionary = false configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = 5 - configurationUploadBackgroundExt.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData + configurationUploadBackgroundExt.requestCachePolicy = requestCachePolicy configurationUploadBackgroundExt.sharedContainerIdentifier = groupIdentifier configurationUploadBackgroundExt.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackgroundExt = URLSession(configuration: configurationUploadBackgroundExt, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 553361d1..78400aba 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -58,8 +58,15 @@ open class NextcloudKit { // MARK: - Session setup - public func setup(delegate: NextcloudKitDelegate?) { + public func setup(delegate: NextcloudKitDelegate?, memoryCapacity:Int = 50, diskCapacity:Int = 200) { self.nkCommonInstance.delegate = delegate + + /// Cache URLSession + /// + let memoryCapacity = memoryCapacity * 1024 * 1024 // default 50 MB in RAM + let diskCapacity = diskCapacity * 1024 * 1024 // default 200 MB on Disk + let urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: nil) + URLCache.shared = urlCache } public func appendSession(account: String, From 49432ad3410aa1baf8c603fdb377b8e7e800978c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 8 Oct 2024 10:13:52 +0200 Subject: [PATCH 062/135] cache Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 78400aba..c2ac4bfe 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -58,7 +58,7 @@ open class NextcloudKit { // MARK: - Session setup - public func setup(delegate: NextcloudKitDelegate?, memoryCapacity:Int = 50, diskCapacity:Int = 200) { + public func setup(delegate: NextcloudKitDelegate?, memoryCapacity:Int = 50, diskCapacity:Int = 200, removeAllCachedResponses: Bool = true) { self.nkCommonInstance.delegate = delegate /// Cache URLSession @@ -67,6 +67,10 @@ open class NextcloudKit { let diskCapacity = diskCapacity * 1024 * 1024 // default 200 MB on Disk let urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: nil) URLCache.shared = urlCache + + if removeAllCachedResponses { + URLCache.shared.removeAllCachedResponses() + } } public func appendSession(account: String, From ddf982321300c3492a964bb059beb666f918faf9 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 8 Oct 2024 10:19:05 +0200 Subject: [PATCH 063/135] cache Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index c2ac4bfe..ce589774 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -58,12 +58,12 @@ open class NextcloudKit { // MARK: - Session setup - public func setup(delegate: NextcloudKitDelegate?, memoryCapacity:Int = 50, diskCapacity:Int = 200, removeAllCachedResponses: Bool = true) { + public func setup(delegate: NextcloudKitDelegate?, memoryCapacity:Int = 30, diskCapacity:Int = 200, removeAllCachedResponses: Bool = false) { self.nkCommonInstance.delegate = delegate /// Cache URLSession /// - let memoryCapacity = memoryCapacity * 1024 * 1024 // default 50 MB in RAM + let memoryCapacity = memoryCapacity * 1024 * 1024 // default 30 MB in RAM let diskCapacity = diskCapacity * 1024 * 1024 // default 200 MB on Disk let urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: nil) URLCache.shared = urlCache From bbe7ae1c86fe455df32bb781e1b75fffbcf10ce1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 8 Oct 2024 10:30:44 +0200 Subject: [PATCH 064/135] cache Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index ce589774..4725ed4b 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -58,13 +58,13 @@ open class NextcloudKit { // MARK: - Session setup - public func setup(delegate: NextcloudKitDelegate?, memoryCapacity:Int = 30, diskCapacity:Int = 200, removeAllCachedResponses: Bool = false) { + public func setup(delegate: NextcloudKitDelegate?, memoryCapacity:Int = 30, diskCapacity:Int = 500, removeAllCachedResponses: Bool = false) { self.nkCommonInstance.delegate = delegate /// Cache URLSession /// let memoryCapacity = memoryCapacity * 1024 * 1024 // default 30 MB in RAM - let diskCapacity = diskCapacity * 1024 * 1024 // default 200 MB on Disk + let diskCapacity = diskCapacity * 1024 * 1024 // default 500 MB on Disk let urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: nil) URLCache.shared = urlCache From 0c4c6b674c0e2082bd3cafc47bd99558df90b17a Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Fri, 27 Sep 2024 15:34:33 +0200 Subject: [PATCH 065/135] Add optional spaces and periods - WCF Signed-off-by: Milen Pivchev WIP Signed-off-by: Milen Pivchev WIP Signed-off-by: Milen Pivchev WIP Signed-off-by: Milen Pivchev WIP Signed-off-by: Milen Pivchev WIP Signed-off-by: Milen Pivchev --- .../Utils/FileNameValidator.swift | 23 ++++++++++++------- .../FileNameValidatorTests.swift | 21 ++++++++++++++--- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index f76d1efa..4a04a40b 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -48,7 +48,7 @@ public class FileNameValidator { } } - public let fileEndsWithSpacePeriodError = NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: NSLocalizedString("_file_name_validator_error_ends_with_space_period_", value: "File name ends with a space or a period.", comment: "")) + public let fileWithSpaceError = NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: NSLocalizedString("_file_name_validator_error_space_", value: "Name must not contain spaces at the beginning or end.", comment: "")) public var fileReservedNameError: NKError { let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_reserved_name_", value: "\"%@\" is a forbidden name.", comment: "") @@ -80,10 +80,6 @@ public class FileNameValidator { } public func checkFileName(_ filename: String) -> NKError? { - if filename.hasSuffix(" ") || filename.hasSuffix(".") { - return fileEndsWithSpacePeriodError - } - if let regex = try? NSRegularExpression(pattern: "[\(forbiddenFileNameCharacters.joined())]"), let invalidCharacterError = checkInvalidCharacters(string: filename, regex: regex) { return invalidCharacterError } @@ -94,9 +90,20 @@ public class FileNameValidator { return fileReservedNameError } - if forbiddenFileNameExtensions.contains(where: { filename.uppercased().hasSuffix($0.uppercased()) }) { - templateString = filename.fileExtension - return fileForbiddenFileExtensionError + for fileNameExtension in forbiddenFileNameExtensions { + if fileNameExtension == " " { + if filename.uppercased().hasSuffix(fileNameExtension) || filename.uppercased().hasPrefix(fileNameExtension) { + return fileWithSpaceError + } + } else if filename.uppercased().hasSuffix(fileNameExtension.uppercased()) { + if fileNameExtension == " " { + return fileWithSpaceError + } + + templateString = filename.fileExtension + + return fileForbiddenFileExtensionError + } } return nil diff --git a/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift b/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift index 2b0f4257..619bb08e 100644 --- a/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift +++ b/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift @@ -34,7 +34,7 @@ class FileNameValidatorTests: XCTestCase { "lpt0", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "lpt¹", "lpt²", "lpt³"], forbiddenFileNameCharacters: ["<", ">", ":", "\\\\", "/", "|", "?", "*", "&"], - forbiddenFileNameExtensions: [".filepart",".part"] + forbiddenFileNameExtensions: [".filepart",".part", ".", ",", " "] ) super.setUp() } @@ -56,15 +56,30 @@ class FileNameValidatorTests: XCTestCase { func testEndsWithSpaceOrPeriod() { let result = fileNameValidator.checkFileName("filename ") - XCTAssertEqual(result?.errorDescription, fileNameValidator.fileEndsWithSpacePeriodError.errorDescription) + XCTAssertEqual(result?.errorDescription, fileNameValidator.fileWithSpaceError.errorDescription) let result2 = fileNameValidator.checkFileName("filename.") - XCTAssertEqual(result2?.errorDescription, fileNameValidator.fileEndsWithSpacePeriodError.errorDescription) + XCTAssertEqual(result2?.errorDescription, fileNameValidator.fileForbiddenFileExtensionError.errorDescription) + + let result3 = fileNameValidator.checkFileName(" filename") + XCTAssertEqual(result3?.errorDescription, fileNameValidator.fileWithSpaceError.errorDescription) + + let result4 = fileNameValidator.checkFileName(" filename. ") + XCTAssertEqual(result4?.errorDescription, fileNameValidator.fileWithSpaceError.errorDescription) } func testValidFileName() { let result = fileNameValidator.checkFileName("validFileName") XCTAssertNil(result?.errorDescription) + + let result2 = fileNameValidator.checkFileName("validFi.leName") + XCTAssertNil(result2?.errorDescription) + + let result3 = fileNameValidator.checkFileName("validFi.leName.txt") + XCTAssertNil(result3?.errorDescription) + + let result4 = fileNameValidator.checkFileName("validFi leName.txt") + XCTAssertNil(result4?.errorDescription) } func testValidFolderAndFilePaths() { From 44123c373766a29b691f8b9f3265500da9664f27 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 14 Oct 2024 10:59:14 +0200 Subject: [PATCH 066/135] Response (#97) * API Signed-off-by: Marino Faggiana * assistant Signed-off-by: Marino Faggiana * comments Signed-off-by: Marino Faggiana * dashboard Signed-off-by: Marino Faggiana * e2ee Signed-off-by: Marino Faggiana * groupfolders Signed-off-by: Marino Faggiana * hovercard Signed-off-by: Marino Faggiana * login Signed-off-by: Marino Faggiana * nctext Signed-off-by: Marino Faggiana * pn Signed-off-by: Marino Faggiana * richdocuments Signed-off-by: Marino Faggiana * search Signed-off-by: Marino Faggiana * share Signed-off-by: Marino Faggiana * userstatus Signed-off-by: Marino Faggiana * webdav Signed-off-by: Marino Faggiana * upload Signed-off-by: Marino Faggiana * API Signed-off-by: Marino Faggiana * download Signed-off-by: Marino Faggiana * upload Signed-off-by: Marino Faggiana * NextcloudKitSessionDelegate Signed-off-by: Marino Faggiana * normalized Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+API.swift | 158 ++++++++---------- .../NextcloudKit/NextcloudKit+Assistant.swift | 40 ++--- .../NextcloudKit/NextcloudKit+Comments.swift | 54 +++--- .../NextcloudKit/NextcloudKit+Dashboard.swift | 16 +- .../NextcloudKit/NextcloudKit+Download.swift | 7 +- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 92 +++++----- .../NextcloudKit/NextcloudKit+FilesLock.swift | 10 +- .../NextcloudKit+Groupfolders.swift | 10 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 10 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 12 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 28 ++-- .../NextcloudKit/NextcloudKit+NCText.swift | 24 +-- .../NextcloudKit+PushNotification.swift | 32 ++-- .../NextcloudKit+Richdocuments.swift | 30 ++-- .../NextcloudKit/NextcloudKit+Search.swift | 16 +- Sources/NextcloudKit/NextcloudKit+Share.swift | 52 +++--- .../NextcloudKit/NextcloudKit+Upload.swift | 15 +- .../NextcloudKit+UserStatus.swift | 64 +++---- .../NextcloudKit/NextcloudKit+WebDAV.swift | 112 ++++++------- .../NextcloudKitSessionDelegate.swift | 2 +- 20 files changed, 381 insertions(+), 403 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index baaa2ea7..9d096e83 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -52,9 +52,9 @@ public extension NextcloudKit { func checkServer(serverUrl: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ error: NKError) -> Void) { + completion: @escaping (_ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrl.asUrl else { - return options.queue.async { completion(.urlError) } + return options.queue.async { completion(nil, .urlError) } } internalSession.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in @@ -67,9 +67,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(error) } + options.queue.async { completion(response, error) } case .success: - options.queue.async { completion(.success) } + options.queue.async { completion(response, .success) } } } } @@ -81,7 +81,7 @@ public extension NextcloudKit { method: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { @@ -99,9 +99,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, response.data, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -111,7 +111,7 @@ public extension NextcloudKit { func getExternalSite(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var externalSites: [NKExternalSite] = [] let endpoint = "ocs/v2.php/apps/external/api/v1" guard let nkSession = nkCommonInstance.getSession(account: account), @@ -130,7 +130,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, externalSites, nil, error) } + options.queue.async { completion(account, externalSites, response, error) } case .success(let jsonData): let json = JSON(jsonData) let ocsdata = json["ocs"]["data"] @@ -144,7 +144,7 @@ public extension NextcloudKit { extrernalSite.url = subJson["url"].stringValue externalSites.append(extrernalSite) } - options.queue.async { completion(account, externalSites, jsonData, .success) } + options.queue.async { completion(account, externalSites, response, .success) } } } } @@ -172,10 +172,10 @@ public extension NextcloudKit { func getServerStatus(serverUrl: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (ServerInfoResult) -> Void) { + completion: @escaping (_ responseData: AFDataResponse?, ServerInfoResult) -> Void) { let endpoint = "status.php" guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { - return options.queue.async { completion(ServerInfoResult.failure(.urlError)) } + return options.queue.async { completion(nil, ServerInfoResult.failure(.urlError)) } } var headers: HTTPHeaders? if let userAgent = options.customUserAgent { @@ -192,7 +192,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - return options.queue.async { completion(ServerInfoResult.failure(error)) } + return options.queue.async { completion(response, ServerInfoResult.failure(error)) } case .success(let jsonData): let json = JSON(jsonData) var versionMajor = 0, versionMinor = 0, versionMicro = 0 @@ -218,7 +218,7 @@ public extension NextcloudKit { versionMinor: versionMinor, versionMicro: versionMicro, data: jsonData) - options.queue.async { completion(ServerInfoResult.success(serverInfo)) } + options.queue.async { completion(response, ServerInfoResult.success(serverInfo)) } } } } @@ -230,7 +230,7 @@ public extension NextcloudKit { etag: String? = nil, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } @@ -251,13 +251,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, error) } + options.queue.async { completion(account, response, error) } case .success: - if let data = response.data { - options.queue.async { completion(account, data, .success) } - } else { - options.queue.async { completion(account, nil, .invalidData) } - } + options.queue.async { completion(account, response, .success) } } } } @@ -273,12 +269,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ width: Int, _ height: Int, _ etag: String?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ width: Int, _ height: Int, _ etag: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, width, height, nil, .urlError) } + return options.queue.async { completion(account, width, height, nil, nil, .urlError) } } if var etag = etag { @@ -296,14 +292,10 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, width, height, nil, error) } + options.queue.async { completion(account, width, height, nil, response, error) } case .success: - if let data = response.data { - let etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields)?.replacingOccurrences(of: "\"", with: "") - options.queue.async { completion(account, data, width, height, etag, .success) } - } else { - options.queue.async { completion(account, nil, width, height, nil, .invalidData) } - } + let etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: response.response?.allHeaderFields)?.replacingOccurrences(of: "\"", with: "") + options.queue.async { completion(account, width, height, etag, response, .success) } } } } @@ -318,13 +310,13 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ width: Int, _ height: Int, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ width: Int, _ height: Int, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, width, height, .urlError) } + return options.queue.async { completion(account, width, height, nil, .urlError) } } nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -337,13 +329,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, width, height, error) } + options.queue.async { completion(account, width, height, response, error) } case .success: - if let data = response.data { - options.queue.async { completion(account, data, width, height, .success) } - } else { - options.queue.async { completion(account, nil, width, height, .invalidData) } - } + options.queue.async { completion(account, width, height, response, .success) } } } } @@ -356,12 +344,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "index.php/avatar/\(user)/\(sizeImage)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, nil, .urlError) } + return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } if var etag = etag { @@ -379,7 +367,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, nil, error) } + options.queue.async { completion(account, nil, nil, nil, response, error) } case .success: if let data = response.data { let imageOriginal = UIImage(data: data) @@ -429,12 +417,12 @@ public extension NextcloudKit { } else { try data.write(to: url) } - options.queue.async { completion(account, imageAvatar, imageOriginal, etag, .success) } + options.queue.async { completion(account, imageAvatar, imageOriginal, etag, response, .success) } } catch { - options.queue.async { completion(account, nil, nil, nil, NKError(error: error)) } + options.queue.async { completion(account, nil, nil, nil, response, NKError(error: error)) } } } else { - options.queue.async { completion(account, nil, nil, nil, .invalidData) } + options.queue.async { completion(account, nil, nil, nil, response, .invalidData) } } } } @@ -444,7 +432,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrl.asUrl, let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { @@ -461,13 +449,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, error) } + options.queue.async { completion(account, response, error) } case .success: - if let data = response.data { - options.queue.async { completion(account, data, .success) } - } else { - options.queue.async { completion(account, nil, .invalidData) } - } + options.queue.async { completion(account, response, .success) } } } } @@ -477,7 +461,7 @@ public extension NextcloudKit { func getUserProfile(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/cloud/user" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -495,7 +479,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let ocs = json["ocs"] @@ -535,9 +519,9 @@ public extension NextcloudKit { userProfile.twitter = data["twitter"].stringValue userProfile.website = data["website"].stringValue - options.queue.async { completion(account, userProfile, jsonData, .success) } + options.queue.async { completion(account, userProfile, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -546,7 +530,7 @@ public extension NextcloudKit { func getCapabilities(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v1.php/cloud/capabilities" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -564,13 +548,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, error) } + options.queue.async { completion(account, response, error) } case .success: - if let jsonData = response.data { - options.queue.async { completion(account, jsonData, .success) } - } else { - options.queue.async { completion(account, nil, .invalidData) } - } + options.queue.async { completion(account, response, .success) } } } } @@ -582,7 +562,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ wipe: Bool, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ wipe: Bool, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "index.php/core/wipe/check" let parameters: [String: Any] = ["token": token] /// @@ -604,11 +584,11 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, false, nil, error) } + options.queue.async { completion(account, false, response, error) } case .success(let jsonData): let json = JSON(jsonData) let wipe = json["wipe"].boolValue - options.queue.async { completion(account, wipe, jsonData, .success) } + options.queue.async { completion(account, wipe, response, .success) } } } } @@ -618,7 +598,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "index.php/core/wipe/success" let parameters: [String: Any] = ["token": token] /// @@ -627,7 +607,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -640,9 +620,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -657,7 +637,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var activities: [NKActivity] = [] var activityFirstKnown = 0 var activityLastGiven = 0 @@ -696,7 +676,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, activities, activityFirstKnown, 0, nil, error) } + options.queue.async { completion(account, activities, activityFirstKnown, 0, response, error) } case .success(let jsonData): let json = JSON(jsonData) let ocsdata = json["ocs"]["data"] @@ -744,7 +724,7 @@ public extension NextcloudKit { if let iFirstKnown = Int(firstKnown) { activityFirstKnown = iFirstKnown } if let ilastGiven = Int(lastGiven) { activityLastGiven = ilastGiven } - options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, jsonData, .success) } + options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, response, .success) } } } } @@ -754,7 +734,7 @@ public extension NextcloudKit { func getNotifications(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -772,7 +752,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) if json["ocs"]["meta"]["statuscode"].int == 200 { @@ -814,9 +794,9 @@ public extension NextcloudKit { notification.user = subJson["user"].stringValue notifications.append(notification) } - options.queue.async { completion(account, notifications, jsonData, .success) } + options.queue.async { completion(account, notifications, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -828,10 +808,10 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } var url: URLConvertible? if serverUrl == nil { @@ -841,7 +821,7 @@ public extension NextcloudKit { url = serverUrl?.asUrl } guard let urlRequest = url else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: method) @@ -855,9 +835,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -868,7 +848,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ url: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/dav/api/v1/direct" let parameters: [String: Any] = [ "fileId": fileId, @@ -890,12 +870,12 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let ocsdata = json["ocs"]["data"] let url = ocsdata["url"].string - options.queue.async { completion(account, url, jsonData, .success) } + options.queue.async { completion(account, url, response, .success) } } } } @@ -906,7 +886,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/security_guard/diagnostics" /// options.contentType = "application/json" @@ -914,14 +894,14 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } var urlRequest: URLRequest do { try urlRequest = URLRequest(url: url, method: .put, headers: headers) urlRequest.httpBody = data } catch { - return options.queue.async { completion(account, NKError(error: error)) } + return options.queue.async { completion(account, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -934,9 +914,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 6b02e271..88321915 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -29,7 +29,7 @@ public extension NextcloudKit { func textProcessingGetTypes(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ types: [NKTextProcessingTaskType]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ types: [NKTextProcessingTaskType]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/textprocessing/tasktypes" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -47,16 +47,16 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let data = json["ocs"]["data"]["types"] let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let results = NKTextProcessingTaskType.factory(data: data) - options.queue.async { completion(account, results, jsonData, .success) } + options.queue.async { completion(account, results, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -69,7 +69,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "/ocs/v2.php/textprocessing/schedule" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -88,16 +88,16 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let data = json["ocs"]["data"]["task"] let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let result = NKTextProcessingTask.factory(data: data) - options.queue.async { completion(account, result, jsonData, .success) } + options.queue.async { completion(account, result, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -107,7 +107,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -125,16 +125,16 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let data = json["ocs"]["data"]["task"] let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let result = NKTextProcessingTask.factory(data: data) - options.queue.async { completion(account, result, jsonData, .success) } + options.queue.async { completion(account, result, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -144,7 +144,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ task: NKTextProcessingTask?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "/ocs/v2.php/textprocessing/task/\(taskId)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -162,16 +162,16 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let data = json["ocs"]["data"]["task"] let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let result = NKTextProcessingTask.factory(data: data) - options.queue.async { completion(account, result, jsonData, .success) } + options.queue.async { completion(account, result, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -181,7 +181,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ task: [NKTextProcessingTask]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ task: [NKTextProcessingTask]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "/ocs/v2.php/textprocessing/tasks/app/\(appId)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -199,16 +199,16 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let data = json["ocs"]["data"]["tasks"] let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let results = NKTextProcessingTask.factories(data: data) - options.queue.async { completion(account, results, jsonData, .success) } + options.queue.async { completion(account, results, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 3667b298..4853d646 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -29,7 +29,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ items: [NKComments]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ items: [NKComments]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { /// options.contentType = "application/xml" /// @@ -62,13 +62,13 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success: if let xmlData = response.data { let items = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataComments(xmlData: xmlData) - options.queue.async { completion(account, items, xmlData, .success) } + options.queue.async { completion(account, items, response, .success) } } else { - options.queue.async { completion(account, nil, nil, .invalidData) } + options.queue.async { completion(account, nil, response, .invalidData) } } } } @@ -79,17 +79,17 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { /// options.contentType = "application/json" /// guard let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } var urlRequest: URLRequest @@ -98,7 +98,7 @@ public extension NextcloudKit { let parameters = "{\"actorType\":\"users\",\"verb\":\"comment\",\"message\":\"" + message + "\"}" urlRequest.httpBody = parameters.data(using: .utf8) } catch { - return options.queue.async { completion(account, NKError(error: error)) } + return options.queue.async { completion(account, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -112,9 +112,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -125,17 +125,17 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { /// options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)/\(messageId)" guard let url = serverUrlEndpoint.encodedToUrl else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") var urlRequest: URLRequest @@ -145,7 +145,7 @@ public extension NextcloudKit { let parameters = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyCommentsUpdate, message) urlRequest.httpBody = parameters.data(using: .utf8) } catch { - return options.queue.async { completion(account, NKError(error: error)) } + return options.queue.async { completion(account, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -159,9 +159,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response ,.success) } } } } @@ -171,14 +171,14 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)/\(messageId)" guard let url = serverUrlEndpoint.encodedToUrl else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -192,9 +192,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -203,17 +203,17 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { /// options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") var urlRequest: URLRequest @@ -223,7 +223,7 @@ public extension NextcloudKit { let parameters = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyCommentsMarkAsRead) urlRequest.httpBody = parameters.data(using: .utf8) } catch { - return options.queue.async { completion(account, NKError(error: error)) } + return options.queue.async { completion(account, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -237,9 +237,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 4c4efe1a..3405e8a7 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -30,7 +30,7 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ dashboardWidgets: [NCCDashboardWidget]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ dashboardWidgets: [NCCDashboardWidget]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widgets" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -52,13 +52,13 @@ public extension NextcloudKit { let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let results = NCCDashboardWidget.factory(data: data) - options.queue.async { completion(account, results, jsonData, .success) } + options.queue.async { completion(account, results, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } } } options.queue.async { request(dashboardRequest) } @@ -69,7 +69,7 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ dashboardApplications: [NCCDashboardApplication]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ dashboardApplications: [NCCDashboardApplication]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/dashboard/api/v1/widget-items?widgets[]=\(items)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -91,13 +91,13 @@ public extension NextcloudKit { let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let results = NCCDashboardApplication.factory(data: data) - options.queue.async { completion(account, results, jsonData, .success) } + options.queue.async { completion(account, results, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } } } options.queue.async { request(dashboardRequest) } diff --git a/Sources/NextcloudKit/NextcloudKit+Download.swift b/Sources/NextcloudKit/NextcloudKit+Download.swift index e5725600..cbc5f0dc 100644 --- a/Sources/NextcloudKit/NextcloudKit+Download.swift +++ b/Sources/NextcloudKit/NextcloudKit+Download.swift @@ -33,7 +33,7 @@ public extension NextcloudKit { requestHandler: @escaping (_ request: DownloadRequest) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, - completionHandler: @escaping (_ account: String, _ etag: String?, _ date: Date?, _ lenght: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nKError: NKError) -> Void) { + completionHandler: @escaping (_ account: String, _ etag: String?, _ date: Date?, _ lenght: Int64, _ responseData: AFDownloadResponse?, _ afError: AFError?, _ nKError: NKError) -> Void) { var convertible: URLConvertible? if serverUrlFileName is URL { convertible = serverUrlFileName as? URLConvertible @@ -61,12 +61,11 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let resultError = NKError(error: error, afResponse: response, responseData: nil) - options.queue.async { completionHandler(account, nil, nil, 0, nil, error, resultError) } + options.queue.async { completionHandler(account, nil, nil, 0, response, error, resultError) } case .success: var date: Date? var etag: String? var length: Int64 = 0 - let allHeaderFields = response.response?.allHeaderFields if let result = response.response?.allHeaderFields["Content-Length"] as? String { length = Int64(result) ?? 0 @@ -83,7 +82,7 @@ public extension NextcloudKit { date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") } - options.queue.async { completionHandler(account, etag, date, length, allHeaderFields, nil, .success) } + options.queue.async { completionHandler(account, etag, date, length, response, nil, .success) } } } diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index 2dee0358..4c60353b 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -31,7 +31,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -40,7 +40,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let method: HTTPMethod = delete ? .delete : .put @@ -54,14 +54,14 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } else { - options.queue.async { completion(account, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -74,7 +74,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ e2eToken: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ e2eToken: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -106,15 +106,15 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let e2eToken = json["ocs"]["data"]["e2e-token"].string - options.queue.async { completion(account, e2eToken, jsonData, .success) } + options.queue.async { completion(account, e2eToken, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -125,7 +125,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ e2eMetadata: String?, _ signature: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ e2eMetadata: String?, _ signature: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -151,16 +151,16 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, nil, error) } + options.queue.async { completion(account, nil, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let e2eMetadata = json["ocs"]["data"]["meta-data"].string let signature = response.response?.allHeaderFields["X-NC-E2EE-SIGNATURE"] as? String - options.queue.async { completion(account, e2eMetadata, signature, jsonData, .success) } + options.queue.async { completion(account, e2eMetadata, signature, response, .success) } } else { - options.queue.async { completion(account, nil, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -174,7 +174,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ metadata: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ metadata: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -206,15 +206,15 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - return options.queue.async { completion(account, nil, nil, error) } + return options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let metadata = json["ocs"]["data"]["meta-data"].string - options.queue.async { completion(account, metadata, jsonData, .success) } + options.queue.async { completion(account, metadata, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -226,7 +226,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ certificate: String?, _ certificateUser: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ certificate: String?, _ certificateUser: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { @@ -257,7 +257,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, nil, error) } + options.queue.async { completion(account, nil, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError @@ -265,12 +265,12 @@ public extension NextcloudKit { let key = json["ocs"]["data"]["public-keys"][nkSession.userId].stringValue if let user = user { let keyUser = json["ocs"]["data"]["public-keys"][user].string - options.queue.async { completion(account, key, keyUser, jsonData, .success) } + options.queue.async { completion(account, key, keyUser, response, .success) } } else { - options.queue.async { completion(account, key, nil, jsonData, .success) } + options.queue.async { completion(account, key, nil, response, .success) } } } else { - options.queue.async { completion(account, nil, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -279,7 +279,7 @@ public extension NextcloudKit { func getE2EEPrivateKey(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ privateKey: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -301,15 +301,15 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let key = json["ocs"]["data"]["private-key"].stringValue - options.queue.async { completion(account, key, jsonData, .success) } + options.queue.async { completion(account, key, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -318,7 +318,7 @@ public extension NextcloudKit { func getE2EEPublicKey(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ publicKey: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -340,15 +340,15 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let key = json["ocs"]["data"]["public-key"].stringValue - options.queue.async { completion(account, key, jsonData, .success) } + options.queue.async { completion(account, key, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -358,7 +358,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ certificate: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ certificate: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -381,16 +381,16 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let key = json["ocs"]["data"]["public-key"].stringValue print(key) - options.queue.async { completion(account, key, jsonData, .success) } + options.queue.async { completion(account, key, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -400,7 +400,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ privateKey: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ privateKey: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -423,15 +423,15 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if 200..<300 ~= statusCode { let key = json["ocs"]["data"]["private-key"].stringValue - options.queue.async { completion(account, key, jsonData, .success) } + options.queue.async { completion(account, key, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -440,7 +440,7 @@ public extension NextcloudKit { func deleteE2EECertificate(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -449,7 +449,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription @@ -461,9 +461,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -471,7 +471,7 @@ public extension NextcloudKit { func deleteE2EEPrivateKey(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var version = "v1" if let optionsVesion = options.version { version = optionsVesion @@ -480,7 +480,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -493,9 +493,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index 2cdfa92a..13c1ec53 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -32,15 +32,15 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlFileName.encodedToUrl else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: shouldLock ? "LOCK" : "UNLOCK") guard let nkSession = nkCommonInstance.getSession(account: account), var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } headers.update(name: "X-User-Lock", value: "1") @@ -54,9 +54,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 3c54f4bb..1f97f54d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -29,12 +29,12 @@ public extension NextcloudKit { func getGroupfolders(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ results: [NKGroupfolders]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ results: [NKGroupfolders]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "index.php/apps/groupfolders/folders?applicable=1" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -47,14 +47,14 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, response.data, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let data = json["ocs"]["data"] guard json["ocs"]["meta"]["statuscode"].int == 200 || json["ocs"]["meta"]["statuscode"].int == 100 else { let error = NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode) - options.queue.async { completion(account, nil, jsonData, error) } + options.queue.async { completion(account, nil, response, error) } return } var results = [NKGroupfolders]() @@ -63,7 +63,7 @@ public extension NextcloudKit { results.append(result) } } - options.queue.async { completion(account, results, jsonData, .success) } + options.queue.async { completion(account, results, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index 93331745..f9308d22 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -31,12 +31,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ result: NKHovercard?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ result: NKHovercard?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/hovercard/v1/\(userId)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -49,7 +49,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let data = json["ocs"]["data"] @@ -57,10 +57,10 @@ public extension NextcloudKit { let result = NKHovercard(jsonData: data) else { let error = NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode) - options.queue.async { completion(account, nil, jsonData, error) } + options.queue.async { completion(account, nil, response, error) } return } - options.queue.async { completion(account, result, jsonData, .success) } + options.queue.async { completion(account, result, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 091047d5..b586b3f0 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -30,17 +30,17 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlfileNamePath.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") /// options.contentType = "application/xml" /// guard let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } var urlRequest: URLRequest do { @@ -48,7 +48,7 @@ public extension NextcloudKit { let parameters = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodyLivephoto, livePhotoFile) urlRequest.httpBody = parameters.data(using: .utf8) } catch { - return options.queue.async { completion(account, NKError(error: error)) } + return options.queue.async { completion(account, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -61,9 +61,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 19a7a48a..de9561c4 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -33,7 +33,7 @@ public extension NextcloudKit { userAgent: String? = nil, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ token: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ token: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/getapppassword" guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: url, endpoint: endpoint, options: options) else { return options.queue.async { completion(nil, nil, .urlError) } @@ -61,13 +61,13 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(nil, nil, error) } + options.queue.async { completion(nil, response, error) } case .success(let xmlData): if let data = xmlData { let apppassword = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataAppPassword(data: data) - options.queue.async { completion(apppassword, xmlData, .success) } + options.queue.async { completion(apppassword, response, .success) } } else { - options.queue.async { completion(nil, nil, .xmlError) } + options.queue.async { completion(nil, response, .xmlError) } } } } @@ -80,7 +80,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/core/apppassword" guard let nkSession = nkCommonInstance.getSession(account: account), let url = self.nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { @@ -109,9 +109,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(nil, error) } - case .success(let xmlData): - options.queue.async { completion(xmlData, .success) } + options.queue.async { completion(response, error) } + case .success: + options.queue.async { completion(response, .success) } } } } @@ -121,7 +121,7 @@ public extension NextcloudKit { func getLoginFlowV2(serverUrl: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ token: String?, _ endpoint: String?, _ login: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ token: String?, _ endpoint: String?, _ login: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "index.php/login/v2" guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { return options.queue.async { completion(nil, nil, nil, nil, .urlError) } @@ -141,7 +141,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(nil, nil, nil, nil, error) } + options.queue.async { completion(nil, nil, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) @@ -149,7 +149,7 @@ public extension NextcloudKit { let endpoint = json["poll"]["endpoint"].string let login = json["login"].string - options.queue.async { completion(token, endpoint, login, jsonData, .success) } + options.queue.async { completion(token, endpoint, login, response, .success) } } } } @@ -158,7 +158,7 @@ public extension NextcloudKit { endpoint: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ server: String?, _ loginName: String?, _ appPassword: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ server: String?, _ loginName: String?, _ appPassword: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let serverUrl = endpoint + "?token=" + token guard let url = serverUrl.asUrl else { return options.queue.async { completion(nil, nil, nil, nil, .urlError) } @@ -178,14 +178,14 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(nil, nil, nil, nil, error) } + options.queue.async { completion(nil, nil, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let server = json["server"].string let loginName = json["loginName"].string let appPassword = json["appPassword"].string - options.queue.async { completion(server, loginName, appPassword, jsonData, .success) } + options.queue.async { completion(server, loginName, appPassword, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index a23ab7c1..4e14f8af 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -29,7 +29,7 @@ public extension NextcloudKit { func NCTextObtainEditorDetails(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ editors: [NKEditorDetailsEditors], _ creators: [NKEditorDetailsCreators], _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing" var editors: [NKEditorDetailsEditors] = [] var creators: [NKEditorDetailsCreators] = [] @@ -49,7 +49,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, editors, creators, nil, error) } + options.queue.async { completion(account, editors, creators, response, error) } case .success(let jsonData): let json = JSON(jsonData) let ocsdataeditors = json["ocs"]["data"]["editors"] @@ -84,7 +84,7 @@ public extension NextcloudKit { creators.append(creator) } - options.queue.async { completion(account, editors, creators, jsonData, .success) } + options.queue.async { completion(account, editors, creators, response, .success) } } } } @@ -95,7 +95,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ url: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -119,11 +119,11 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let url = json["ocs"]["data"]["url"].stringValue - options.queue.async { completion(account, url, jsonData, .success) } + options.queue.async { completion(account, url, response, .success) } } } } @@ -131,7 +131,7 @@ public extension NextcloudKit { func NCTextGetListOfTemplates(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ templates: [NKEditorTemplates]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ templates: [NKEditorTemplates]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files/api/v1/directEditing/templates/text/textdocumenttemplate" var templates: [NKEditorTemplates] = [] guard let nkSession = nkCommonInstance.getSession(account: account), @@ -150,7 +150,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, templates, nil, error) } + options.queue.async { completion(account, templates, response, error) } case .success(let jsonData): let json = JSON(jsonData) let ocsdatatemplates = json["ocs"]["data"]["editors"] @@ -165,7 +165,7 @@ public extension NextcloudKit { templates.append(template) } - options.queue.async { completion(account, templates, jsonData, .success) } + options.queue.async { completion(account, templates, response, .success) } } } } @@ -177,7 +177,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ url: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let fileNamePath = fileNamePath.urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -203,11 +203,11 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let url = json["ocs"]["data"]["url"].stringValue - options.queue.async { completion(account, url, jsonData, .success) } + options.queue.async { completion(account, url, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index 2c1cc184..c25759f1 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -33,7 +33,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -56,7 +56,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, nil, nil, error) } + options.queue.async { completion(account, nil, nil, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError @@ -64,9 +64,9 @@ public extension NextcloudKit { let deviceIdentifier = json["ocs"]["data"]["deviceIdentifier"].stringValue let signature = json["ocs"]["data"]["signature"].stringValue let publicKey = json["ocs"]["data"]["publicKey"].stringValue - options.queue.async { completion(account, deviceIdentifier, signature, publicKey, jsonData, .success) } + options.queue.async { completion(account, deviceIdentifier, signature, publicKey, response, .success) } } else { - options.queue.async { completion(account, nil, nil, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, nil, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -76,12 +76,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -94,9 +94,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -109,12 +109,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "devices?format=json" guard let nkSession = nkCommonInstance.getSession(account: account), let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint, options: options), let userAgent = options.customUserAgent else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let parameters = [ "pushToken": pushToken, @@ -134,9 +134,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -148,12 +148,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "devices" guard let nkSession = nkCommonInstance.getSession(account: account), let url = self.nkCommonInstance.createStandardUrl(serverUrl: proxyServerUrl, endpoint: endpoint, options: options), let userAgent = options.customUserAgent else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let parameters = [ "deviceIdentifier": deviceIdentifier, @@ -172,9 +172,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index 5cb9d6e9..13308d95 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -30,7 +30,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ url: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/document" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -49,14 +49,14 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) if json["ocs"]["meta"]["statuscode"].int == 200 { let url = json["ocs"]["data"]["url"].stringValue - options.queue.async { completion(account, url, jsonData, .success) } + options.queue.async { completion(account, url, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -66,7 +66,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ templates: [NKRichdocumentsTemplate]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ templates: [NKRichdocumentsTemplate]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/\(typeTemplate)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -84,7 +84,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let data = json["ocs"]["data"].arrayValue @@ -101,9 +101,9 @@ public extension NextcloudKit { template.type = templateJSON["type"].stringValue templates.append(template) } - options.queue.async { completion(account, templates, jsonData, .success) } + options.queue.async { completion(account, templates, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -114,7 +114,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ url: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/richdocuments/api/v1/templates/new" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -133,14 +133,14 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) if json["ocs"]["meta"]["statuscode"].int == 200 { let url = json["ocs"]["data"]["url"].stringValue - options.queue.async { completion(account, url, jsonData, .success) } + options.queue.async { completion(account, url, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -150,7 +150,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ url: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ url: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "index.php/apps/richdocuments/assets" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -169,11 +169,11 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let url = json["url"].string - options.queue.async { completion(account, url, jsonData, .success) } + options.queue.async { completion(account, url, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index d89f8a3d..53b28bba 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -51,7 +51,7 @@ public extension NextcloudKit { 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, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/search/providers" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -71,7 +71,7 @@ public extension NextcloudKit { let json = JSON(jsonData) let providerData = json["ocs"]["data"] guard let allProvider = NKSearchProvider.factory(jsonArray: providerData) else { - return completion(account, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) + return completion(account, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } providers(account, allProvider) @@ -88,11 +88,11 @@ public extension NextcloudKit { } group.notify(queue: options.queue) { - completion(account, jsonData, .success) + completion(account, response, .success) } case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - return completion(account, nil, error) + return completion(account, response, error) } } request(requestUnifiedSearch) @@ -121,7 +121,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, NKSearchResult?, _ data: Data?, _ error: NKError) -> Void) -> DataRequest? { + completion: @escaping (_ account: String, NKSearchResult?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) -> DataRequest? { guard let term = term.urlEncoded, let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { @@ -162,12 +162,12 @@ public extension NextcloudKit { let json = JSON(jsonData) let searchData = json["ocs"]["data"] guard let searchResult = NKSearchResult(json: searchData, id: id) else { - return completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) + return completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } - completion(account, searchResult, jsonData, .success) + completion(account, searchResult, response, .success) case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - return completion(account, nil, nil, error) + return completion(account, nil, response, error) } } diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 961ae284..4ad5de87 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -79,7 +79,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ shares: [NKShare]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ shares: [NKShare]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: parameters.endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { @@ -96,13 +96,13 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) guard json["ocs"]["meta"]["statuscode"].int == 200 else { let error = NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode) - options.queue.async { completion(account, nil, jsonData, error) } + options.queue.async { completion(account, nil, response, error) } return } var shares: [NKShare] = [] @@ -110,7 +110,7 @@ public extension NextcloudKit { let share = self.convertResponseShare(json: subJson, account: account) shares.append(share) } - options.queue.async { completion(account, shares, jsonData, .success) } + options.queue.async { completion(account, shares, response, .success) } } } } @@ -130,7 +130,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ sharees: [NKSharee]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ sharees: [NKSharee]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/sharees" var lookupString = "false" if lookup { @@ -159,7 +159,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) @@ -213,9 +213,9 @@ public extension NextcloudKit { sharees.append(sharee) } } - options.queue.async { completion(account, sharees, jsonData, .success) } + options.queue.async { completion(account, sharees, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -248,12 +248,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ share: NKShare?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { createShare(path: path, shareType: 3, shareWith: nil, publicUpload: publicUpload, hideDownload: hideDownload, password: password, permissions: permissions, account: account, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) - } completion: { account, share, data, error in - completion(account, share, data, error) + } completion: { account, share, responseData, error in + completion(account, share, responseData, error) } } @@ -267,12 +267,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ share: NKShare?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { createShare(path: path, shareType: shareType, shareWith: shareWith, publicUpload: false, note: note, hideDownload: false, password: password, permissions: permissions, attributes: attributes, account: account, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) - } completion: { account, share, data, error in - completion(account, share, data, error) + } completion: { account, share, responseData, error in + completion(account, share, responseData, error) } } @@ -288,7 +288,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ share: NKShare?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -329,14 +329,14 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - options.queue.async { completion(account, self.convertResponseShare(json: json["ocs"]["data"], account: account), jsonData, .success) } + options.queue.async { completion(account, self.convertResponseShare(json: json["ocs"]["data"], account: account), response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -374,7 +374,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ share: NKShare?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ share: NKShare?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -414,14 +414,14 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - options.queue.async { completion(account, self.convertResponseShare(json: json["ocs"]["data"], account: account), jsonData, .success) } + options.queue.async { completion(account, self.convertResponseShare(json: json["ocs"]["data"], account: account), response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -434,12 +434,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/files_sharing/api/v1/shares/\(idShare)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -452,9 +452,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index ca98a185..ec4d58b5 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -36,7 +36,7 @@ public extension NextcloudKit { requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }, - completionHandler: @escaping (_ account: String, _ ocId: String?, _ etag: String?, _ date: Date?, _ size: Int64, _ allHeaderFields: [AnyHashable: Any]?, _ afError: AFError?, _ nkError: NKError) -> Void) { + completionHandler: @escaping (_ account: String, _ ocId: String?, _ etag: String?, _ date: Date?, _ size: Int64, _ responseData: AFDataResponse?, _ afError: AFError?, _ nkError: NKError) -> Void) { var convertible: URLConvertible? var size: Int64 = 0 if serverUrlFileName is URL { @@ -72,10 +72,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let resultError = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, error, resultError) } + options.queue.async { completionHandler(account, nil, nil, nil, 0, response, error, resultError) } case .success: var ocId: String?, etag: String? - let allHeaderFields = response.response?.allHeaderFields if self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: response.response?.allHeaderFields) != nil { ocId = self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: response.response?.allHeaderFields) } else if self.nkCommonInstance.findHeader("fileid", allHeaderFields: response.response?.allHeaderFields) != nil { @@ -91,12 +90,12 @@ public extension NextcloudKit { } if let dateString = self.nkCommonInstance.findHeader("date", allHeaderFields: response.response?.allHeaderFields) { if let date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") { - options.queue.async { completionHandler(account, ocId, etag, date, size, allHeaderFields, nil, .success) } + options.queue.async { completionHandler(account, ocId, etag, date, size, response, nil, .success) } } else { - options.queue.async { completionHandler(account, nil, nil, nil, 0, allHeaderFields, nil, .invalidDate) } + options.queue.async { completionHandler(account, nil, nil, nil, 0, response, nil, .invalidDate) } } } else { - options.queue.async { completionHandler(account, nil, nil, nil, 0, allHeaderFields, nil, .invalidDate) } + options.queue.async { completionHandler(account, nil, nil, nil, 0, response, nil, .invalidDate) } } } } @@ -179,7 +178,7 @@ public extension NextcloudKit { if error == .success { completion(NKError()) } else if error.errorCode == 404 { - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, account: account, options: options) { _, _, _, error in + NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, account: account, options: options) { _, _, _, _, error in completion(error) } } else { @@ -264,7 +263,7 @@ public extension NextcloudKit { let assembleTimeMax: Double = 30 * 60 // 30 min options.timeout = max(assembleTimeMin, min(assembleTimePerGB * assembleSizeInGB, assembleTimeMax)) - self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account, options: options) { _, error in + self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account, options: options) { _, _, error in guard error == .success else { return completion(account, filesChunkOutput, nil, nil, NKError(errorCode: NKError.chunkMoveFile, errorDescription: error.errorDescription)) } diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index cb18b7c0..fc127bc9 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -30,7 +30,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ clearAt: Date?, _ icon: String?, _ message: String?, _ messageId: String?, _ messageIsPredefined: Bool, _ status: String?, _ statusIsUserDefined: Bool, _ userId: String?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ clearAt: Date?, _ icon: String?, _ message: String?, _ messageId: String?, _ messageIsPredefined: Bool, _ status: String?, _ statusIsUserDefined: Bool, _ userId: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status" if let userId = userId { endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/\(userId)" @@ -51,7 +51,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, nil, error) } + options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError @@ -69,9 +69,9 @@ public extension NextcloudKit { let statusIsUserDefined = json["ocs"]["data"]["statusIsUserDefined"].boolValue let userId = json["ocs"]["data"]["userId"].string - options.queue.async { completion(account, clearAt, icon, message, messageId, messageIsPredefined, status, statusIsUserDefined, userId, jsonData, .success) } + options.queue.async { completion(account, clearAt, icon, message, messageId, messageIsPredefined, status, statusIsUserDefined, userId, response, .success) } } else { - options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -81,12 +81,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/status" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let parameters = [ "statusType": String(status) @@ -102,14 +102,14 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } else { - options.queue.async { completion(account, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -120,12 +120,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/predefined" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } var parameters = [ "messageId": String(messageId) @@ -144,15 +144,15 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } else { - options.queue.async { completion(account, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -164,12 +164,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message/custom" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } var parameters = [ "message": String(message) @@ -191,15 +191,15 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } else { - options.queue.async { completion(account, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -208,12 +208,12 @@ public extension NextcloudKit { func clearMessage(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/user_status/api/v1/user_status/message" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -226,15 +226,15 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError if statusCode == 200 { - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } else { - options.queue.async { completion(account, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -243,7 +243,7 @@ public extension NextcloudKit { func getUserStatusPredefinedStatuses(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/user_status/api/v1/predefined_statuses" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -261,7 +261,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): var userStatuses: [NKUserStatus] = [] let json = JSON(jsonData) @@ -283,9 +283,9 @@ public extension NextcloudKit { userStatus.predefined = true userStatuses.append(userStatus) } - options.queue.async { completion(account, userStatuses, jsonData, .success) } + options.queue.async { completion(account, userStatuses, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } @@ -296,7 +296,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ userStatuses: [NKUserStatus]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { let endpoint = "ocs/v2.php/apps/user_status/api/v1/statuses" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), @@ -318,7 +318,7 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, response, error) } case .success(let jsonData): let json = JSON(jsonData) let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError @@ -338,9 +338,9 @@ public extension NextcloudKit { userStatus.userId = subJson["userId"].string userStatuses.append(userStatus) } - options.queue.async { completion(account, userStatuses, jsonData, .success) } + options.queue.async { completion(account, userStatuses, response, .success) } } else { - options.queue.async { completion(account, nil, jsonData, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 81ad2d47..69555c90 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -30,11 +30,11 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlFileName.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, .urlError) } + return options.queue.async { completion(account, nil, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: "MKCOL") var urlRequest: URLRequest @@ -42,7 +42,7 @@ public extension NextcloudKit { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout } catch { - return options.queue.async { completion(account, nil, nil, NKError(error: error)) } + return options.queue.async { completion(account, nil, nil, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -55,17 +55,17 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, nil, error) } + options.queue.async { completion(account, nil, nil, response, error) } case .success: let ocId = self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: response.response?.allHeaderFields) if let dateString = self.nkCommonInstance.findHeader("date", allHeaderFields: response.response?.allHeaderFields) { if let date = self.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz") { - options.queue.async { completion(account, ocId, date, .success) } + options.queue.async { completion(account, ocId, date, response, .success) } } else { - options.queue.async { completion(account, nil, nil, .invalidDate) } + options.queue.async { completion(account, nil, nil, response, .invalidDate) } } } else { - options.queue.async { completion(account, nil, nil, .invalidDate) } + options.queue.async { completion(account, nil, nil, response, .invalidDate) } } } } @@ -75,18 +75,18 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlFileName.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + 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, NKError(error: error)) } + return options.queue.async { completion(account, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -99,9 +99,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -112,11 +112,11 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlFileNameSource.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "MOVE") headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") @@ -130,7 +130,7 @@ public extension NextcloudKit { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout } catch { - return options.queue.async { completion(account, NKError(error: error)) } + return options.queue.async { completion(account, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -143,9 +143,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -156,11 +156,11 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlFileNameSource.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "COPY") headers.update(name: "Destination", value: serverUrlFileNameDestination.urlEncoded ?? "") @@ -175,7 +175,7 @@ public extension NextcloudKit { try urlRequest = URLRequest(url: url, method: method, headers: headers) urlRequest.timeoutInterval = options.timeout } catch { - return options.queue.async { completion(account, NKError(error: error)) } + return options.queue.async { completion(account, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -188,9 +188,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -203,7 +203,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var files: [NKFile] = [] var serverUrlFileName = serverUrlFileName /// @@ -245,13 +245,13 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, files, nil, error) } + options.queue.async { completion(account, files, response, error) } case .success: if let xmlData = response.data { files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) - options.queue.async { completion(account, files, xmlData, .success) } + options.queue.async { completion(account, files, response, .success) } } else { - options.queue.async { completion(account, files, nil, .xmlError) } + options.queue.async { completion(account, files, response, .xmlError) } } } } @@ -262,7 +262,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ file: NKFile?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ file: NKFile?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account) else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -282,8 +282,8 @@ public extension NextcloudKit { search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: true, includeHiddenFiles: [], account: account, options: options) { task in task.taskDescription = options.taskDescription taskHandler(task) - } completion: { account, files, data, error in - options.queue.async { completion(account, files?.first, data, error) } + } completion: { account, files, responseData, error in + options.queue.async { completion(account, files?.first, responseData, error) } } } @@ -294,12 +294,12 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { if let httpBody = requestBody.data(using: .utf8) { search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) - } completion: { account, files, data, error in - options.queue.async { completion(account, files, data, error) } + } completion: { account, files, responseData, error in + options.queue.async { completion(account, files, responseData, error) } } } } @@ -312,7 +312,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), let href = ("/files/" + nkSession.userId).urlEncoded else { return options.queue.async { completion(account, nil, nil, .urlError) } @@ -321,8 +321,8 @@ public extension NextcloudKit { if let httpBody = requestBody.data(using: .utf8) { search(serverUrl: serverUrl, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) - } completion: { account, files, data, error in - options.queue.async { completion(account, files, data, error) } + } completion: { account, files, responseData, error in + options.queue.async { completion(account, files, responseData, error) } } } } @@ -337,7 +337,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account) else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -370,8 +370,8 @@ public extension NextcloudKit { if let httpBody = requestBody.data(using: .utf8) { search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in taskHandler(task) - } completion: { account, files, data, error in - options.queue.async { completion(account, files, data, error) } + } completion: { account, files, responseData, error in + options.queue.async { completion(account, files, responseData, error) } } } } @@ -383,7 +383,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { /// options.contentType = "application/xml" /// @@ -415,13 +415,13 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, files, nil, error) } + options.queue.async { completion(account, files, response, error) } case .success: if let xmlData = response.data { files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) - options.queue.async { completion(account, files, xmlData, .success) } + options.queue.async { completion(account, files, response, .success) } } else { - options.queue.async { completion(account, files, nil, .xmlError) } + options.queue.async { completion(account, files, response, .xmlError) } } } } @@ -432,17 +432,17 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { /// options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + "/" + fileName guard let url = serverUrlFileName.encodedToUrl else { - return options.queue.async { completion(account, .urlError) } + return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "PROPPATCH") var urlRequest: URLRequest @@ -453,7 +453,7 @@ public extension NextcloudKit { urlRequest.httpBody = body.data(using: .utf8) urlRequest.timeoutInterval = options.timeout } catch { - return options.queue.async { completion(account, NKError(error: error)) } + return options.queue.async { completion(account, nil, NKError(error: error)) } } nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -466,9 +466,9 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } + options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, .success) } + options.queue.async { completion(account, response, .success) } } } } @@ -478,7 +478,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ files: [NKFile]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ files: [NKFile]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { /// options.contentType = "application/xml" /// @@ -512,13 +512,13 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, files, nil, error) } + options.queue.async { completion(account, files, response, error) } case .success: if let xmlData = response.data { files = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataFile(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles) - options.queue.async { completion(account, files, xmlData, .success) } + options.queue.async { completion(account, files, response, .success) } } else { - options.queue.async { completion(account, files, nil, .xmlError) } + options.queue.async { completion(account, files, response, .xmlError) } } } } @@ -529,7 +529,7 @@ public extension NextcloudKit { account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ items: [NKTrash]?, _ data: Data?, _ error: NKError) -> Void) { + completion: @escaping (_ account: String, _ items: [NKTrash]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { /// options.contentType = "application/xml" /// @@ -567,13 +567,13 @@ public extension NextcloudKit { switch response.result { case .failure(let error): let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, items, nil, error) } + options.queue.async { completion(account, items, response, error) } case .success: if let xmlData = response.data { items = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).convertDataTrash(xmlData: xmlData, nkSession: nkSession, showHiddenFiles: showHiddenFiles) - options.queue.async { completion(account, items, xmlData, .success) } + options.queue.async { completion(account, items, response, .success) } } else { - options.queue.async { completion(account, items, nil, .xmlError) } + options.queue.async { completion(account, items, response, .xmlError) } } } } diff --git a/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift index 32e38857..5965a49e 100644 --- a/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift +++ b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift @@ -31,7 +31,7 @@ import UIKit import Alamofire import SwiftyJSON -open class NextcloudKitSessionDelegate: SessionDelegate { +final class NextcloudKitSessionDelegate: SessionDelegate { public var nkCommonInstance: NKCommon? = nil override public init(fileManager: FileManager = .default) { From bb750f2f501539b067f87e354c7ddda71b1fd7e5 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 16 Oct 2024 15:33:20 +0200 Subject: [PATCH 067/135] ThreadSafeArray Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index d71e0974..97bdf81c 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -110,7 +110,7 @@ public class NKCommon: NSObject { internal var utiCache = NSCache() internal var mimeTypeCache = NSCache() internal var filePropertiesCache = NSCache() - internal var internalTypeIdentifiers: [UTTypeConformsToServer] = [] + internal var internalTypeIdentifiers = ThreadSafeArray() public var filenamePathLog: String = "" public var levelLog: Int = 0 From cdcfedb76f18178187a4b166ec8d94061f2e7487 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 20 Oct 2024 10:10:06 +0200 Subject: [PATCH 068/135] logger delegate Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 2 ++ Sources/NextcloudKit/NKLogger.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 97bdf81c..281dbfb9 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -43,6 +43,8 @@ public protocol NextcloudKitDelegate { func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: Date?, dateLastModified: Date?, length: Int64, task: URLSessionTask, error: NKError) func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: Date?, size: Int64, task: URLSessionTask, error: NKError) + + func request(_ request: DataRequest, didParseResponse response: AFDataResponse) } public class NKCommon: NSObject { diff --git a/Sources/NextcloudKit/NKLogger.swift b/Sources/NextcloudKit/NKLogger.swift index 43fa09b1..32c87c62 100644 --- a/Sources/NextcloudKit/NKLogger.swift +++ b/Sources/NextcloudKit/NKLogger.swift @@ -46,6 +46,8 @@ final class NKLogger: EventMonitor { } func request(_ request: DataRequest, didParseResponse response: AFDataResponse) { + self.nkCommonInstance.delegate?.request(request, didParseResponse: response) + guard let date = self.nkCommonInstance.convertDate(Date(), format: "yyyy-MM-dd' 'HH:mm:ss") else { return } let responseResultString = String("\(response.result)") let responseDebugDescription = String("\(response.debugDescription)") From 93c2b50e1fa6806a098c1562c57c36626a27f569 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 21 Oct 2024 16:14:26 +0200 Subject: [PATCH 069/135] fix message 503 Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index f815bb9b..5c4c75db 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -121,7 +121,7 @@ public class NKError: NSObject { case 500: return NSLocalizedString("_internal_server_", value: "Internal server error", comment: "") case 503: - return NSLocalizedString("_server_error_retry_", value: "The server is temporarily unavailable", comment: "") + return NSLocalizedString("_server_error_retry_", value: "Server is currently in maintenance mode", comment: "") case 507: return NSLocalizedString("_user_over_quota_", value: "Storage quota is reached", comment: "") case 200: From 87d6f37626f0cc6f9b3f4bc9bf4352a84c8ca40a Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 21 Oct 2024 16:16:24 +0200 Subject: [PATCH 070/135] public Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index 5c4c75db..845a0690 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -73,7 +73,7 @@ public class NKError: NSObject { public static let invalidData = NKError(errorCode: NSURLErrorCannotDecodeContentData, errorDescription: NSLocalizedString("_invalid_data_format_", value: "Invalid data format", comment: "")) public static let success = NKError(errorCode: 0, errorDescription: "") - private static func getErrorDescription(for code: Int) -> String? { + public static func getErrorDescription(for code: Int) -> String? { switch code { case -9999: return NSLocalizedString("_internal_server_", value: "Internal error", comment: "") From 354bc42de5c2cefdda229c0e6219ded771afdb63 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 21 Oct 2024 16:31:08 +0200 Subject: [PATCH 071/135] fix error Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index 845a0690..10bec7ab 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -121,7 +121,7 @@ public class NKError: NSObject { case 500: return NSLocalizedString("_internal_server_", value: "Internal server error", comment: "") case 503: - return NSLocalizedString("_server_error_retry_", value: "Server is currently in maintenance mode", comment: "") + return NSLocalizedString("_server_maintenance_mode_", value: "Server is currently in maintenance mode", comment: "") case 507: return NSLocalizedString("_user_over_quota_", value: "Storage quota is reached", comment: "") case 200: From b7d757ff0ed318e3e84b680dff896dfaa4abfa10 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Tue, 22 Oct 2024 11:52:31 +0200 Subject: [PATCH 072/135] Add reuse compliance (#96) --- .github/ISSUE_TEMPLATE.md.license | 2 + .github/workflows/lint.yml | 4 +- .github/workflows/reuse.yml | 22 ++ .github/workflows/xcode.xxx | 2 + .gitignore | 5 +- AUTHORS | 2 - AUTHORS.md | 11 + LICENSES/CC0-1.0.txt | 121 +++++++++ LICENSES/GPL-3.0-or-later.txt | 232 ++++++++++++++++++ LICENSES/LicenseRef-NextcloudTrademarks.txt | 9 + Package.swift | 4 + README.md | 6 + REUSE.toml | 24 ++ Sourcery/EnvVars.stencil | 4 +- .../Extensions/Image+Extension.swift | 24 +- .../Extensions/String+Extension.swift | 21 +- Sources/NextcloudKit/NKCommon.swift | 22 +- Sources/NextcloudKit/NKError.swift | 23 +- Sources/NextcloudKit/NKModel.swift | 23 +- Sources/NextcloudKit/NKRequestOptions.swift | 22 +- Sources/NextcloudKit/NKShareAccounts.swift | 22 +- .../NextcloudKit/NextcloudKit+Assistant.swift | 22 +- .../NextcloudKit/NextcloudKit+Comments.swift | 22 +- .../NextcloudKit/NextcloudKit+Dashboard.swift | 22 +- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 22 +- .../NextcloudKit/NextcloudKit+FilesLock.swift | 23 +- .../NextcloudKit+Groupfolders.swift | 22 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 23 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 22 +- .../NextcloudKit/NextcloudKit+NCText.swift | 22 +- .../NextcloudKit+PushNotification.swift | 22 +- .../NextcloudKit+Richdocuments.swift | 22 +- .../NextcloudKit/NextcloudKit+Search.swift | 23 +- Sources/NextcloudKit/NextcloudKit+Share.swift | 22 +- .../NextcloudKit+UserStatus.swift | 22 +- .../NextcloudKit/NextcloudKit+WebDAV.swift | 22 +- Sources/NextcloudKit/NextcloudKit.h | 22 +- Sources/NextcloudKit/NextcloudKit.swift | 22 +- .../NextcloudKit/NextcloudKitBackground.swift | 22 +- .../BaseIntegrationXCTestCase.swift | 21 +- .../FilesIntegrationTests.swift | 20 +- .../ShareIntegrationTests.swift | 21 +- .../LoginUnitTests.swift | 20 +- create-docker-test-server.sh | 4 + generate-env-vars.sh | 5 +- 45 files changed, 508 insertions(+), 587 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md.license create mode 100644 .github/workflows/reuse.yml delete mode 100644 AUTHORS create mode 100644 AUTHORS.md create mode 100644 LICENSES/CC0-1.0.txt create mode 100644 LICENSES/GPL-3.0-or-later.txt create mode 100644 LICENSES/LicenseRef-NextcloudTrademarks.txt create mode 100644 REUSE.toml diff --git a/.github/ISSUE_TEMPLATE.md.license b/.github/ISSUE_TEMPLATE.md.license new file mode 100644 index 00000000..d8d7a109 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 47b10823..2b2c2bc8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,6 @@ - +# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later +# # Lints the project using SwiftLint name: SwiftLint diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml new file mode 100644 index 00000000..95eaba80 --- /dev/null +++ b/.github/workflows/reuse.yml @@ -0,0 +1,22 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization + +# SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. +# +# SPDX-License-Identifier: CC0-1.0 + +name: REUSE Compliance Check + +on: [pull_request] + +jobs: + reuse-compliance-check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: REUSE Compliance Check + uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0 diff --git a/.github/workflows/xcode.xxx b/.github/workflows/xcode.xxx index d9d791cc..b0ace302 100644 --- a/.github/workflows/xcode.xxx +++ b/.github/workflows/xcode.xxx @@ -1,3 +1,5 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later name: Build and test on: diff --git a/.gitignore b/.gitignore index 418c482f..9ce0965d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # file - +# +# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later +# ######################################################################### # # # Title - .gitignore file # diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 74656bbd..00000000 --- a/AUTHORS +++ /dev/null @@ -1,2 +0,0 @@ -Marino Faggiana -All contributors can be viewed at https://github.com/nextcloud/NextcloudKit/graphs/contributors diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 00000000..5c082017 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,11 @@ + +# Authors + +- Claudio Cambra +- Ivan Sein +- Marcel Müller +- Marino Faggiana +- Milen Pivchev diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/GPL-3.0-or-later.txt b/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 00000000..f6cdd22a --- /dev/null +++ b/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/LICENSES/LicenseRef-NextcloudTrademarks.txt b/LICENSES/LicenseRef-NextcloudTrademarks.txt new file mode 100644 index 00000000..464a30b5 --- /dev/null +++ b/LICENSES/LicenseRef-NextcloudTrademarks.txt @@ -0,0 +1,9 @@ +The Nextcloud marks +Nextcloud and the Nextcloud logo is a registered trademark of Nextcloud GmbH in Germany and/or other countries. +These guidelines cover the following marks pertaining both to the product names and the logo: “Nextcloud” +and the blue/white cloud logo with or without the word Nextcloud; the service “Nextcloud Enterprise”; +and our products: “Nextcloud Files”; “Nextcloud Groupware” and “Nextcloud Talk”. +This set of marks is collectively referred to as the “Nextcloud marks.” + +Use of Nextcloud logos and other marks is only permitted under the guidelines provided by the Nextcloud GmbH. +A copy can be found at https://nextcloud.com/trademarks/ diff --git a/Package.swift b/Package.swift index b9ba90ff..05616674 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,9 @@ // swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. +// +// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// import PackageDescription diff --git a/README.md b/README.md index 8414a902..9043ccef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ + # NextcloudKit V 2 Demo of the Nextcloud iOS files app +[![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/NextcloudKit)](https://api.reuse.software/info/github.com/nextcloud/NextcloudKit) + ## Installation ### Carthage diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 00000000..e4881960 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later +version = 1 +SPDX-PackageName = "NextcloudKit" +SPDX-PackageSupplier = "2024 Nextcloud GmbH and Nextcloud contributors" +SPDX-PackageDownloadLocation = "https://github.com/nextcloud/NextcloudKit" + +[[annotations]] +path = ["image.png", "NextcloudKit.png", "NextcloudKit.svg"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2023 Nextcloud GmbH" +SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks" + +[[annotations]] +path = ["Cartfile", "Gemfile", "Package.resolved"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Nextcloud GmbH and Nextcloud contributors" +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = [".swiftlint.yml", "Sourcery/bin/sourcery", "Tests/NextcloudKitUnitTests/Resources/PollMock.json"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2023 Nextcloud GmbH and Nextcloud contributors" +SPDX-License-Identifier = "GPL-3.0-or-later" diff --git a/Sourcery/EnvVars.stencil b/Sourcery/EnvVars.stencil index 99638d73..80348c8c 100644 --- a/Sourcery/EnvVars.stencil +++ b/Sourcery/EnvVars.stencil @@ -2,8 +2,8 @@ // EnvVars.stencil.swift // NextcloudIntegrationTests // -// Created by Milen on 31.05.23. -// Copyright © 2023 Marino Faggiana. All rights reserved. +// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/Extensions/Image+Extension.swift b/Sources/NextcloudKit/Extensions/Image+Extension.swift index d155e3bb..ef660981 100644 --- a/Sources/NextcloudKit/Extensions/Image+Extension.swift +++ b/Sources/NextcloudKit/Extensions/Image+Extension.swift @@ -1,26 +1,6 @@ // -// Image+Extension.swift -// NextcloudKit -// -// Created by Marino Faggiana on 21/12/20. -// Copyright © 2020 Marino Faggiana. All rights reserved. -// Copyright © 2021 Henrik Storch. All rights reserved. -// -// Author Marino Faggiana -// Author Henrik Storch -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // #if os(macOS) diff --git a/Sources/NextcloudKit/Extensions/String+Extension.swift b/Sources/NextcloudKit/Extensions/String+Extension.swift index 805babb7..c3826f2e 100644 --- a/Sources/NextcloudKit/Extensions/String+Extension.swift +++ b/Sources/NextcloudKit/Extensions/String+Extension.swift @@ -1,23 +1,6 @@ // -// String+Extension.swift -// NextcloudKit -// -// Created by Marino Faggiana on 02/02/23. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 281dbfb9..c119369f 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -1,24 +1,6 @@ // -// NKCommon.swift -// NextcloudKit -// -// Created by Marino Faggiana on 12/10/19. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index 10bec7ab..35633a34 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -1,25 +1,6 @@ // -// NKError.swift -// NextcloudKit -// -// Created by Henrik Storch on 18/08/22. -// Copyright © 2022 Henrik Sorch. All rights reserved. -// -// Author Marino Faggiana -// Author Henrik Storch -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 78a668c3..cc5b8e10 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -1,25 +1,6 @@ // -// NKModel.swift -// NextcloudKit -// -// Created by Marino Faggiana on 12/10/19. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// Author Henrik Storch -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NKRequestOptions.swift b/Sources/NextcloudKit/NKRequestOptions.swift index 6c364b69..7a0f03fd 100644 --- a/Sources/NextcloudKit/NKRequestOptions.swift +++ b/Sources/NextcloudKit/NKRequestOptions.swift @@ -1,24 +1,6 @@ // -// NKRequestOptions.swift -// NextcloudKit -// -// Created by Henrik Storch on 26.11.2021. -// Copyright © 2022 Henrik Sorch. All rights reserved. -// -// Author Henrik Storch -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NKShareAccounts.swift b/Sources/NextcloudKit/NKShareAccounts.swift index b9b082bb..3bf85026 100644 --- a/Sources/NextcloudKit/NKShareAccounts.swift +++ b/Sources/NextcloudKit/NKShareAccounts.swift @@ -1,24 +1,6 @@ // -// NKShareAccounts.swift -// NextcloudKit -// -// Created by Marino Faggiana on 07/02/23. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 88321915..9307c5e4 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+Assistant.swift -// NextcloudKit -// -// Created by Marino Faggiana on 26/03/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 4853d646..53c0b68b 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+Comments.swift -// NextcloudKit -// -// Created by Marino Faggiana on 21/05/2020. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 3405e8a7..8d8d3dfc 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+Dashboard.swift -// NextcloudKit -// -// Created by Marino Faggiana on 31/08/22. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index 4c60353b..00e336e1 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+E2EE.swift -// NextcloudKit -// -// Created by Marino Faggiana on 22/05/2020. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index 13c1ec53..c70f45f7 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -1,25 +1,6 @@ // -// NextcloudKit+FilesLock.swift -// NextcloudKit -// -// Created by Henrik Storch on 23.03.22. -// Copyright © 2022 Henrik Sorch. All rights reserved. -// -// Author Henrik Storch -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 1f97f54d..3b2db639 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+Groupfolders.swift -// NextcloudKit -// -// Created by Henrik Storch on 12/04/2023. -// Copyright © 2023 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index f9308d22..d1a621be 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -1,25 +1,6 @@ // -// NextcloudKit+Hovercard.swift -// NextcloudKit -// -// Created by Henrik Storch on 04/11/2021. -// Copyright © 2021 Henrik Sorch. All rights reserved. -// -// Author Henrik Storch -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index b586b3f0..615df69a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+Livephoto.swift -// NextcloudKit -// -// Created by Marino Faggiana on 09/11/2023. -// Copyright © 2023 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index 4e14f8af..2fd02e34 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+NCText.swift -// NextcloudKit -// -// Created by Marino Faggiana on 07/05/2020. -// Copyright © 2020 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index c25759f1..d7a30c9d 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+PushNotification.swift -// NextcloudKit -// -// Created by Marino Faggiana on 22/05/2020. -// Copyright © 2020 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index 13308d95..372d040a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+Richdocuments.swift -// NextcloudKit -// -// Created by Marino Faggiana on 18/05/2020. -// Copyright © 2020 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index 53b28bba..619d6201 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -1,25 +1,6 @@ // -// NextcloudKit+Search.swift -// NextcloudKit -// -// Created by Henrik Storch on 2022. -// Copyright © 2022 Henrik Storch. All rights reserved. -// -// Author Henrik Storch -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 4ad5de87..ea92f8e8 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+Share.swift -// NextcloudKit -// -// Created by Marino Faggiana on 15/06/2020. -// Copyright © 2020 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index fc127bc9..a877b38b 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+UserStatus.swift -// NextcloudKit -// -// Created by Marino Faggiana on 22/11/20. -// Copyright © 2020 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 69555c90..01ec094b 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -1,24 +1,6 @@ // -// NextcloudKit+WebDAV.swift -// NextcloudKit -// -// Created by Marino Faggiana on 07/05/2020. -// Copyright © 2020 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit.h b/Sources/NextcloudKit/NextcloudKit.h index 6f95251d..3c8c3f84 100644 --- a/Sources/NextcloudKit/NextcloudKit.h +++ b/Sources/NextcloudKit/NextcloudKit.h @@ -1,24 +1,6 @@ // -// NextcloudKit.h -// NextcloudKit -// -// Created by Marino Faggiana on 17/08/22. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // #import diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 4725ed4b..38cf690c 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -1,24 +1,6 @@ // -// NextcloudKit.swift -// NextcloudKit -// -// Created by Marino Faggiana on 12/10/19. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // #if os(macOS) diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 98188d9b..03c05715 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -1,24 +1,6 @@ // -// NextcloudKitBackground.swift -// NextcloudKit -// -// Created by Marino Faggiana on 29/10/19. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import Foundation diff --git a/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift b/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift index 8a0009e3..1bfb73eb 100644 --- a/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift +++ b/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift @@ -1,23 +1,6 @@ // -// BaseIntegrationXCTestCase.swift -// -// -// Created by Milen Pivchev on 20.06.23. -// Copyright © 2023 Milen Pivchev. All rights reserved. -// -// Author: Milen Pivchev -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import XCTest diff --git a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift index e32a8ab3..5d57580b 100644 --- a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift @@ -1,22 +1,6 @@ // -// NextcloudKitIntegrationTests.swift -// -// Created by Milen Pivchev on 23.05.23. -// Copyright © 2023 Milen Pivchev. All rights reserved. -// -// Author: Milen Pivchev -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import XCTest diff --git a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift index 68c8339f..b5c0b049 100644 --- a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift @@ -1,23 +1,6 @@ // -// ShareIntegrationTests.swift -// -// -// Created by Milen Pivchev on 20.06.23. -// Copyright © 2023 Milen Pivchev. All rights reserved. -// -// Author: Milen Pivchev -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import XCTest diff --git a/Tests/NextcloudKitUnitTests/LoginUnitTests.swift b/Tests/NextcloudKitUnitTests/LoginUnitTests.swift index a6940d35..69de866c 100644 --- a/Tests/NextcloudKitUnitTests/LoginUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/LoginUnitTests.swift @@ -1,22 +1,6 @@ // -// NextcloudKitUnitTests.swift -// -// Created by Milen Pivchev on 23.05.23. -// Copyright © 2023 Milen Pivchev. All rights reserved. -// -// Author: Milen Pivchev -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later // import XCTest diff --git a/create-docker-test-server.sh b/create-docker-test-server.sh index 644e01a4..e668aead 100755 --- a/create-docker-test-server.sh +++ b/create-docker-test-server.sh @@ -1,4 +1,8 @@ #!/usr/bin/env bash +# +# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later +# #This script creates a testable Docker enviroment of the Nextcloud server, and is used by the CI for tests. container_name="nextcloud_test" diff --git a/generate-env-vars.sh b/generate-env-vars.sh index 1bd9125f..5a98710f 100755 --- a/generate-env-vars.sh +++ b/generate-env-vars.sh @@ -1,6 +1,9 @@ #This generates an env-vars file that the project uses for various tasks, mostly testing. If you want to modify the env vars, open .env-vars in the root dir. #It also generates a .swift file in the project that contains the env vars as swift constants for easy use. Check EnvVars.generated.swift - +# +# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later +# if [ ! -f ".env-vars" ]; then touch .env-vars echo "export TEST_SERVER_URL=" >> .env-vars From 14c9da523b11ced10685df61ed855d2cd8821ef5 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 4 Nov 2024 14:51:42 +0100 Subject: [PATCH 073/135] Auto rename (#99) * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * License Signed-off-by: Milen Pivchev * Licenses Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * Swiftlint fixes Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * Fix lint issue Signed-off-by: Milen Pivchev * Fix ordering Signed-off-by: Milen Pivchev * Refactor Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * License Signed-off-by: Milen Pivchev * Licenses Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * Swiftlint fixes Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * Fix lint issue Signed-off-by: Milen Pivchev * Fix ordering Signed-off-by: Milen Pivchev * Refactor Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev --------- Signed-off-by: Milen Pivchev Co-authored-by: Marino Faggiana --- .swiftlint.yml | 2 + .../Extensions/Image+Extension.swift | 1 - Sources/NextcloudKit/NKCommon.swift | 2 +- Sources/NextcloudKit/NKLogger.swift | 25 +-- Sources/NextcloudKit/NKModel.swift | 12 +- Sources/NextcloudKit/NKSession.swift | 24 +-- Sources/NextcloudKit/NextcloudKit+API.swift | 24 +-- .../NextcloudKit/NextcloudKit+Assistant.swift | 13 +- .../NextcloudKit/NextcloudKit+Comments.swift | 4 +- .../NextcloudKit/NextcloudKit+Dashboard.swift | 4 +- .../NextcloudKit/NextcloudKit+Download.swift | 24 +-- .../NextcloudKit/NextcloudKit+Hovercard.swift | 2 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 24 +-- Sources/NextcloudKit/NextcloudKit+Share.swift | 6 +- .../NextcloudKit/NextcloudKit+Upload.swift | 25 +-- Sources/NextcloudKit/NextcloudKit.swift | 2 +- .../NextcloudKitSessionDelegate.swift | 26 +-- .../NextcloudKit/Utils/FileAutoRenamer.swift | 93 ++++++++++ .../Utils/FileNameValidator.swift | 35 ++-- .../NextcloudKit/Utils/ThreadSafeArray.swift | 25 +-- .../FilesIntegrationTests.swift | 74 ++++---- .../ShareIntegrationTests.swift | 38 ++--- .../FileAutoRenamerUnitTests.swift | 160 ++++++++++++++++++ ...swift => FileNameValidatorUnitTests.swift} | 67 +------- .../LoginUnitTests.swift | 8 +- 25 files changed, 367 insertions(+), 353 deletions(-) create mode 100644 Sources/NextcloudKit/Utils/FileAutoRenamer.swift create mode 100644 Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift rename Tests/NextcloudKitUnitTests/{FileNameValidatorTests.swift => FileNameValidatorUnitTests.swift} (64%) diff --git a/.swiftlint.yml b/.swiftlint.yml index 99b53809..9d2f297c 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -46,5 +46,7 @@ excluded: - Pods - Package.swift - Tests + - DerivedData + - .build reporter: "xcode" diff --git a/Sources/NextcloudKit/Extensions/Image+Extension.swift b/Sources/NextcloudKit/Extensions/Image+Extension.swift index ef660981..e338b910 100644 --- a/Sources/NextcloudKit/Extensions/Image+Extension.swift +++ b/Sources/NextcloudKit/Extensions/Image+Extension.swift @@ -80,4 +80,3 @@ extension UIImage { } } #endif - diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index c119369f..ef8e0d7b 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -274,7 +274,7 @@ public class NKCommon: NSObject { // MARK: - Chunked File - public func chunkedFile(inputDirectory: String, + public func chunkedFile(inputDirectory: String, outputDirectory: String, fileName: String, chunkSize: Int, diff --git a/Sources/NextcloudKit/NKLogger.swift b/Sources/NextcloudKit/NKLogger.swift index 32c87c62..da40f79d 100644 --- a/Sources/NextcloudKit/NKLogger.swift +++ b/Sources/NextcloudKit/NKLogger.swift @@ -1,26 +1,5 @@ -// -// NKLogger.swift -// NextcloudKit -// -// Created by Marino Faggiana on 16/08/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index cc5b8e10..22b948ba 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -355,7 +355,7 @@ class NKDataFileXML: NSObject { """ func getRequestBodyFile(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { - let request = """ + let request = """ @@ -379,7 +379,7 @@ class NKDataFileXML: NSObject { """ func getRequestBodyFileListingFavorites(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { - let request = """ + let request = """ @@ -394,7 +394,7 @@ class NKDataFileXML: NSObject { } func getRequestBodySearchFileName(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { - let request = """ + let request = """ @@ -422,7 +422,7 @@ class NKDataFileXML: NSObject { } func getRequestBodySearchFileId(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { - let request = """ + let request = """ @@ -450,7 +450,7 @@ class NKDataFileXML: NSObject { } func getRequestBodySearchMedia(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { - let request = """ + let request = """ @@ -508,7 +508,7 @@ class NKDataFileXML: NSObject { } func getRequestBodySearchMediaWithLimit(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { - let request = """ + let request = """ diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index cbd575f7..580237a7 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -1,25 +1,5 @@ -// -// NKSession.swift -// -// -// Created by Marino Faggiana on 20/07/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 9d096e83..7e52aa20 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -1,25 +1,5 @@ -// -// NextcloudKit+API.swift -// NextcloudKit -// -// Created by Marino Faggiana on 07/05/2020. -// Copyright © 2022 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later #if os(macOS) import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 9307c5e4..7372f2cb 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -16,7 +16,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -56,7 +56,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] @@ -94,7 +94,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -131,7 +131,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -168,7 +168,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -261,7 +261,6 @@ public class NKTextProcessingTask { } static func factory(data: JSON) -> NKTextProcessingTask? { - NKTextProcessingTask.init(json: data) + NKTextProcessingTask(json: data) } } - diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 53c0b68b..201603ab 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -17,7 +17,7 @@ public extension NextcloudKit { /// guard let nkSession = nkCommonInstance.getSession(account: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } let serverUrlEndpoint = nkSession.urlBase + "/" + nkSession.dav + "/comments/files/\(fileId)" guard let url = serverUrlEndpoint.encodedToUrl else { @@ -143,7 +143,7 @@ public extension NextcloudKit { let error = NKError(error: error, afResponse: response, responseData: response.data) options.queue.async { completion(account, response, error) } case .success: - options.queue.async { completion(account, response ,.success) } + options.queue.async { completion(account, response, .success) } } } } diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 8d8d3dfc..3aa00fa9 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -17,7 +17,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in @@ -56,7 +56,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in diff --git a/Sources/NextcloudKit/NextcloudKit+Download.swift b/Sources/NextcloudKit/NextcloudKit+Download.swift index cbc5f0dc..6bb8f3a5 100644 --- a/Sources/NextcloudKit/NextcloudKit+Download.swift +++ b/Sources/NextcloudKit/NextcloudKit+Download.swift @@ -1,25 +1,5 @@ -// -// NextcloudKit+Download.swift -// NextcloudKit -// -// Created by Marino Faggiana on 16/08/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index d1a621be..25630b79 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -50,7 +50,7 @@ public extension NextcloudKit { public class NKHovercard: NSObject { public let userId, displayName: String public let actions: [Action] - + init?(jsonData: JSON) { guard let userId = jsonData["userId"].string, let displayName = jsonData["displayName"].string, diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index de9561c4..4f41dabf 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -1,25 +1,5 @@ -// -// NextcloudKit+LoginFlowV2.swift -// NextcloudKit -// -// Created by Marino Faggiana on 07/05/2020. -// Copyright © 2020 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index ea92f8e8..d1b883e5 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -121,7 +121,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } let parameters = [ "search": search, @@ -275,7 +275,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } var parameters = [ "path": path, @@ -361,7 +361,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil,nil, .urlError) } + return options.queue.async { completion(account, nil, nil, .urlError) } } var parameters = [ "permissions": String(permissions) diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index ec4d58b5..fb0f159d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -1,25 +1,5 @@ -// -// NextcloudKit+Upload.swift -// NextcloudKit -// -// Created by Marino Faggiana on 16/08/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import Alamofire @@ -278,4 +258,3 @@ public extension NextcloudKit { } } } - diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 38cf690c..a9d3ef0f 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -40,7 +40,7 @@ open class NextcloudKit { // MARK: - Session setup - public func setup(delegate: NextcloudKitDelegate?, memoryCapacity:Int = 30, diskCapacity:Int = 500, removeAllCachedResponses: Bool = false) { + public func setup(delegate: NextcloudKitDelegate?, memoryCapacity: Int = 30, diskCapacity: Int = 500, removeAllCachedResponses: Bool = false) { self.nkCommonInstance.delegate = delegate /// Cache URLSession diff --git a/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift index 5965a49e..8e6aaeda 100644 --- a/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift +++ b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift @@ -1,25 +1,5 @@ -// -// NextcloudKitSessionDelegate.swift -// NextcloudKit -// -// Created by Marino Faggiana on 07/08/2024. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation @@ -32,7 +12,7 @@ import Alamofire import SwiftyJSON final class NextcloudKitSessionDelegate: SessionDelegate { - public var nkCommonInstance: NKCommon? = nil + public var nkCommonInstance: NKCommon? override public init(fileManager: FileManager = .default) { super.init(fileManager: fileManager) diff --git a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift new file mode 100644 index 00000000..a3eb24ab --- /dev/null +++ b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +// +// AutoRenameManager.swift +// Nextcloud +// +// Created by Milen Pivchev on 09.10.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +public class FileAutoRenamer { + public static let shared: FileAutoRenamer = { + let instance = FileAutoRenamer() + return instance + }() + + private var forbiddenFileNameCharacters: [String] = [] + + private var forbiddenFileNameExtensions: [String] = [] { + didSet { + forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.uppercased()}) + } + } + + private let replacement = "_" + + public func setup(forbiddenFileNameCharacters: [String], forbiddenFileNameExtensions: [String]) { + self.forbiddenFileNameCharacters = forbiddenFileNameCharacters + self.forbiddenFileNameExtensions = forbiddenFileNameExtensions + } + + public func rename(filename: String, isFolderPath: Bool = false) -> String { + var pathSegments = filename.split(separator: "/", omittingEmptySubsequences: false).map { String($0) } + + if isFolderPath { + forbiddenFileNameCharacters.removeAll { $0 == "/" } + } + + pathSegments = pathSegments.map { segment in + var modifiedSegment = segment + + forbiddenFileNameCharacters.forEach { forbiddenChar in + if modifiedSegment.contains(forbiddenChar) { + modifiedSegment = modifiedSegment.replacingOccurrences(of: forbiddenChar, with: replacement, options: .caseInsensitive) + } + } + + if forbiddenFileNameExtensions.contains(" ") { + modifiedSegment = modifiedSegment.trimmingCharacters(in: .whitespaces) + } + + forbiddenFileNameExtensions.forEach { forbiddenExtension in + if modifiedSegment.uppercased().hasSuffix(forbiddenExtension) && isFullExtension(forbiddenExtension) { + modifiedSegment = modifiedSegment.replacingOccurrences(of: ".", with: replacement, options: .caseInsensitive) + } + + if modifiedSegment.uppercased().hasSuffix(forbiddenExtension) || modifiedSegment.uppercased().hasPrefix(forbiddenExtension) { + modifiedSegment = modifiedSegment.replacingOccurrences(of: forbiddenExtension, with: replacement, options: .caseInsensitive) + } + } + + return modifiedSegment + } + + let result = pathSegments.joined(separator: "/") + return removeNonPrintableUnicodeCharacters(convertToUTF8(result)) + } + + private func convertToUTF8(_ filename: String) -> String { + return String(data: filename.data(using: .utf8) ?? Data(), encoding: .utf8) ?? filename + } + + private func isFullExtension(_ string: String) -> Bool { + let pattern = "\\.[a-zA-Z0-9]+$" + let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) + let range = NSRange(location: 0, length: string.utf16.count) + return regex?.firstMatch(in: string, options: [], range: range) != nil + } + + private func removeNonPrintableUnicodeCharacters(_ filename: String) -> String { + do { + let regex = try NSRegularExpression(pattern: "\\p{C}", options: []) + let range = NSRange(location: 0, length: filename.utf16.count) + return regex.stringByReplacingMatches(in: filename, options: [], range: range, withTemplate: "") + } catch { + print("Could not remove printable unicode characters.") + return filename + } + } +} diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index 4a04a40b..dd313945 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -1,24 +1,5 @@ -// -// FileNameValidator.swift -// -// -// Created by Milen Pivchev on 12.07.24. -// Copyright © 2024 Milen Pivchev. All rights reserved. -// -// Author: Milen Pivchev -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation @@ -28,21 +9,21 @@ public class FileNameValidator { return instance }() - public private(set) var forbiddenFileNames: [String] = [] { + private var forbiddenFileNames: [String] = [] { didSet { forbiddenFileNames = forbiddenFileNames.map({$0.uppercased()}) } } - public private(set) var forbiddenFileNameBasenames: [String] = [] { + private var forbiddenFileNameBasenames: [String] = [] { didSet { forbiddenFileNameBasenames = forbiddenFileNameBasenames.map({$0.uppercased()}) } } - public private(set) var forbiddenFileNameCharacters: [String] = [] + private var forbiddenFileNameCharacters: [String] = [] - public private(set) var forbiddenFileNameExtensions: [String] = [] { + private var forbiddenFileNameExtensions: [String] = [] { didSet { forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.uppercased()}) } @@ -114,6 +95,10 @@ public class FileNameValidator { .allSatisfy { checkFileName(String($0)) == nil } } + public func isFileHidden(_ name: String) -> Bool { + return !name.isEmpty && name.first == "." + } + private func checkInvalidCharacters(string: String, regex: NSRegularExpression) -> NKError? { for char in string { let charAsString = String(char) diff --git a/Sources/NextcloudKit/Utils/ThreadSafeArray.swift b/Sources/NextcloudKit/Utils/ThreadSafeArray.swift index acd6f736..7fbf334d 100644 --- a/Sources/NextcloudKit/Utils/ThreadSafeArray.swift +++ b/Sources/NextcloudKit/Utils/ThreadSafeArray.swift @@ -1,26 +1,5 @@ -// -// ThreadSafeArray.swift -// Nextcloud -// -// Created by Marino Faggiana on 31/01/24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// http://basememara.com/creating-thread-safe-arrays-in-swift/ -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation diff --git a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift index 5d57580b..bf99892e 100644 --- a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift @@ -15,43 +15,43 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { NextcloudKit.shared.appendSession(account: account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") - // Test creating folder - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in - XCTAssertEqual(self.account, account) - - XCTAssertEqual(NKError.success.errorCode, error.errorCode) - XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) - - Thread.sleep(forTimeInterval: 0.2) - - // Test reading folder, should exist - NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account) { account, files, data, error in - XCTAssertEqual(self.account, account) - XCTAssertEqual(NKError.success.errorCode, error.errorCode) - XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) - XCTAssertEqual(files?[0].fileName, folderName) - - Thread.sleep(forTimeInterval: 0.2) - - // Test deleting folder - NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: account) { account, error in - XCTAssertEqual(self.account, account) - XCTAssertEqual(NKError.success.errorCode, error.errorCode) - XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) - - Thread.sleep(forTimeInterval: 0.2) - - // Test reading folder, should NOT exist - NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account) { account, files, data, error in - defer { expectation.fulfill() } - - XCTAssertEqual(404, error.errorCode) - XCTAssertEqual(self.account, account) - XCTAssertTrue(files?.isEmpty ?? false) - } - } - } - } +// // Test creating folder +// NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in +// XCTAssertEqual(self.account, account) +// +// XCTAssertEqual(NKError.success.errorCode, error.errorCode) +// XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) +// +// Thread.sleep(forTimeInterval: 0.2) +// +// // Test reading folder, should exist +// NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account) { account, files, data, error in +// XCTAssertEqual(self.account, account) +// XCTAssertEqual(NKError.success.errorCode, error.errorCode) +// XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) +// XCTAssertEqual(files?[0].fileName, folderName) +// +// Thread.sleep(forTimeInterval: 0.2) +// +// // Test deleting folder +// NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: account) { account, error in +// XCTAssertEqual(self.account, account) +// XCTAssertEqual(NKError.success.errorCode, error.errorCode) +// XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) +// +// Thread.sleep(forTimeInterval: 0.2) +// +// // Test reading folder, should NOT exist +// NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account) { account, files, data, error in +// defer { expectation.fulfill() } +// +// XCTAssertEqual(404, error.errorCode) +// XCTAssertEqual(self.account, account) +// XCTAssertTrue(files?.isEmpty ?? false) +// } +// } +// } +// } waitForExpectations(timeout: 100) } diff --git a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift index b5c0b049..7c8cb4bb 100644 --- a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift @@ -17,25 +17,25 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { NextcloudKit.shared.appendSession(account: account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in - XCTAssertEqual(self.account, account) - - XCTAssertEqual(NKError.success.errorCode, error.errorCode) - XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) - - Thread.sleep(forTimeInterval: 0.2) - - let note = "Test note" - - NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: "") { account, share, data, error in - defer { expectation.fulfill() } - - XCTAssertEqual(self.account, account) - XCTAssertEqual(NKError.success.errorCode, error.errorCode) - XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) - XCTAssertEqual(note, share?.note) - } - } +// NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in +// XCTAssertEqual(self.account, account) +// +// XCTAssertEqual(NKError.success.errorCode, error.errorCode) +// XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) +// +// Thread.sleep(forTimeInterval: 0.2) +// +// let note = "Test note" +// +// NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: "") { account, share, data, error in +// defer { expectation.fulfill() } +// +// XCTAssertEqual(self.account, account) +// XCTAssertEqual(NKError.success.errorCode, error.errorCode) +// XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) +// XCTAssertEqual(note, share?.note) +// } +// } waitForExpectations(timeout: 100) } diff --git a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift new file mode 100644 index 00000000..0aa61a4b --- /dev/null +++ b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +import XCTest +@testable import NextcloudKit + +final class FileAutoRenamerUnitTests: XCTestCase { + let fileAutoRenamer = FileAutoRenamer.shared + + let forbiddenFilenameCharacter = ">" + let forbiddenFilenameExtension = "." + + let initialCharacters = ["<", ">", ":", "\\\\", "/", "|", "?", "*", "&"] + let initialExtensions = [" ", ",", ".", ".filepart", ".part"] + + override func setUp() { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: initialCharacters, + forbiddenFileNameExtensions: initialExtensions + ) + super.setUp() + } + + func testInvalidChar() { + let filename = "file\(forbiddenFilenameCharacter)file.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "file_file.txt" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testInvalidExtension() { + let filename = "file\(forbiddenFilenameExtension)" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "file_" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testMultipleInvalidChars() { + let filename = "file|name?<>.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "file_name___.txt" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testStartEndInvalidExtensions() { + let filename = " .file.part " + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_file_part" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testStartEndInvalidExtensions2() { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: initialCharacters, + forbiddenFileNameExtensions: [",", ".", ".filepart", ".part", " "] + ) + + let filename = " .file.part " + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_file_part" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testStartEndInvalidExtensions3() { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: initialCharacters, + forbiddenFileNameExtensions: [".FILEPART", ".PART", " ", ",", "."] + ) + + let filename = " .file.part " + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_file_part" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testStartInvalidExtension() { + let filename = " .file.part" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_file_part" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testEndInvalidExtension() { + let filename = ".file.part " + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_file_part" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testMiddleNonPrintableChar() { + let filename = "file\u{0001}name.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "filename.txt" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testStartNonPrintableChar() { + let filename = "\u{0001}filename.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "filename.txt" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testEndNonPrintableChar() { + let filename = "filename.txt\u{0001}" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "filename.txt" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testExtensionNonPrintableChar() { + let filename = "filename.t\u{0001}xt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "filename.txt" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testMiddleInvalidFolderChar() { + let folderPath = "abc/def/kg\(forbiddenFilenameCharacter)/lmo/pp" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "abc/def/kg_/lmo/pp" + XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + } + + func testEndInvalidFolderChar() { + let folderPath = "abc/def/kg/lmo/pp\(forbiddenFilenameCharacter)" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "abc/def/kg/lmo/pp_" + XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + } + + func testStartInvalidFolderChar() { + let folderPath = "\(forbiddenFilenameCharacter)abc/def/kg/lmo/pp" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "_abc/def/kg/lmo/pp" + XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + } + + func testMixedInvalidChar() { + let filename = " file\u{0001}na\(forbiddenFilenameCharacter)me.txt " + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "filena_me.txt" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testStartsWithPathSeparator() { + let folderPath = "/abc/def/kg/lmo/pp\(forbiddenFilenameCharacter)/file.txt/" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "/abc/def/kg/lmo/pp_/file.txt/" + XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + } + + func testStartsWithPathSeparatorAndValidFilepath() { + let folderPath = "/COm02/2569.webp" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "/COm02/2569.webp" + XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + } +} + diff --git a/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift b/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift similarity index 64% rename from Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift rename to Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift index 0fe9e06b..92a9865d 100644 --- a/Tests/NextcloudKitUnitTests/FileNameValidatorTests.swift +++ b/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift @@ -1,29 +1,10 @@ -// -// fileNameValidatorTests.swift -// -// -// Created by Milen Pivchev on 12.07.24. -// Copyright © 2024 Milen Pivchev. All rights reserved. -// -// Author: Milen Pivchev -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later import XCTest @testable import NextcloudKit -class FileNameValidatorTests: XCTestCase { +class FileNameValidatorUnitTests: XCTestCase { let fileNameValidator = FileNameValidator.shared override func setUp() { @@ -123,46 +104,4 @@ class FileNameValidatorTests: XCTestCase { let result = fileNameValidator.checkFolderPath(folderPath) XCTAssertFalse(result) } - - func testValidFolderAndFilePaths() { - let folderPath = "validFolder" - - let result = fileNameValidator.checkFolderPath(folderPath: folderPath) - XCTAssertTrue(result) - } - - func testFolderPathWithReservedName() { - let folderPath = "CON" - - let result = fileNameValidator.checkFolderPath(folderPath: folderPath) - XCTAssertFalse(result) - } - - func testFolderPathWithInvalidCharacter() { - let folderPath = "invalid Date: Mon, 4 Nov 2024 16:52:58 +0100 Subject: [PATCH 074/135] Keep original extension Signed-off-by: Milen Pivchev --- .../NextcloudKit/Utils/FileAutoRenamer.swift | 20 ++++- .../FileAutoRenamerUnitTests.swift | 82 +++++++++++-------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift index a3eb24ab..fa050551 100644 --- a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift +++ b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift @@ -21,7 +21,7 @@ public class FileAutoRenamer { private var forbiddenFileNameExtensions: [String] = [] { didSet { - forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.uppercased()}) + forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.lowercased()}) } } @@ -52,16 +52,30 @@ public class FileAutoRenamer { modifiedSegment = modifiedSegment.trimmingCharacters(in: .whitespaces) } + // Replace forbidden extension, if any (ex. .part -> _part) forbiddenFileNameExtensions.forEach { forbiddenExtension in - if modifiedSegment.uppercased().hasSuffix(forbiddenExtension) && isFullExtension(forbiddenExtension) { - modifiedSegment = modifiedSegment.replacingOccurrences(of: ".", with: replacement, options: .caseInsensitive) + if modifiedSegment.lowercased().hasSuffix(forbiddenExtension) && isFullExtension(forbiddenExtension) { + let changedExtension = forbiddenExtension.replacingOccurrences(of: ".", with: replacement, options: .caseInsensitive) + modifiedSegment = modifiedSegment.replacingOccurrences(of: forbiddenExtension, with: changedExtension, options: .caseInsensitive) } + } + + // Keep original allowed extension and add it at the end (ex file.test.txt becomes file.test) + let fileExtension = modifiedSegment.fileExtension + modifiedSegment = modifiedSegment.withRemovedFileExtension + // Replace other forbidden extensions. Original allowed extension is ignored. + forbiddenFileNameExtensions.forEach { forbiddenExtension in if modifiedSegment.uppercased().hasSuffix(forbiddenExtension) || modifiedSegment.uppercased().hasPrefix(forbiddenExtension) { modifiedSegment = modifiedSegment.replacingOccurrences(of: forbiddenExtension, with: replacement, options: .caseInsensitive) } } + // If there is an original allowed extension, add it back (ex file_test becomes file_test.txt) + if !fileExtension.isEmpty { + modifiedSegment.append(".\(fileExtension.lowercased())") + } + return modifiedSegment } diff --git a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift index 0aa61a4b..8edf30c2 100644 --- a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift @@ -22,30 +22,30 @@ final class FileAutoRenamerUnitTests: XCTestCase { } func testInvalidChar() { - let filename = "file\(forbiddenFilenameCharacter)file.txt" + let filename = "File\(forbiddenFilenameCharacter)File.txt" let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "file_file.txt" + let expectedFilename = "File_File.txt" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testInvalidExtension() { - let filename = "file\(forbiddenFilenameExtension)" + let filename = "File\(forbiddenFilenameExtension)" let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "file_" + let expectedFilename = "File_" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testMultipleInvalidChars() { - let filename = "file|name?<>.txt" + let filename = "File|name?<>.txt" let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "file_name___.txt" + let expectedFilename = "File_name___.txt" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testStartEndInvalidExtensions() { - let filename = " .file.part " + let filename = " .File.part " let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_file_part" + let expectedFilename = "_File_part" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } @@ -55,9 +55,9 @@ final class FileAutoRenamerUnitTests: XCTestCase { forbiddenFileNameExtensions: [",", ".", ".filepart", ".part", " "] ) - let filename = " .file.part " + let filename = " .File.part " let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_file_part" + let expectedFilename = "_File_part" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } @@ -67,86 +67,100 @@ final class FileAutoRenamerUnitTests: XCTestCase { forbiddenFileNameExtensions: [".FILEPART", ".PART", " ", ",", "."] ) - let filename = " .file.part " + let filename = " .File.part " let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_file_part" + let expectedFilename = "_File_part" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testStartInvalidExtension() { - let filename = " .file.part" + let filename = " .File.part" let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_file_part" + let expectedFilename = "_File_part" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testEndInvalidExtension() { - let filename = ".file.part " + let filename = ".File.part " let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_file_part" + let expectedFilename = "_File_part" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testHiddenFile() { + let filename = ".Filename.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_Filename.txt" + XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + } + + func testUppercaseExtension() { + let filename = ".Filename.TXT" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_Filename.txt" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testMiddleNonPrintableChar() { - let filename = "file\u{0001}name.txt" + let filename = "File\u{0001}name.txt" let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "filename.txt" + let expectedFilename = "Filename.txt" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testStartNonPrintableChar() { - let filename = "\u{0001}filename.txt" + let filename = "\u{0001}Filename.txt" let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "filename.txt" + let expectedFilename = "Filename.txt" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testEndNonPrintableChar() { - let filename = "filename.txt\u{0001}" + let filename = "Filename.txt\u{0001}" let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "filename.txt" + let expectedFilename = "Filename.txt" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testExtensionNonPrintableChar() { - let filename = "filename.t\u{0001}xt" + let filename = "Filename.t\u{0001}xt" let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "filename.txt" + let expectedFilename = "Filename.txt" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testMiddleInvalidFolderChar() { - let folderPath = "abc/def/kg\(forbiddenFilenameCharacter)/lmo/pp" + let folderPath = "Abc/Def/kg\(forbiddenFilenameCharacter)/lmo/pp" let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) - let expectedFolderName = "abc/def/kg_/lmo/pp" + let expectedFolderName = "Abc/Def/kg_/lmo/pp" XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") } func testEndInvalidFolderChar() { - let folderPath = "abc/def/kg/lmo/pp\(forbiddenFilenameCharacter)" + let folderPath = "Abc/Def/kg/lmo/pp\(forbiddenFilenameCharacter)" let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) - let expectedFolderName = "abc/def/kg/lmo/pp_" + let expectedFolderName = "Abc/Def/kg/lmo/pp_" XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") } func testStartInvalidFolderChar() { - let folderPath = "\(forbiddenFilenameCharacter)abc/def/kg/lmo/pp" + let folderPath = "\(forbiddenFilenameCharacter)Abc/Def/kg/lmo/pp" let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) - let expectedFolderName = "_abc/def/kg/lmo/pp" + let expectedFolderName = "_Abc/Def/kg/lmo/pp" XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") } func testMixedInvalidChar() { - let filename = " file\u{0001}na\(forbiddenFilenameCharacter)me.txt " + let filename = " File\u{0001}na\(forbiddenFilenameCharacter)me.txt " let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "filena_me.txt" + let expectedFilename = "Filena_me.txt" XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") } func testStartsWithPathSeparator() { - let folderPath = "/abc/def/kg/lmo/pp\(forbiddenFilenameCharacter)/file.txt/" + let folderPath = "/Abc/Def/kg/lmo/pp\(forbiddenFilenameCharacter)/File.txt/" let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) - let expectedFolderName = "/abc/def/kg/lmo/pp_/file.txt/" + let expectedFolderName = "/Abc/Def/kg/lmo/pp_/File.txt/" XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") } From 6d794ec6470284a3d2728a3660b1391c29edf2c3 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 4 Nov 2024 16:55:25 +0100 Subject: [PATCH 075/135] Refactor Signed-off-by: Milen Pivchev --- Sources/NextcloudKit/Utils/FileAutoRenamer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift index fa050551..304d0097 100644 --- a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift +++ b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift @@ -66,7 +66,7 @@ public class FileAutoRenamer { // Replace other forbidden extensions. Original allowed extension is ignored. forbiddenFileNameExtensions.forEach { forbiddenExtension in - if modifiedSegment.uppercased().hasSuffix(forbiddenExtension) || modifiedSegment.uppercased().hasPrefix(forbiddenExtension) { + if modifiedSegment.lowercased().hasSuffix(forbiddenExtension) || modifiedSegment.lowercased().hasPrefix(forbiddenExtension) { modifiedSegment = modifiedSegment.replacingOccurrences(of: forbiddenExtension, with: replacement, options: .caseInsensitive) } } From 1a54b9a55934325ce99b618163c21b3829210a0c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 5 Nov 2024 11:41:04 +0100 Subject: [PATCH 076/135] License (#102) Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/Extensions/Image+Extension.swift | 6 +++--- Sources/NextcloudKit/Extensions/String+Extension.swift | 5 ++--- Sources/NextcloudKit/NKCommon.swift | 6 +++--- Sources/NextcloudKit/NKError.swift | 6 +++--- Sources/NextcloudKit/NKLogger.swift | 3 ++- Sources/NextcloudKit/NKModel.swift | 6 +++--- Sources/NextcloudKit/NKRequestOptions.swift | 6 +++--- Sources/NextcloudKit/NKSession.swift | 3 ++- Sources/NextcloudKit/NKShareAccounts.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+API.swift | 4 +++- Sources/NextcloudKit/NextcloudKit+Assistant.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+Comments.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+Dashboard.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+Download.swift | 3 ++- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+FilesLock.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+Groupfolders.swift | 6 +++--- Sources/NextcloudKit/NextcloudKit+Hovercard.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+Livephoto.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+Login.swift | 3 ++- Sources/NextcloudKit/NextcloudKit+NCText.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+PushNotification.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+Richdocuments.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+Search.swift | 6 +++--- Sources/NextcloudKit/NextcloudKit+Share.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+Upload.swift | 3 ++- Sources/NextcloudKit/NextcloudKit+UserStatus.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit+WebDAV.swift | 5 ++--- Sources/NextcloudKit/NextcloudKit.h | 5 ++--- Sources/NextcloudKit/NextcloudKit.swift | 5 ++--- Sources/NextcloudKit/NextcloudKitBackground.swift | 5 ++--- Sources/NextcloudKit/NextcloudKitSessionDelegate.swift | 4 +++- Sources/NextcloudKit/Utils/FileAutoRenamer.swift | 3 ++- Sources/NextcloudKit/Utils/FileNameValidator.swift | 3 ++- Sources/NextcloudKit/Utils/ThreadSafeArray.swift | 5 ++++- .../BaseIntegrationXCTestCase.swift | 5 ++--- .../FilesIntegrationTests.swift | 5 ++--- .../ShareIntegrationTests.swift | 5 ++--- Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift | 3 ++- .../NextcloudKitUnitTests/FileNameValidatorUnitTests.swift | 3 ++- Tests/NextcloudKitUnitTests/LoginUnitTests.swift | 5 ++--- 41 files changed, 93 insertions(+), 99 deletions(-) diff --git a/Sources/NextcloudKit/Extensions/Image+Extension.swift b/Sources/NextcloudKit/Extensions/Image+Extension.swift index e338b910..c45c6789 100644 --- a/Sources/NextcloudKit/Extensions/Image+Extension.swift +++ b/Sources/NextcloudKit/Extensions/Image+Extension.swift @@ -1,7 +1,7 @@ -// -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana +// SPDX-FileCopyrightText: 2021 Henrik Storch // SPDX-License-Identifier: GPL-3.0-or-later -// #if os(macOS) import Foundation diff --git a/Sources/NextcloudKit/Extensions/String+Extension.swift b/Sources/NextcloudKit/Extensions/String+Extension.swift index c3826f2e..ebfcde33 100644 --- a/Sources/NextcloudKit/Extensions/String+Extension.swift +++ b/Sources/NextcloudKit/Extensions/String+Extension.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2023 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index ef8e0d7b..2b0650fa 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -1,7 +1,7 @@ -// -// SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index 35633a34..f162a261 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -1,7 +1,7 @@ -// -// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Henrik Sorch +// SPDX-FileCopyrightText: 2023 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NKLogger.swift b/Sources/NextcloudKit/NKLogger.swift index da40f79d..5b85215b 100644 --- a/Sources/NextcloudKit/NKLogger.swift +++ b/Sources/NextcloudKit/NKLogger.swift @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later import Foundation diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKModel.swift index 22b948ba..dc843a04 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKModel.swift @@ -1,7 +1,7 @@ -// -// SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation diff --git a/Sources/NextcloudKit/NKRequestOptions.swift b/Sources/NextcloudKit/NKRequestOptions.swift index 7a0f03fd..e7bdeecd 100644 --- a/Sources/NextcloudKit/NKRequestOptions.swift +++ b/Sources/NextcloudKit/NKRequestOptions.swift @@ -1,7 +1,7 @@ -// -// SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2021 Henrik Sorch +// SPDX-FileCopyrightText: 2021 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index 580237a7..c945f5c6 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later import Foundation diff --git a/Sources/NextcloudKit/NKShareAccounts.swift b/Sources/NextcloudKit/NKShareAccounts.swift index 3bf85026..e9dcb00f 100644 --- a/Sources/NextcloudKit/NKShareAccounts.swift +++ b/Sources/NextcloudKit/NKShareAccounts.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2023 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation #if os(iOS) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 7e52aa20..a9ad6158 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -1,4 +1,6 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra // SPDX-License-Identifier: GPL-3.0-or-later #if os(macOS) diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 7372f2cb..1b558de2 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 201603ab..818bd325 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 3aa00fa9..160b0122 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Download.swift b/Sources/NextcloudKit/NextcloudKit+Download.swift index 6bb8f3a5..16081acb 100644 --- a/Sources/NextcloudKit/NextcloudKit+Download.swift +++ b/Sources/NextcloudKit/NextcloudKit+Download.swift @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index 00e336e1..38f6af7d 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index c70f45f7..c9d5678e 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Henrik Sorch // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 3b2db639..80413c3d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -1,7 +1,7 @@ -// -// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2023 Henrik Storch +// SPDX-FileCopyrightText: 2023 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index 25630b79..b5ba2dd6 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2021 Henrik Sorch // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 615df69a..5c49ad86 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2023 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 4f41dabf..df5ce0d7 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index 2fd02e34..9d2fb097 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index d7a30c9d..847093d9 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index 372d040a..e6e25894 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index 619d6201..82fecb02 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -1,7 +1,7 @@ -// -// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Henrik Storch +// SPDX-FileCopyrightText: 2023 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index d1b883e5..1b64ddbf 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index fb0f159d..a8d3ffa7 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later import Foundation diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index a877b38b..72260b4f 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 01ec094b..b29c461a 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation import Alamofire diff --git a/Sources/NextcloudKit/NextcloudKit.h b/Sources/NextcloudKit/NextcloudKit.h index 3c8c3f84..ac61028c 100644 --- a/Sources/NextcloudKit/NextcloudKit.h +++ b/Sources/NextcloudKit/NextcloudKit.h @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// #import diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index a9d3ef0f..e6006501 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// #if os(macOS) import Foundation diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 03c05715..8b7da1ec 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later -// import Foundation diff --git a/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift index 8e6aaeda..c8b42707 100644 --- a/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift +++ b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift @@ -1,4 +1,6 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 MarinoFaggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra // SPDX-License-Identifier: GPL-3.0-or-later import Foundation diff --git a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift index 304d0097..ca0d508a 100644 --- a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift +++ b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later import Foundation diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index dd313945..3c21ded5 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later import Foundation diff --git a/Sources/NextcloudKit/Utils/ThreadSafeArray.swift b/Sources/NextcloudKit/Utils/ThreadSafeArray.swift index 7fbf334d..1dec61d1 100644 --- a/Sources/NextcloudKit/Utils/ThreadSafeArray.swift +++ b/Sources/NextcloudKit/Utils/ThreadSafeArray.swift @@ -1,5 +1,8 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later +// +// This code derived from: http://basememara.com/creating-thread-safe-arrays-in-swift/ import Foundation diff --git a/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift b/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift index 1bfb73eb..1c29b757 100644 --- a/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift +++ b/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later -// import XCTest @testable import NextcloudKit diff --git a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift index bf99892e..9607a6e9 100644 --- a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later -// import XCTest @testable import NextcloudKit diff --git a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift index 7c8cb4bb..98b5572e 100644 --- a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later -// import XCTest import Alamofire diff --git a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift index 8edf30c2..27ba55cb 100644 --- a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later import XCTest diff --git a/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift b/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift index 92a9865d..fab4d2ed 100644 --- a/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift @@ -1,4 +1,5 @@ -// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later import XCTest diff --git a/Tests/NextcloudKitUnitTests/LoginUnitTests.swift b/Tests/NextcloudKitUnitTests/LoginUnitTests.swift index 8da52b43..d2293ff3 100644 --- a/Tests/NextcloudKitUnitTests/LoginUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/LoginUnitTests.swift @@ -1,7 +1,6 @@ -// -// SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later -// import XCTest @testable import NextcloudKit From 6f80c4d08b5fd87f865b587b9a7cfa0ef9df7f71 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 5 Nov 2024 12:11:29 +0100 Subject: [PATCH 077/135] ( multipathServiceType ) Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index c945f5c6..d24fe5ed 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -49,6 +49,7 @@ public class NKSession { /// Session Alamofire let configuration = URLSessionConfiguration.af.default configuration.requestCachePolicy = requestCachePolicy + configuration.multipathServiceType = .handover configuration.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionData = Alamofire.Session(configuration: configuration, delegate: NextcloudKitSessionDelegate(nkCommonInstance: NextcloudKit.shared.nkCommonInstance), @@ -64,6 +65,7 @@ public class NKSession { configurationDownloadBackground.isDiscretionary = false configurationDownloadBackground.httpMaximumConnectionsPerHost = 5 configurationDownloadBackground.requestCachePolicy = requestCachePolicy + configurationDownloadBackground.multipathServiceType = .handover configurationDownloadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionDownloadBackground = URLSession(configuration: configurationDownloadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) @@ -74,6 +76,7 @@ public class NKSession { configurationUploadBackground.isDiscretionary = false configurationUploadBackground.httpMaximumConnectionsPerHost = 5 configurationUploadBackground.requestCachePolicy = requestCachePolicy + configurationUploadBackground.multipathServiceType = .handover configurationUploadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackground = URLSession(configuration: configurationUploadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) @@ -95,6 +98,7 @@ public class NKSession { configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = 5 configurationUploadBackgroundExt.requestCachePolicy = requestCachePolicy configurationUploadBackgroundExt.sharedContainerIdentifier = groupIdentifier + configurationUploadBackgroundExt.multipathServiceType = .handover configurationUploadBackgroundExt.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackgroundExt = URLSession(configuration: configurationUploadBackgroundExt, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) } From a5b6f5cfb6559eead9c20267aaef9a4d6c8d01fc Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 6 Nov 2024 15:00:27 +0100 Subject: [PATCH 078/135] Add more extensive test for AutoRenamer (#101) * Add more extensive test Signed-off-by: Milen Pivchev * Force check Signed-off-by: Milen Pivchev * Revert "Force check" This reverts commit b7fadf683cb15be6f92ac6d9de6436db5bd4eb9e. * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev --------- Signed-off-by: Milen Pivchev Co-authored-by: Marino Faggiana --- .github/workflows/xcode.xxx | 71 ---- .github/workflows/xcode.yml | 96 +++++ .../BaseIntegrationXCTestCase.swift | 7 +- .../Common/BaseXCTestCase.swift | 33 ++ .../Common/TestConstants.swift | 14 + .../FilesIntegrationTests.swift | 34 +- .../ShareIntegrationTests.swift | 30 +- .../FileAutoRenamerUnitTests.swift | 353 ++++++++++++------ 8 files changed, 409 insertions(+), 229 deletions(-) delete mode 100644 .github/workflows/xcode.xxx create mode 100644 .github/workflows/xcode.yml create mode 100644 Tests/NextcloudKitIntegrationTests/Common/BaseXCTestCase.swift create mode 100644 Tests/NextcloudKitIntegrationTests/Common/TestConstants.swift diff --git a/.github/workflows/xcode.xxx b/.github/workflows/xcode.xxx deleted file mode 100644 index b0ace302..00000000 --- a/.github/workflows/xcode.xxx +++ /dev/null @@ -1,71 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: GPL-3.0-or-later -name: Build and test - -on: - push: - branches: - - master - - develop - pull_request: - types: [synchronize, opened, reopened, ready_for_review] - branches: - - master - - develop - -env: - DESTINATION_IOS: platform=iOS Simulator,name=iPhone 14 - DESTINATION_MACOS: platform=macOS,arch=x86_64 - SCHEME: NextcloudKit - -jobs: - build-and-test: - name: Build and Test - runs-on: macOS-latest - if: github.event.pull_request.draft == false - steps: - - name: Set env var - run: echo "DEVELOPER_DIR=$(xcode-select --print-path)" >> $GITHUB_ENV - - uses: actions/checkout@v3 - - name: Setup Bundler and Install Gems - run: | - gem install bundler - bundle install - bundle update - - name: Install docker - run: | - # Workaround for https://github.com/actions/runner-images/issues/8104 - brew remove --ignore-dependencies qemu - curl -o ./qemu.rb https://raw.githubusercontent.com/Homebrew/homebrew-core/dc0669eca9479e9eeb495397ba3a7480aaa45c2e/Formula/qemu.rb - brew install ./qemu.rb - - brew install docker - colima start - - name: Create docker test server and export enviroment variables - run: | - source ./create-docker-test-server.sh - if [ ! -f ".env-vars" ]; then - touch .env-vars - echo "export TEST_SERVER_URL=$TEST_SERVER_URL" >> .env-vars - echo "export TEST_USER=$TEST_USER" >> .env-vars - echo "export TEST_APP_PASSWORD=$TEST_APP_PASSWORD" >> .env-vars - fi - - name: Generate EnvVars file - run: | - ./generate-env-vars.sh - - name: Build & Test NextcloudKit - run: | - set -o pipefail && xcodebuild test -scheme "$SCHEME" \ - -destination "$DESTINATION_IOS" \ - -destination "$DESTINATION_MACOS" \ - -enableCodeCoverage YES \ - -test-iterations 3 \ - -retry-tests-on-failure \ - | xcpretty -# Covecov does not yet support pure swift packages. Check here: https://github.com/SlatherOrg/slather/issues/466 -# - name: Upload coverage to codecov -# run: | -# bundle exec slather -# bash <(curl -s https://codecov.io/bash) -f ./cobertura.xml -X coveragepy -X gcov -X xcode -t ${{ secrets.CODECOV_TOKEN }} - - diff --git a/.github/workflows/xcode.yml b/.github/workflows/xcode.yml new file mode 100644 index 00000000..01a0ee60 --- /dev/null +++ b/.github/workflows/xcode.yml @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later +name: Build and test + +on: + push: + branches: + - master + - develop + pull_request: + types: [synchronize, opened, reopened, ready_for_review] + branches: + - master + - develop + +env: + DESTINATION_IOS: platform=iOS Simulator,name=iPhone 16,OS=18.1 + DESTINATION_MACOS: platform=macOS,arch=x86_64 + SCHEME: NextcloudKit + SERVER_BRANCH: stable28 + PHP_VERSION: 8.2 + +jobs: + build-and-test: + name: Build and Test + runs-on: macos-15 + if: github.event.pull_request.draft == false + steps: + - name: Set env var + run: echo "DEVELOPER_DIR=$(xcode-select --print-path)" >> $GITHUB_ENV + - uses: actions/checkout@v4 + + - name: Set up php ${{ env.PHP_VERSION }} + uses: shivammathur/setup-php@8872c784b04a1420e81191df5d64fbd59d3d3033 # v2.30.0 + with: + php-version: ${{ env.PHP_VERSION }} + # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation + extensions: apcu, bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql + coverage: none + ini-file: development + # Temporary workaround for missing pcntl_* in PHP 8.3: ini-values: apc.enable_cli=on + ini-values: apc.enable_cli=on, disable_functions= + + - name: Checkout server + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + submodules: true + repository: nextcloud/server + path: server + ref: ${{ env.SERVER_BRANCH }} + + - name: Set up Nextcloud + run: | + mkdir server/data + ./server/occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./server/occ config:system:set hashing_default_password --value=true --type=boolean + ./server/occ config:system:set auth.bruteforce.protection.enabled --value false --type bool + ./server/occ config:system:set ratelimit.protection.enabled --value false --type bool + ./server/occ config:system:set memcache.local --value="\\OC\\Memcache\\APCu" + ./server/occ config:system:set memcache.distributed --value="\\OC\\Memcache\\APCu" + ./server/occ background:cron + PHP_CLI_SERVER_WORKERS=5 php -S localhost:8080 -t server/ & +# - name: Setup Bundler and Install Gems +# run: | +# gem install bundler +# bundle install +# bundle update +# - name: Install docker +# run: | +# # Workaround for https://github.com/actions/runner-images/issues/8104 +# brew remove --ignore-dependencies qemu +# curl -o ./qemu.rb https://raw.githubusercontent.com/Homebrew/homebrew-core/dc0669eca9479e9eeb495397ba3a7480aaa45c2e/Formula/qemu.rb +# brew install ./qemu.rb +# +# brew install docker +# colima start +# - name: Create docker test server and export enviroment variables +# run: | +# source ./create-docker-test-server.sh +# if [ ! -f ".env-vars" ]; then +# touch .env-vars +# echo "export TEST_SERVER_URL=$TEST_SERVER_URL" >> .env-vars +# echo "export TEST_USER=$TEST_USER" >> .env-vars +# echo "export TEST_APP_PASSWORD=$TEST_APP_PASSWORD" >> .env-vars +# fi +# - name: Generate EnvVars file +# run: | +# ./generate-env-vars.sh + - name: Build & Test NextcloudKit + run: | + set -o pipefail && xcodebuild test -scheme "$SCHEME" \ + -destination "$DESTINATION_IOS" \ + -test-iterations 3 \ + -retry-tests-on-failure \ + | xcpretty + diff --git a/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift b/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift index 1c29b757..f1841edb 100644 --- a/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift +++ b/Tests/NextcloudKitIntegrationTests/BaseIntegrationXCTestCase.swift @@ -5,12 +5,7 @@ import XCTest @testable import NextcloudKit -class BaseIntegrationXCTestCase: XCTestCase { - internal let baseUrl = EnvVars.testServerUrl - internal let user = EnvVars.testUser - internal let userId = EnvVars.testUser - internal let password = EnvVars.testAppPassword - internal lazy var account = "\(userId) \(baseUrl)" +class BaseIntegrationXCTestCase: BaseXCTestCase { internal var randomInt: Int { get { return Int.random(in: 1000...Int.max) diff --git a/Tests/NextcloudKitIntegrationTests/Common/BaseXCTestCase.swift b/Tests/NextcloudKitIntegrationTests/Common/BaseXCTestCase.swift new file mode 100644 index 00000000..393d2e78 --- /dev/null +++ b/Tests/NextcloudKitIntegrationTests/Common/BaseXCTestCase.swift @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import XCTest +import Foundation +import UIKit +import Alamofire +import NextcloudKit + +class BaseXCTestCase: XCTestCase { + var appToken = "" + + func setupAppToken() { + let expectation = expectation(description: "Should get app token") + + NextcloudKit.shared.getAppPassword(url: TestConstants.server, user: TestConstants.username, password: TestConstants.password) { token, _, error in + XCTAssertEqual(error.errorCode, 0) + XCTAssertNotNil(token) + + guard let token else { return XCTFail() } + + self.appToken = token + expectation.fulfill() + } + + waitForExpectations(timeout: TestConstants.timeoutLong) + } + + override func setUpWithError() throws { + setupAppToken() + } +} diff --git a/Tests/NextcloudKitIntegrationTests/Common/TestConstants.swift b/Tests/NextcloudKitIntegrationTests/Common/TestConstants.swift new file mode 100644 index 00000000..13baf802 --- /dev/null +++ b/Tests/NextcloudKitIntegrationTests/Common/TestConstants.swift @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import UIKit + +public class TestConstants { + static let timeoutLong: Double = 400 + static let server = "http://localhost:8080" + static let username = "admin" + static let password = "admin" + static let account = "\(username) \(server)" +} diff --git a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift index 9607a6e9..c9c3a1be 100644 --- a/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/FilesIntegrationTests.swift @@ -6,17 +6,17 @@ import XCTest @testable import NextcloudKit final class FilesIntegrationTests: BaseIntegrationXCTestCase { - func test_createReadDeleteFolder_withProperParams_shouldCreateReadDeleteFolder() throws { - let expectation = expectation(description: "Should finish last callback") - let folderName = "TestFolder\(randomInt)" - let serverUrl = "\(baseUrl)/remote.php/dav/files/\(userId)" - let serverUrlFileName = "\(serverUrl)/\(folderName)" - - NextcloudKit.shared.appendSession(account: account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") - +// func test_createReadDeleteFolder_withProperParams_shouldCreateReadDeleteFolder() throws { +// let expectation = expectation(description: "Should finish last callback") +// let folderName = "TestFolder\(randomInt)" +// let serverUrl = "\(TestConstants.server)/remote.php/dav/files/\(TestConstants.username)" +// let serverUrlFileName = "\(serverUrl)/\(folderName)" +// +// NextcloudKit.shared.appendSession(account: TestConstants.account, urlBase: TestConstants.server, user: TestConstants.username, userId: TestConstants.username, password: TestConstants.password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") +// // // Test creating folder -// NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in -// XCTAssertEqual(self.account, account) +// NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: TestConstants.account) { account, ocId, date, _, error in +// XCTAssertEqual(TestConstants.account, account) // // XCTAssertEqual(NKError.success.errorCode, error.errorCode) // XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) @@ -25,7 +25,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { // // // Test reading folder, should exist // NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", account: account) { account, files, data, error in -// XCTAssertEqual(self.account, account) +// XCTAssertEqual(TestConstants.account, account) // XCTAssertEqual(NKError.success.errorCode, error.errorCode) // XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) // XCTAssertEqual(files?[0].fileName, folderName) @@ -33,8 +33,8 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { // Thread.sleep(forTimeInterval: 0.2) // // // Test deleting folder -// NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: account) { account, error in -// XCTAssertEqual(self.account, account) +// NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: account) { account, _, error in +// XCTAssertEqual(TestConstants.account, account) // XCTAssertEqual(NKError.success.errorCode, error.errorCode) // XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) // @@ -45,13 +45,13 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { // defer { expectation.fulfill() } // // XCTAssertEqual(404, error.errorCode) -// XCTAssertEqual(self.account, account) +// XCTAssertEqual(TestConstants.account, account) // XCTAssertTrue(files?.isEmpty ?? false) // } // } // } // } - - waitForExpectations(timeout: 100) - } +// +// waitForExpectations(timeout: 100) +// } } diff --git a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift index 98b5572e..3f52d073 100644 --- a/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift +++ b/Tests/NextcloudKitIntegrationTests/ShareIntegrationTests.swift @@ -7,17 +7,17 @@ import Alamofire @testable import NextcloudKit final class ShareIntegrationTests: BaseIntegrationXCTestCase { - func test_createShare_withNote_shouldCreateShare() throws { - let expectation = expectation(description: "Should finish last callback") - - let folderName = "Share\(randomInt)" - let serverUrl = "\(baseUrl)/remote.php/dav/files/\(userId)" - let serverUrlFileName = "\(serverUrl)/\(folderName)" - - NextcloudKit.shared.appendSession(account: account, urlBase: baseUrl, user: user, userId: userId, password: password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") - -// NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: account) { account, ocId, date, error in -// XCTAssertEqual(self.account, account) +// func test_createShare_withNote_shouldCreateShare() throws { +// let expectation = expectation(description: "Should finish last callback") +// +// let folderName = "Share\(randomInt)" +// let serverUrl = "\(TestConstants.server)/remote.php/dav/files/\(TestConstants.username)" +// let serverUrlFileName = "\(serverUrl)/\(folderName)" +// +// NextcloudKit.shared.appendSession(account: TestConstants.account, urlBase: TestConstants.server, user: TestConstants.username, userId: TestConstants.username, password: TestConstants.password, userAgent: "", nextcloudVersion: 0, groupIdentifier: "") +// +// NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: TestConstants.account) { account, ocId, date, _, error in +// XCTAssertEqual(TestConstants.account, account) // // XCTAssertEqual(NKError.success.errorCode, error.errorCode) // XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) @@ -29,13 +29,13 @@ final class ShareIntegrationTests: BaseIntegrationXCTestCase { // NextcloudKit.shared.createShare(path: folderName, shareType: 0, shareWith: "nextcloud", note: note, account: "") { account, share, data, error in // defer { expectation.fulfill() } // -// XCTAssertEqual(self.account, account) +// XCTAssertEqual(TestConstants.account, account) // XCTAssertEqual(NKError.success.errorCode, error.errorCode) // XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) // XCTAssertEqual(note, share?.note) // } // } - - waitForExpectations(timeout: 100) - } +// +// waitForExpectations(timeout: 100) +// } } diff --git a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift index 27ba55cb..d82ae672 100644 --- a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift @@ -2,174 +2,287 @@ // SPDX-FileCopyrightText: 2024 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later -import XCTest +import Testing @testable import NextcloudKit -final class FileAutoRenamerUnitTests: XCTestCase { +@Suite(.serialized) struct FileAutoRenamerUnitTests { let fileAutoRenamer = FileAutoRenamer.shared let forbiddenFilenameCharacter = ">" let forbiddenFilenameExtension = "." - let initialCharacters = ["<", ">", ":", "\\\\", "/", "|", "?", "*", "&"] - let initialExtensions = [" ", ",", ".", ".filepart", ".part"] + let characterArrays = [ + ["\\\\", "*", ">", "&", "/", "|", ":", "<", "?"], + [">", ":", "?", "&", "*", "\\\\", "|", "<", "/"], + ["<", "|", "?", ":", "&", "*", "\\\\", "/", ">"], + ["?", "/", ":", "&", "<", "|", ">", "\\\\", "*"], + ["&", "<", "|", "*", "/", "?", ">", ":", "\\\\"] + ] - override func setUp() { - fileAutoRenamer.setup( - forbiddenFileNameCharacters: initialCharacters, - forbiddenFileNameExtensions: initialExtensions - ) - super.setUp() - } + let extensionArrays = [ + [" ", ",", ".", ".filepart", ".part"], + [".filepart", ".part", " ", ".", ","], + [".PART", ".", ",", " ", ".filepart"], + [",", " ", ".FILEPART", ".part", "."], + [".", ".PART", ",", " ", ".FILEPART"] + ] - func testInvalidChar() { - let filename = "File\(forbiddenFilenameCharacter)File.txt" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "File_File.txt" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") - } + let combinedTuples: [([String], [String])] - func testInvalidExtension() { - let filename = "File\(forbiddenFilenameExtension)" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "File_" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + init() { + combinedTuples = zip(characterArrays, extensionArrays).map { ($0, $1) } } - func testMultipleInvalidChars() { - let filename = "File|name?<>.txt" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "File_name___.txt" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testInvalidChar() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = "File\(forbiddenFilenameCharacter)File.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "File_File.txt" + #expect(result == expectedFilename) + } } - func testStartEndInvalidExtensions() { - let filename = " .File.part " - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_File_part" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testInvalidExtension() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = "File\(forbiddenFilenameExtension)" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "File_" + #expect(result == expectedFilename) + } } - func testStartEndInvalidExtensions2() { - fileAutoRenamer.setup( - forbiddenFileNameCharacters: initialCharacters, - forbiddenFileNameExtensions: [",", ".", ".filepart", ".part", " "] - ) + @Test func testMultipleInvalidChars() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) - let filename = " .File.part " - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_File_part" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + let filename = "File|name?<>.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "File_name___.txt" + #expect(result == expectedFilename) + } } - func testStartEndInvalidExtensions3() { - fileAutoRenamer.setup( - forbiddenFileNameCharacters: initialCharacters, - forbiddenFileNameExtensions: [".FILEPART", ".PART", " ", ",", "."] - ) + @Test func testStartEndInvalidExtensions() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) - let filename = " .File.part " - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_File_part" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + let filename = " .File.part " + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_File_part" + #expect(result == expectedFilename) + } } - func testStartInvalidExtension() { - let filename = " .File.part" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_File_part" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testStartInvalidExtension() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = " .File.part" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_File_part" + #expect(result == expectedFilename) + } } - func testEndInvalidExtension() { - let filename = ".File.part " - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_File_part" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testEndInvalidExtension() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = ".File.part " + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_File_part" + #expect(result == expectedFilename) + } } - func testHiddenFile() { - let filename = ".Filename.txt" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_Filename.txt" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testHiddenFile() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = ".Filename.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_Filename.txt" + #expect(result == expectedFilename) + } } - func testUppercaseExtension() { - let filename = ".Filename.TXT" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "_Filename.txt" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testUppercaseExtension() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = ".Filename.TXT" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "_Filename.txt" + #expect(result == expectedFilename) + } } - func testMiddleNonPrintableChar() { - let filename = "File\u{0001}name.txt" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "Filename.txt" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testMiddleNonPrintableChar() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = "File\u{0001}name.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "Filename.txt" + #expect(result == expectedFilename) + } } - func testStartNonPrintableChar() { - let filename = "\u{0001}Filename.txt" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "Filename.txt" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testStartNonPrintableChar() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = "\u{0001}Filename.txt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "Filename.txt" + #expect(result == expectedFilename) + } } - func testEndNonPrintableChar() { - let filename = "Filename.txt\u{0001}" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "Filename.txt" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testEndNonPrintableChar() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = "Filename.txt\u{0001}" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "Filename.txt" + #expect(result == expectedFilename) + } } - func testExtensionNonPrintableChar() { - let filename = "Filename.t\u{0001}xt" - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "Filename.txt" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testExtensionNonPrintableChar() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = "Filename.t\u{0001}xt" + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "Filename.txt" + #expect(result == expectedFilename) + } } - func testMiddleInvalidFolderChar() { - let folderPath = "Abc/Def/kg\(forbiddenFilenameCharacter)/lmo/pp" - let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) - let expectedFolderName = "Abc/Def/kg_/lmo/pp" - XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + @Test func testMiddleInvalidFolderChar() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let folderPath = "Abc/Def/kg\(forbiddenFilenameCharacter)/lmo/pp" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "Abc/Def/kg_/lmo/pp" + #expect(result == expectedFolderName) + } } - func testEndInvalidFolderChar() { - let folderPath = "Abc/Def/kg/lmo/pp\(forbiddenFilenameCharacter)" - let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) - let expectedFolderName = "Abc/Def/kg/lmo/pp_" - XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + @Test func testEndInvalidFolderChar() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let folderPath = "Abc/Def/kg/lmo/pp\(forbiddenFilenameCharacter)" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "Abc/Def/kg/lmo/pp_" + #expect(result == expectedFolderName) + } } - func testStartInvalidFolderChar() { - let folderPath = "\(forbiddenFilenameCharacter)Abc/Def/kg/lmo/pp" - let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) - let expectedFolderName = "_Abc/Def/kg/lmo/pp" - XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + @Test func testStartInvalidFolderChar() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let folderPath = "\(forbiddenFilenameCharacter)Abc/Def/kg/lmo/pp" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "_Abc/Def/kg/lmo/pp" + #expect(result == expectedFolderName) + } } - func testMixedInvalidChar() { - let filename = " File\u{0001}na\(forbiddenFilenameCharacter)me.txt " - let result = fileAutoRenamer.rename(filename: filename) - let expectedFilename = "Filena_me.txt" - XCTAssertEqual(result, expectedFilename, "Expected \(expectedFilename) but got \(result)") + @Test func testMixedInvalidChar() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let filename = " File\u{0001}na\(forbiddenFilenameCharacter)me.txt " + let result = fileAutoRenamer.rename(filename: filename) + let expectedFilename = "Filena_me.txt" + #expect(result == expectedFilename) + } } - func testStartsWithPathSeparator() { - let folderPath = "/Abc/Def/kg/lmo/pp\(forbiddenFilenameCharacter)/File.txt/" - let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) - let expectedFolderName = "/Abc/Def/kg/lmo/pp_/File.txt/" - XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + @Test func testStartsWithPathSeparator() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let folderPath = "/Abc/Def/kg/lmo/pp\(forbiddenFilenameCharacter)/File.txt/" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "/Abc/Def/kg/lmo/pp_/File.txt/" + #expect(result == expectedFolderName) + } } - func testStartsWithPathSeparatorAndValidFilepath() { - let folderPath = "/COm02/2569.webp" - let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) - let expectedFolderName = "/COm02/2569.webp" - XCTAssertEqual(result, expectedFolderName, "Expected \(expectedFolderName) but got \(result)") + @Test func testStartsWithPathSeparatorAndValidFilepath() { + for (characterArray, extensionArray) in combinedTuples { + fileAutoRenamer.setup( + forbiddenFileNameCharacters: characterArray, + forbiddenFileNameExtensions: extensionArray + ) + + let folderPath = "/COm02/2569.webp" + let result = fileAutoRenamer.rename(filename: folderPath, isFolderPath: true) + let expectedFolderName = "/COm02/2569.webp" + #expect(result == expectedFolderName) + } } } From 69d7822399f8789614d55446529887506c8bd3a8 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Thu, 14 Nov 2024 15:18:46 +0100 Subject: [PATCH 079/135] Split NKModel.swift into designated source code files for every contained type. Signed-off-by: Iva Horn --- Sources/NextcloudKit/Models/NKActivity.swift | 24 ++ Sources/NextcloudKit/Models/NKComments.swift | 20 ++ .../Models/NKEditorDetailsCreators.swift | 15 + .../Models/NKEditorDetailsEditors.swift | 13 + .../Models/NKEditorTemplates.swift | 15 + .../NextcloudKit/Models/NKExternalSite.swift | 16 + Sources/NextcloudKit/Models/NKFile.swift | 77 +++++ .../NextcloudKit/Models/NKFileProperty.swift | 13 + .../NextcloudKit/Models/NKProperties.swift | 75 +++++ .../Models/NKRichdocumentsTemplate.swift | 15 + Sources/NextcloudKit/Models/NKSharee.swift | 20 ++ Sources/NextcloudKit/Models/NKTrash.swift | 22 ++ .../NextcloudKit/Models/NKUserProfile.swift | 32 ++ .../NextcloudKit/Models/NKUserStatus.swift | 18 ++ .../{NKModel.swift => NKDataFileXML.swift} | 305 +----------------- 15 files changed, 376 insertions(+), 304 deletions(-) create mode 100644 Sources/NextcloudKit/Models/NKActivity.swift create mode 100644 Sources/NextcloudKit/Models/NKComments.swift create mode 100644 Sources/NextcloudKit/Models/NKEditorDetailsCreators.swift create mode 100644 Sources/NextcloudKit/Models/NKEditorDetailsEditors.swift create mode 100644 Sources/NextcloudKit/Models/NKEditorTemplates.swift create mode 100644 Sources/NextcloudKit/Models/NKExternalSite.swift create mode 100644 Sources/NextcloudKit/Models/NKFile.swift create mode 100644 Sources/NextcloudKit/Models/NKFileProperty.swift create mode 100644 Sources/NextcloudKit/Models/NKProperties.swift create mode 100644 Sources/NextcloudKit/Models/NKRichdocumentsTemplate.swift create mode 100644 Sources/NextcloudKit/Models/NKSharee.swift create mode 100644 Sources/NextcloudKit/Models/NKTrash.swift create mode 100644 Sources/NextcloudKit/Models/NKUserProfile.swift create mode 100644 Sources/NextcloudKit/Models/NKUserStatus.swift rename Sources/NextcloudKit/{NKModel.swift => NKDataFileXML.swift} (71%) diff --git a/Sources/NextcloudKit/Models/NKActivity.swift b/Sources/NextcloudKit/Models/NKActivity.swift new file mode 100644 index 00000000..6fd939ba --- /dev/null +++ b/Sources/NextcloudKit/Models/NKActivity.swift @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKActivity: NSObject { + public var app = "" + public var date = Date() + public var idActivity: Int = 0 + public var icon = "" + public var link = "" + public var message = "" + public var messageRich: Data? + public var objectId: Int = 0 + public var objectName = "" + public var objectType = "" + public var previews: Data? + public var subject = "" + public var subjectRich: Data? + public var type = "" + public var user = "" +} diff --git a/Sources/NextcloudKit/Models/NKComments.swift b/Sources/NextcloudKit/Models/NKComments.swift new file mode 100644 index 00000000..e187d18a --- /dev/null +++ b/Sources/NextcloudKit/Models/NKComments.swift @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKComments: NSObject { + public var actorDisplayName = "" + public var actorId = "" + public var actorType = "" + public var creationDateTime = Date() + public var isUnread: Bool = false + public var message = "" + public var messageId = "" + public var objectId = "" + public var objectType = "" + public var path = "" + public var verb = "" +} diff --git a/Sources/NextcloudKit/Models/NKEditorDetailsCreators.swift b/Sources/NextcloudKit/Models/NKEditorDetailsCreators.swift new file mode 100644 index 00000000..75243edc --- /dev/null +++ b/Sources/NextcloudKit/Models/NKEditorDetailsCreators.swift @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKEditorDetailsCreators: NSObject { + public var editor = "" + public var ext = "" + public var identifier = "" + public var mimetype = "" + public var name = "" + public var templates: Int = 0 +} diff --git a/Sources/NextcloudKit/Models/NKEditorDetailsEditors.swift b/Sources/NextcloudKit/Models/NKEditorDetailsEditors.swift new file mode 100644 index 00000000..322b2db5 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKEditorDetailsEditors.swift @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKEditorDetailsEditors: NSObject { + public var mimetypes: [String] = [] + public var name = "" + public var optionalMimetypes: [String] = [] + public var secure: Int = 0 +} diff --git a/Sources/NextcloudKit/Models/NKEditorTemplates.swift b/Sources/NextcloudKit/Models/NKEditorTemplates.swift new file mode 100644 index 00000000..a14c25bf --- /dev/null +++ b/Sources/NextcloudKit/Models/NKEditorTemplates.swift @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKEditorTemplates: NSObject { + public var delete = "" + public var ext = "" + public var identifier = "" + public var name = "" + public var preview = "" + public var type = "" +} diff --git a/Sources/NextcloudKit/Models/NKExternalSite.swift b/Sources/NextcloudKit/Models/NKExternalSite.swift new file mode 100644 index 00000000..158418b8 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKExternalSite.swift @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKExternalSite: NSObject { + public var icon = "" + public var idExternalSite: Int = 0 + public var lang = "" + public var name = "" + public var order: Int = 0 + public var type = "" + public var url = "" +} diff --git a/Sources/NextcloudKit/Models/NKFile.swift b/Sources/NextcloudKit/Models/NKFile.swift new file mode 100644 index 00000000..1da7b9d5 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKFile.swift @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKFile: NSObject { + public var account = "" + public var classFile = "" + public var commentsUnread: Bool = false + public var contentType = "" + public var checksums = "" + public var creationDate: Date? + public var dataFingerprint = "" + public var date = Date() + public var directory: Bool = false + public var downloadURL = "" + public var e2eEncrypted: Bool = false + public var etag = "" + public var favorite: Bool = false + public var fileId = "" + public var fileName = "" + public var hasPreview: Bool = false + public var iconName = "" + public var mountType = "" + public var name = "" + public var note = "" + public var ocId = "" + public var ownerId = "" + public var ownerDisplayName = "" + public var lock = false + public var lockOwner = "" + public var lockOwnerEditor = "" + public var lockOwnerType = 0 + public var lockOwnerDisplayName = "" + public var lockTime: Date? + public var lockTimeOut: Date? + public var path = "" + public var permissions = "" + public var quotaUsedBytes: Int64 = 0 + public var quotaAvailableBytes: Int64 = 0 + public var resourceType = "" + public var richWorkspace: String? + public var sharePermissionsCollaborationServices: Int = 0 + public var sharePermissionsCloudMesh: [String] = [] + public var shareType: [Int] = [] + public var size: Int64 = 0 + public var serverUrl = "" + public var tags: [String] = [] + public var trashbinFileName = "" + public var trashbinOriginalLocation = "" + public var trashbinDeletionTime = Date() + public var uploadDate: Date? + public var urlBase = "" + public var user = "" + public var userId = "" + public var latitude: Double = 0 + public var longitude: Double = 0 + public var altitude: Double = 0 + public var height: Double = 0 + public var width: Double = 0 + public var hidden = false + /// If this is not empty, the media is a live photo. New media gets this straight from server, but old media needs to be detected as live photo (look isFlaggedAsLivePhotoByServer) + public var livePhotoFile = "" + /// Indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side + public var isFlaggedAsLivePhotoByServer = false + /// + public var datePhotosOriginal: Date? + /// + public struct ChildElement { + let name: String + let text: String? + } + public var exifPhotos = [[String: String?]]() + public var placePhotos: String? +} diff --git a/Sources/NextcloudKit/Models/NKFileProperty.swift b/Sources/NextcloudKit/Models/NKFileProperty.swift new file mode 100644 index 00000000..1da8f4d5 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKFileProperty.swift @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKFileProperty: NSObject { + public var classFile: String = "" + public var iconName: String = "" + public var name: String = "" + public var ext: String = "" +} diff --git a/Sources/NextcloudKit/Models/NKProperties.swift b/Sources/NextcloudKit/Models/NKProperties.swift new file mode 100644 index 00000000..929d4473 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKProperties.swift @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +public enum NKProperties: String, CaseIterable { + /// DAV + case displayname = "" + case getlastmodified = "" + case getetag = "" + case getcontenttype = "" + case resourcetype = "" + case quotaavailablebytes = "" + case quotausedbytes = "" + case getcontentlength = "" + /// owncloud.org + case permissions = "" + case id = "" + case fileid = "" + case size = "" + case favorite = "" + case sharetypes = "" + case ownerid = "" + case ownerdisplayname = "" + case commentsunread = "" + case checksums = "" + case downloadURL = "" + case datafingerprint = "" + /// nextcloud.org + case creationtime = "" + case uploadtime = "" + case isencrypted = "" + case haspreview = "" + case mounttype = "" + case richworkspace = "" + case note = "" + case lock = "" + case lockowner = "" + case lockownereditor = "" + case lockownerdisplayname = "" + case lockownertype = "" + case locktime = "" + case locktimeout = "" + case systemtags = "" + case filemetadatasize = "" + case filemetadatagps = "" + case metadataphotosexif = "" + case metadataphotosgps = "" + case metadataphotosoriginaldatetime = "" + case metadataphotoplace = "" + case metadataphotossize = "" + case metadatafileslivephoto = "" + case hidden = "" + /// open-collaboration-services.org + case sharepermissionscollaboration = "" + /// open-cloud-mesh.org + case sharepermissionscloudmesh = "" + + static func properties(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { + var properties = allCases.map { $0.rawValue }.joined() + if let createProperties { + properties = "" + properties = createProperties.map { $0.rawValue }.joined(separator: "") + } + for removeProperty in removeProperties { + properties = properties.replacingOccurrences(of: removeProperty.rawValue, with: "") + } + return properties + } + + static func trashProperties() -> String { + let properties: [String] = [displayname.rawValue, getcontenttype.rawValue, resourcetype.rawValue, id.rawValue, fileid.rawValue, size.rawValue, haspreview.rawValue, "", "", ""] + return properties.joined() + } +} diff --git a/Sources/NextcloudKit/Models/NKRichdocumentsTemplate.swift b/Sources/NextcloudKit/Models/NKRichdocumentsTemplate.swift new file mode 100644 index 00000000..7b26206f --- /dev/null +++ b/Sources/NextcloudKit/Models/NKRichdocumentsTemplate.swift @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKRichdocumentsTemplate: NSObject { + public var delete = "" + public var ext = "" + public var name = "" + public var preview = "" + public var templateId: Int = 0 + public var type = "" +} diff --git a/Sources/NextcloudKit/Models/NKSharee.swift b/Sources/NextcloudKit/Models/NKSharee.swift new file mode 100644 index 00000000..6ef868b8 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKSharee.swift @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKSharee: NSObject { + public var circleInfo = "" + public var circleOwner = "" + public var label = "" + public var name = "" + public var shareType: Int = 0 + public var shareWith = "" + public var uuid = "" + public var userClearAt: Date? + public var userIcon = "" + public var userMessage = "" + public var userStatus = "" +} diff --git a/Sources/NextcloudKit/Models/NKTrash.swift b/Sources/NextcloudKit/Models/NKTrash.swift new file mode 100644 index 00000000..eb002578 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKTrash.swift @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKTrash: NSObject { + public var contentType = "" + public var date = Date() + public var directory: Bool = false + public var fileId = "" + public var fileName = "" + public var filePath = "" + public var hasPreview: Bool = false + public var iconName = "" + public var size: Int64 = 0 + public var classFile = "" + public var trashbinFileName = "" + public var trashbinOriginalLocation = "" + public var trashbinDeletionTime = Date() +} diff --git a/Sources/NextcloudKit/Models/NKUserProfile.swift b/Sources/NextcloudKit/Models/NKUserProfile.swift new file mode 100644 index 00000000..4b6ed0fd --- /dev/null +++ b/Sources/NextcloudKit/Models/NKUserProfile.swift @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKUserProfile: NSObject { + public var address = "" + public var backend = "" + public var backendCapabilitiesSetDisplayName: Bool = false + public var backendCapabilitiesSetPassword: Bool = false + public var displayName = "" + public var email = "" + public var enabled: Bool = false + public var groups: [String] = [] + public var language = "" + public var lastLogin: Int64 = 0 + public var locale = "" + public var organisation = "" + public var phone = "" + public var quota: Int64 = 0 + public var quotaFree: Int64 = 0 + public var quotaRelative: Double = 0 + public var quotaTotal: Int64 = 0 + public var quotaUsed: Int64 = 0 + public var storageLocation = "" + public var subadmin: [String] = [] + public var twitter = "" + public var userId = "" + public var website = "" +} diff --git a/Sources/NextcloudKit/Models/NKUserStatus.swift b/Sources/NextcloudKit/Models/NKUserStatus.swift new file mode 100644 index 00000000..886ee71e --- /dev/null +++ b/Sources/NextcloudKit/Models/NKUserStatus.swift @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-FileCopyrightText: 2023 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKUserStatus: NSObject { + public var clearAt: Date? + public var clearAtTime: String? + public var clearAtType: String? + public var icon: String? + public var id: String? + public var message: String? + public var predefined: Bool = false + public var status: String? + public var userId: String? +} diff --git a/Sources/NextcloudKit/NKModel.swift b/Sources/NextcloudKit/NKDataFileXML.swift similarity index 71% rename from Sources/NextcloudKit/NKModel.swift rename to Sources/NextcloudKit/NKDataFileXML.swift index dc843a04..2225fce5 100644 --- a/Sources/NextcloudKit/NKModel.swift +++ b/Sources/NextcloudKit/NKDataFileXML.swift @@ -4,310 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import Foundation - -#if os(iOS) -import MobileCoreServices -#endif - import SwiftyXMLParser -import SwiftyJSON - -// MARK: - - -public enum NKProperties: String, CaseIterable { - /// DAV - case displayname = "" - case getlastmodified = "" - case getetag = "" - case getcontenttype = "" - case resourcetype = "" - case quotaavailablebytes = "" - case quotausedbytes = "" - case getcontentlength = "" - /// owncloud.org - case permissions = "" - case id = "" - case fileid = "" - case size = "" - case favorite = "" - case sharetypes = "" - case ownerid = "" - case ownerdisplayname = "" - case commentsunread = "" - case checksums = "" - case downloadURL = "" - case datafingerprint = "" - /// nextcloud.org - case creationtime = "" - case uploadtime = "" - case isencrypted = "" - case haspreview = "" - case mounttype = "" - case richworkspace = "" - case note = "" - case lock = "" - case lockowner = "" - case lockownereditor = "" - case lockownerdisplayname = "" - case lockownertype = "" - case locktime = "" - case locktimeout = "" - case systemtags = "" - case filemetadatasize = "" - case filemetadatagps = "" - case metadataphotosexif = "" - case metadataphotosgps = "" - case metadataphotosoriginaldatetime = "" - case metadataphotoplace = "" - case metadataphotossize = "" - case metadatafileslivephoto = "" - case hidden = "" - /// open-collaboration-services.org - case sharepermissionscollaboration = "" - /// open-cloud-mesh.org - case sharepermissionscloudmesh = "" - - static func properties(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { - var properties = allCases.map { $0.rawValue }.joined() - if let createProperties { - properties = "" - properties = createProperties.map { $0.rawValue }.joined(separator: "") - } - for removeProperty in removeProperties { - properties = properties.replacingOccurrences(of: removeProperty.rawValue, with: "") - } - return properties - } - - static func trashProperties() -> String { - let properties: [String] = [displayname.rawValue, getcontenttype.rawValue, resourcetype.rawValue, id.rawValue, fileid.rawValue, size.rawValue, haspreview.rawValue, "", "", ""] - return properties.joined() - } -} - -public class NKActivity: NSObject { - public var app = "" - public var date = Date() - public var idActivity: Int = 0 - public var icon = "" - public var link = "" - public var message = "" - public var messageRich: Data? - public var objectId: Int = 0 - public var objectName = "" - public var objectType = "" - public var previews: Data? - public var subject = "" - public var subjectRich: Data? - public var type = "" - public var user = "" -} - -public class NKComments: NSObject { - public var actorDisplayName = "" - public var actorId = "" - public var actorType = "" - public var creationDateTime = Date() - public var isUnread: Bool = false - public var message = "" - public var messageId = "" - public var objectId = "" - public var objectType = "" - public var path = "" - public var verb = "" -} - -public class NKEditorDetailsCreators: NSObject { - public var editor = "" - public var ext = "" - public var identifier = "" - public var mimetype = "" - public var name = "" - public var templates: Int = 0 -} - -public class NKEditorDetailsEditors: NSObject { - public var mimetypes: [String] = [] - public var name = "" - public var optionalMimetypes: [String] = [] - public var secure: Int = 0 -} - -public class NKEditorTemplates: NSObject { - public var delete = "" - public var ext = "" - public var identifier = "" - public var name = "" - public var preview = "" - public var type = "" -} - -public class NKExternalSite: NSObject { - public var icon = "" - public var idExternalSite: Int = 0 - public var lang = "" - public var name = "" - public var order: Int = 0 - public var type = "" - public var url = "" -} - -public class NKFile: NSObject { - public var account = "" - public var classFile = "" - public var commentsUnread: Bool = false - public var contentType = "" - public var checksums = "" - public var creationDate: Date? - public var dataFingerprint = "" - public var date = Date() - public var directory: Bool = false - public var downloadURL = "" - public var e2eEncrypted: Bool = false - public var etag = "" - public var favorite: Bool = false - public var fileId = "" - public var fileName = "" - public var hasPreview: Bool = false - public var iconName = "" - public var mountType = "" - public var name = "" - public var note = "" - public var ocId = "" - public var ownerId = "" - public var ownerDisplayName = "" - public var lock = false - public var lockOwner = "" - public var lockOwnerEditor = "" - public var lockOwnerType = 0 - public var lockOwnerDisplayName = "" - public var lockTime: Date? - public var lockTimeOut: Date? - public var path = "" - public var permissions = "" - public var quotaUsedBytes: Int64 = 0 - public var quotaAvailableBytes: Int64 = 0 - public var resourceType = "" - public var richWorkspace: String? - public var sharePermissionsCollaborationServices: Int = 0 - public var sharePermissionsCloudMesh: [String] = [] - public var shareType: [Int] = [] - public var size: Int64 = 0 - public var serverUrl = "" - public var tags: [String] = [] - public var trashbinFileName = "" - public var trashbinOriginalLocation = "" - public var trashbinDeletionTime = Date() - public var uploadDate: Date? - public var urlBase = "" - public var user = "" - public var userId = "" - public var latitude: Double = 0 - public var longitude: Double = 0 - public var altitude: Double = 0 - public var height: Double = 0 - public var width: Double = 0 - public var hidden = false - /// If this is not empty, the media is a live photo. New media gets this straight from server, but old media needs to be detected as live photo (look isFlaggedAsLivePhotoByServer) - public var livePhotoFile = "" - /// Indicating if the file is sent as a live photo from the server, or if we should detect it as such and convert it client-side - public var isFlaggedAsLivePhotoByServer = false - /// - public var datePhotosOriginal: Date? - /// - public struct ChildElement { - let name: String - let text: String? - } - public var exifPhotos = [[String: String?]]() - public var placePhotos: String? -} - -public class NKFileProperty: NSObject { - public var classFile: String = "" - public var iconName: String = "" - public var name: String = "" - public var ext: String = "" -} - -public class NKRichdocumentsTemplate: NSObject { - public var delete = "" - public var ext = "" - public var name = "" - public var preview = "" - public var templateId: Int = 0 - public var type = "" -} - -public class NKSharee: NSObject { - public var circleInfo = "" - public var circleOwner = "" - public var label = "" - public var name = "" - public var shareType: Int = 0 - public var shareWith = "" - public var uuid = "" - public var userClearAt: Date? - public var userIcon = "" - public var userMessage = "" - public var userStatus = "" -} - -public class NKTrash: NSObject { - public var contentType = "" - public var date = Date() - public var directory: Bool = false - public var fileId = "" - public var fileName = "" - public var filePath = "" - public var hasPreview: Bool = false - public var iconName = "" - public var size: Int64 = 0 - public var classFile = "" - public var trashbinFileName = "" - public var trashbinOriginalLocation = "" - public var trashbinDeletionTime = Date() -} - -public class NKUserProfile: NSObject { - public var address = "" - public var backend = "" - public var backendCapabilitiesSetDisplayName: Bool = false - public var backendCapabilitiesSetPassword: Bool = false - public var displayName = "" - public var email = "" - public var enabled: Bool = false - public var groups: [String] = [] - public var language = "" - public var lastLogin: Int64 = 0 - public var locale = "" - public var organisation = "" - public var phone = "" - public var quota: Int64 = 0 - public var quotaFree: Int64 = 0 - public var quotaRelative: Double = 0 - public var quotaTotal: Int64 = 0 - public var quotaUsed: Int64 = 0 - public var storageLocation = "" - public var subadmin: [String] = [] - public var twitter = "" - public var userId = "" - public var website = "" -} - -public class NKUserStatus: NSObject { - public var clearAt: Date? - public var clearAtTime: String? - public var clearAtType: String? - public var icon: String? - public var id: String? - public var message: String? - public var predefined: Bool = false - public var status: String? - public var userId: String? -} - -// MARK: - Data File class NKDataFileXML: NSObject { let nkCommonInstance: NKCommon @@ -863,7 +560,7 @@ class NKDataFileXML: NSObject { } file.placePhotos = propstat["d:prop", "nc:metadata-photos-place"].text - + let results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) file.contentType = results.mimeType From 0853d79ba4ad72f6b671a4558dd054ee6c47536d Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 25 Nov 2024 14:59:37 +0100 Subject: [PATCH 080/135] Update README.md (#106) Signed-off-by: Milen Pivchev --- README.md | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9043ccef..54d2c047 100644 --- a/README.md +++ b/README.md @@ -45,32 +45,21 @@ Then, add `NextcloudKit.xcodeproj` to your project, select your app target and a Since most functions in NextcloudKit involve a server call, you can mock the Alamofire session request. For that we use [Mocker](https://github.com/WeTransfer/Mocker). ### Integration tests: -To run integration tests, we need a docker instance of a Nextcloud test server. -The CI does all this automatically, but to do it manually: -1. Run `docker run --rm -d -p 8080:80 ghcr.io/juliushaertl/nextcloud-dev-php80:latest` to spin up a docker container of the Nextcloud test server. -2. Log in on the test server and generate an app password for device. There are a couple test accounts, but `admin` as username and password works best. -3. Run `./generate-env-vars.sh`. This will generate an `.env-vars` file in the root directory. It contains env vars that the project will use for testing. -4. Provide proper values for the env vars inside the file. Here is an example: -``` -export TEST_SERVER_URL=http://localhost:8080 -export TEST_USER=nextcloud -export TEST_PASSWORD=FAeSR-6Jk7s-DzLny-CCQHL-f49BP -``` -5. Run `./generate-env-vars.sh` again to regenerate the env vars. If all the values are set correctly you will see a generated file called `EnvVars.generated.swift`. It contains the env vars as Swift fields that can be easily used in code: +To run integration tests, you need a docker instance of a Nextcloud test server. [This](https://github.com/szaimen/nextcloud-easy-test) is a good start. + +1. In `TestConstants.swift` you must specify your instance credentials. App Token is automatically generated. + ``` -/** -This is generated from the .env-vars file in the root directory. If there is an environment variable here that is needed and not filled, please look into this file. - */ - public struct EnvVars { - static let testUser = "nextcloud" - static let testPassword = "FAeSR-6Jk7s-DzLny-CCQHL-f49BP" - static let testServerUrl = "http://localhost:8080" +public class TestConstants { + static let timeoutLong: Double = 400 + static let server = "http://localhost:8080" + static let username = "admin" + static let password = "admin" + static let account = "\(username) \(server)" } ``` -Note that you always have to run `./generate-env-vars.sh` if you change the values inside `.env-vars`. - -6. You can now run the integration tests. They will use the env vars to connect to the test server to do the testing. +2. Run the integration tests. ## Contribution Guidelines & License From e6e0a54381812d371c563c4baf279886f31e686e Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Mon, 25 Nov 2024 15:56:32 +0100 Subject: [PATCH 081/135] Prettier Landing Page (#108) - Replaced logo with an self-created image based on the Apple symbol design for frameworks. - Removed the "V 2" in the top level heading because it contradicts the current major version 5. - Corrected alt text of image which appears to be copied and pasted from Nextcloud iOS app README. - Improved orthography on testing headings. - Added syntax definitions to some code fences. - Minor formatting improvements. --- NextcloudKit.png | Bin 151237 -> 115034 bytes NextcloudKit.pxd | Bin 0 -> 197677 bytes NextcloudKit.svg | 48 +++++++++++++++++++++++++++++++++++------------ README.md | 24 +++++++++++------------- REUSE.toml | 4 ++-- image.png | Bin 493199 -> 0 bytes 6 files changed, 49 insertions(+), 27 deletions(-) create mode 100644 NextcloudKit.pxd delete mode 100644 image.png diff --git a/NextcloudKit.png b/NextcloudKit.png index ce5b601e4fec4a762487fd8b896caa82faa08658..25eb6718721e3c420c0fba8c302c2ee95dcde235 100644 GIT binary patch literal 115034 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

bLPnzDi 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 Il173510JNBJpcdz literal 151237 zcmZU*dpwls|37}+Ztk1RG}4KtW@bfdOR3RNP3CU5Hd3*)t*8tYstH@l=rD6T6s3bw z5@NPh*wv;eigG(tIyl5s6q%$PY8i(K!;IfG?fd=tem;-iA2t5K+;iX8^}3!%$KBSi zrjMI84gf%Rc3SBUfIj?E9~kPv7p1gV4quEyoHp$PV0?w{A97*tcqY(WALhJr#RhTe zm;1Xe*L{sJg%ktAU6{lzDCw^>*++lgnW?6`Z zbtP`TVv^Nw9OI2A%`CzW9PkgDzl>Y7`P>Po<5$+LxiWjstE2K$_uQ_m?C$>Z{LAKp z2d@>i-?IOEQBiYn$uBk8f9_lVa!Ka;`MrK%?? zj%YU{Jx3EX)Af6b+C9$qcU2E7eIE2{<)cRqY&Gafu6d$7JbB5^n(vfb4{d$Bgt6|O z>pz!YQ_CEz(myNihdipO@9(!2r>D90SBIrDdmgknq%IF8!@h(c+n1P@(s1d>X)>&h zf7#*8+LqU0+P?0*)0fpXmz2dvQc)J&W6y)eLvcSke-H=iQN( zH%DAk)ipUr3|qVMqZ;tw_Woc7&C z?fAa--HI(k;XTGTu3O@RQ3?%b`hz*`9z{KP)bFFh?!&wKB$Jn^@WF)6emC1|>^I9? zk68x~IqJU)ugPvcyPWlk+dk*2@ABn^?D3BV$~(E-^LYDv<$cwhlV2JXT3Udo^q5@^ z`}n(HKWoSHY^0~Z-L5^A@o_mm_)_TUbE?|=c>h}QJH4LH_ud_iNhO&vs^^ONgVPOq z^7?rPs$+WKz54Br;}22G_IdgkV*|Ip+}-5(+COUK-Ljq)_&{ea%kk*hEy551_Aac( zSaD>q=~(+s=R54JWg%`v8TmpW)2;DfG- z3JY;`WIqjSxtTC_IG&p$4e8rz)_As5b6;`!w+G;zkGS3E!Qj!?<%7-r2FeGA)~8SY zHCu3Qpx%9~Ib%$JM|iEPI(=M%_HdW<+1>Tlf*@CoziSA?NipzO? zYL!L$K-_%O^YZk|-R)COI&{{gelD`UWgB=+8z9ae``$^-bnUx!6M1((Q@c7ddo1*< zEcIciPk6Z7dd+L$6p_H?z#LrMdi^s~ck-Iy0|ne{q)HMPxRN z(N&Qn&(CQC&sL^)%EuNe=jS`cUei|WOCE6bkoL_vJHVY{w)fcOUA+OC}G2j}y^kgJ`eTFus zbmYBl=f$;}O>~mVWo3@sO*pH*Sa&38V4}xZ{S$e*c#XBD^ibDIcj@g=L4WGU3~fNl zG1cLF>B}ZMkG<$PV*R3Jww2~wwqSbelcNXLZfwMH2U}63l&Uagki=vRG9! zOJl2YP0b5sXJ~(`6NGfNBo6eXt*Da@-dy=kuq^%UvM1Rk&piZ%ADjhiHTBM8N7mOZ zIIEsm)A{3Cjnz4AO^(Qi|0F7w( z2Yb+4Ripz0ctsxL5m(I%H&vl_xAkc(VXD7^ra=5KA{BQkK;|b$yba`%N>GvT6l|C` z`Ep9cT9?{Kpn^};RuRcaMR+@u#8z5x&Y723)w3iacX1^OTC(482k9NnD# z32~y41Xs2+C#F5$+@UfUdup>{E`!>I*`=GKR1kKHPt||rlP?1Jt0PPAC*zbkxb9B` zgy2;K%8)$;ls~XS^pg>Z3%nFdj7YY8m2Sx}uT+9SrY*tcyfUq*A|ervJ!*ymkFkNMYg$VdCuLw?koI`usngHUbLwsO zV;Sz!85rlVThfr1T7FPm0RFi)lE138wqCI%39l@#2stZk9`#63voDpnSZY?X6#3=y zu_?*w8*7x{muHwK>snmJPfJx7y)APwVHwB}w%!zjPZlejHC!5KLv|%-*Ra4Ark%%_ zvEn|3+6HTPu2*ZWX?t5&2my!T=A`~O)#$T)?6-KepxRZF-6^TJ==D0Uem$SDjz$za zJ!g_MlCJ#^mP8EM z!F+0Wj@;)Yd@khkORk!km@LHDZ8%$qe($aKb!IE>0K_EvBGUJ*Y2jBiw}FJKDS}?e zx-YowST6z*c;yhn&qHQ2IHg#I7*JRa+h6LJF>L9j=OG~IOt)P|RQ5|#+eUKVbe>|m z_*Y*vkWT_8sYcow$O#(o^S0a>p$02?t+ zxefBuLr8qh{tXQv-`sYO(cY+$^Yo5Zyg`%k*u}*}_mfn<7103d6N!$5G&_dv4%CoO zFqu>z8p>`$7$FteZ1N$NMf;q@=Dan{>AtPsE*b?tL=}zGUCWyW(3~ji%vPb%J#Qb3 zf+6eWPA;-~Gs|$Yaoq}u22?8MKgL!{z%(Ysu039UFO^Ls(UVl@Bp#<8IJju8YCv@Z z*xD8R2;1zE-q9y~j3ucN!vZiTLt7Q6-^d7wI@`T1TM$JLIj8oxP8G#PPiAu3@!B<- zQ={M)5xJ+!#}DQG(?G7tHQT~^-T>@6d#W+8;b; zZXiR^_=Ske5E3gQ-i#AD76xd*`(-Z}!bez04z;xWUMw3dM8XD9L;B}%9(h*n(Fk7I z5PBk#N)V#Ef{;c2=$9PIf=}(Lz?#xBwD41gcrHl04X-&5IqAFna2pE4 z)HBP~bqqIF(~Ml?9o%(1&S%`|CemO-+)$X2Yq=p0PvffknQJn>+XIt5WymPB zb`-1^lhqRP_i*5d5+bZY0#v0(%mkz_mkKsYtO6s~I42R$*v^*@&(j)3Y<#Nc1y@KKCo_{szgs#pgHS^oa&t?85t%2B2J*v`5IxlRsF8e<8lL?>ORt~g5$<^barz@qe+~^d2?TDKP`mX#?)C{;dR42K*9+EHz;kLZYSssSi5lUi`f4t9 zY^h)n>(oa*r2AOFGs1-rT5-?ESa+}c*pYv54(&69SfvcgS(6f^%~l|m=Opt~XUs@t z=tK1UdKBZdB2ykSfEaXbTBH6$-V%umX`kQ!xE)xqWWD+Y8Uo`yMD^RB{!v7umbL$J zfKNPJw<4_(RK)~L;Q6Cdsu{#~)X)ZrpqsQJe@;2GfonUPnfv!UQ{^aVZUiA$s>qEl zxo{~|+eDmT%g)h>*R;Gup^1RFXH~en(X_(&q^p4Z!Nsi;7v4k|RMGcL6YuDw4dmoc zxu+T36vM3?+c1oj#Wa|P4rM&XnTpp8lhd8%JjKCap3ItIl84w^_xyMhSsJx0(CD<% zrVQTGyH=hCHb^TTza5>1h0QAd>k%e*>`Nc$$VHM?IZUtpoh`iX!X-2r_)en=W>g4K zlZQn66WWBdPo*=Iz=7$;Um^i{iHgM=U1XJ5#wbWE0Ka5tQ$tIhW0OXK_1AOSzn`I- zEe{VNViDmR9`c*9n;cBXL`0{ZA3D7k+;0SNH7DX%S+pX3bjrLE%TH^27!_!+akJ&@ z?6IGftX?oGPfs0~o1rZ$05==RHyP>oolLG%$=PED%B@0FJ%V_~MO*Wzi)>=Ks^=3f zTwwZJ*9`@(Xx3Z#70thii00-->6TY?wscl!&toifon}=7S+3X;i+7}}?S~M{2J&bh z8t*QR%V1yAUgerK-9D#n>P(;qI%$3hLi}zbqZuXV=$u)aIuS@zTaSXRIaKMP_r)Di zbn)qOEPwpxMNuR15>PJpsbJW;s+~ok8qk_(@m_l85yRvNlten7pmz4?$3)pFd^S8Q z?9n*_ay-Mt7R1%k3~j+e2HfYPJk*R~k`h%kjKuQEATBk44(-AMX_RLb*xjgXpgj)< zXZ>Y3!;Np1p3MgD5xyFkloH|3k|iQpBCPhMUNF{2BgZk^_HFD150a7Z8AK4a(p#^D zPrPE-end{tK=I6SMPw`Zus9;`x4=CBpFmt%^_?p@L!o-#NI z>I5@V2N}#TB^uig%rT)Gi*wIl_T_GS&j^IU$>)cz(Li>#+tsc!y=An1UpTzeVN;H6 zWXX2*Dm(Ec9x!CUGfTd}^h$O~N0s1GM*2nd2O8LRmUB&;>>7#0CdRDn>VX@&*dX?f zlUv&8tx^SICLz1Cr3?YNw$634rJ4PhyjicpRr7>TvIXS&u5|NPE)w$36e&E;JW}Oj z=bz?E%!+5iTVhZtU?_Dg=%*+=DN`%YD4C=3VN;Cwn|d`^5LZBM>GF97R??}TDfIdJ zyVtl)W7t9YLmfMY1HI#s+zc#cM!3-S8`J zBp{t#-2BkuVZ`zxJt9#!c=;3FQH`BXr2oR=Z1#$k04`7ASYjpX1#O9(8jLRjyBvZna&wNf8v8ZA-`@%Zv}Mjn{gpg=u&JYw8!Ad$oJ4) zZbW7K(oh~yp-G#fk78kyd2qzKizqZAkt{`{&wQsFSubn|K8skWviC)u3P9DAlV7R? zGa7;U+}v4AMb)^q>opN6pz6#(-*B#EuC+m3EX&{{BR z8yO^|Y%98aYB2dcMvm*rzGqR3nYnYGV<(f;Q{gW6ib$!4+~| z_Cr9i&)BU%ULpyJ5dl@(O{AiJf7gQN*q3HKeKXREK`hy#MzX4?zqW|UA4%Noo_TFA zN_j_BQa2BB$vem~UBX_~`(tLl?cYJpRN~@>kMEb*V)Rq$QXX)i7IrU=lDw}MkwK;xp?gdec(WN#K zt}I!22u=gl=sXQ*TQ4j?9Kbp4MiwDF8}F=HB_;zKI|}*Rry5OT#NX13%NV(o0cZB4 z#Fz=Az=1)zsE;P%59KZKI57$y2wP_cl!eVxrQ4W(7)J7QsCqfO7p;YCKh&q0!AVwM z4ANeLQ==u1O`XRbFfmz|eWClJ$C$4MWXpX{;qfuY@7j;qDFK6J{}E}(7Q`TmOzn1z zEJrKs$80rVGN1gpO8_9*pr6z3>?+|%fJ4r&&7)BZK?X)94|r#4^`b_;chdCJi0@S` zJHRzQ*_^<76=y#da!hrVE#v20Gy{tmz?|eWC|AwxQIJu3{~S`8seMe;^&GLxO}A>NX^9z5w4S-24LpldcOIw$QwgF=Ii zroqv0M?~G5%A-oyoas$-y&~VNh9@MmqTMD!z~cyV65co8?x>&9c1?D+k$h9LfA2q~ zg%V&<=`xbBx{Tqr9bHrOhg=$PHut`PQzSiJ8e0JNn1q7` z67tw8i?O5;#Ih0mB^UIe+ovACZTvp_e78%cHh)!QA6yIB@{;(0A{83nNItgi5r!)u zNC@>2o#kwb*z_87ZDM!(dP|k-P2_mq>Moz92$S;^ zjkd00*t%$56qgjRA=S(pLOexey}aZsU7U%CC;0*s78Sv!qH%>CiWA)U0Ps8+G2fRf zb|H3Si}sKkxjKZogG=~=lPMA7C14f&-`+LOdX@$%nopcG69i;}yre5HwORuFx}?b|>Nt8gBs%BRgM*M* zAMKxa$g`aboVsd0A=Sq-wYv44ynccJ#Nb&+re|nr6GI$T?GPmPgVs*R1RO$4ly9A+ ze$D7U1J~5SJ~3p)!7du}@`FQ=GJQW$+578PThfR@-b6;8G00rRQb0|%myk+$=8K=6 zq{q%H+s6Z%ipnSJR$LR2$!sFWO8EiL3N*4~Ut8swN{7dGHI_P~j8S`;Zn6uDwJFUz znkY>=q(o zaSq4YL?{zV7S9fz&|Qtms>td9{_t0b4Qv~AVur{w^CpsVm`AqIOkz}P1f&_05E<=F zAIN9`Z~bKh&aNR_?S!bIgq*`99Cvs{{#m-^Ap z()ARp&7p=fN|rI*wi!%$k1Q$xJD?8ZG4?}UO}?H>E|M>Afzx9rPYAV`^XhE{;Bt2* zCjNk2qf;CaHDgY05`3z;$W?=Fn+4SHa2Yv*RAx)})ulUY>f=_FLFKkCX-Au#%DN9cEdVdv z$2_{qmp);TSG9&rPQP8I_InZN%n3l7GPQO*pu?rzSJ}eg*=MGae~@(B0jO_ue=i~| zR2R3S(}xfzXTcm*%?NVqo1_8-vn`97YkI*1rW@Sc`Fa8P;N$f42Os$4xPO(LgDGtC zuyO5p)Ff>KCM#nIU(q)I{1GvU3)#&L*!O;Q7DGie8(rV zRu%rPTW;<&P-I?Pv^cgBK;fjb^oY+0qF#)uY)UQc(5oI1cp$6k=rPszEV3NfEk-|Y z6$1&^mRb0E2|8$21xlb*?U&of7a@&55*cW`S#--ZLu=!~yHn z%SMnc4=KOXIzy|E5gmC~{FC4kU?cEA-LxGv^@CM&m0eI8TC2&^X{LeJOFDx`b1G-6 ztZhLZA3&{QLH5|f_yO0N%WV&m)b^PpZF0eV{&siiaqEX5KAhapb;eTD9E_NLMU#}^ z@Z69Hq=Q0f9eAyI6L+KHM~*5zV6;u{a}IIJ&`uYS1G$dLY}u}mV6#{KGyr+a)sg=F=HdSr6(4J=!VGK48Qxj%1R`9T{77zO=_oH^eXGaf0@sfZW@4<1;RHdk7_T(86#l#e`>UnFRlB z18{dS89|&xZtKrzAoEvwHZp2QkDs+* z)|N7I7Zug->zkAX(j{OUPw{;^z1IBI>H*Yb#a4~$!SQ(1vcIeM?C}5^o$1P>98}h6Ge;182!#&wWdBUzSTHFk&8aCyCnk$uo?(+o>VhLd(N>yz zop#{1-`MS8z^WPUQavW+I{3R$QlZoHU}WzjO#c~Lja6ojeEj%3RHnmt@Y!(=8pr?8 z1=UblHhjl;D1jg9TJQsvY%((pc_NU$oVmbmEiQlN}Zp<~4vnbyBgs51jO!aZUS41Rjq&*{rwy z6@a?xbe^m?^suqpXT{%7Flb$^gLfbo>9rxOFyfJpe9Uh5OOAS&5)rizqu^?LH!0&K z%}EI!RT1_qC_Fp7M-Gl4yL3YVtbgmzsW+AD*b4a@H`nOhFJf|u&Rw~82c8?`FfvU{ zeoB_-cojJOlcUHTrO`V_};+w~e_XXy#t>;;3^3RSKW zL#WdOg+*!Pap&w3f7E0c`6OME=no(Oc=&Ne8eV80?n4L&$e>OkPBy0VFpi)wT|xV7 z-H=^!1xkf@$9c7j8lFw$6jVXtfnU){;bf)(w3u{cEN%$-iVe50*?=eD&Tn}YY-KGLM-txj1e#BhodoD8uBUT8>uN2 zXh#QTxT;rlG0tg~y_eJ;;p7ZVQMb%@tnE8@Eu}egq`Z2?xe5XtNq3;{XNVekAau&7E1!9+l3|V-TNf#dCJHdYlnbb%w z^Rijdzi!167RSMUa4|E!7tLrSFUwm_(`Q3cW3%Fz4pnk)AwSt5*UdS1^b6UD4{gIC1$ z2GDxkN~0GS{X3hi!o>ZkFuNokj`YR`#XQqy_px~l;ZyA7S#>}Iklbsjz~+$Z;6C=FHGFb#YCps>6131v*4X7C zoaPt__(Lub6x!21Ul9R8=W9rZgds6}V(9b;Vk#ot@Azh7@0+*u4d8Q zL>QC~4*b`|9r7oV@Y#z|g?txQ%j3MIDWG}EXMucu`W`aMUv?bHYD0e7g~1P3S4AD$ z!-bH_ghh(o%|W41=CZ2YpMZ$YrmUQYZWln0B*gy6fn8YZs#Prv%A4QaQ{d0>1BQI+ z<9DOLoJBP-6b0snc-RCMamxyB_dnCSqpQGEkdYEGkD(kG*piOB&3KhbBP|%VPTY9r ztKV3hdfHGw8oLsqK>kdmUttqJ+Z|gB^aWt-6MJu6 zjTfsgee&XYFsUFY2chjq?a>unRS9@!%qOG*@Y`ACBlL@ZQk(MpB7f}6{ znC$%&*+l4u=r7X&69?VkDGS)lk|}T^he`_vy`fSyiwoW#nx$IM^c4*Umok07K@2{% zZYhJ<0g{8XzkR?BTYeUh6IrrQEUIYxJ=Y~%K6TfKK{%;TxsQ!1)(>%3N8@IX8MQIE zlcnZAVtLYxS0B%5XR97pUtZC463=2cH4h=r-N$m+!W&5CWhg#8vMHUc+*(iDa0`L= z8==tYu~VR|f%L`NVR$>AI7Sb7VJef5jU)A)c@)`)3TOZ6Tg97hlrdyuKa4`yf!6deD z$6$F{olcx2#Rs;bD@Vb1#V&qm>JW0P{NTY5$Y-{Uf}->R+EZ+^>m7YVX6+lA;Z39} zeIQn6b5L#)awds8l=Y)SP<=e6!UUAuBIgTkGOg*C1|XGypImAOl#S#b<%8yz4B)Ld zkig2Yir-DMh*RDy``2hSS@2(@HCV^Tid=6YwuJ^x&=HQI$2+Nyi@+0x0s@~k>e5>g zYUISTs5p5`1EaQ>Kl>JPRqm5Shj;$tsX|JVnZlXDlYN@P8Os(d%5iDY+prx?+t-ad z(YoO;`s_{4_q8Ny{Gmc_W{(CkIgink9eC^%#&ZVLHbTAhuCRrqKL`L#eb8kk7cz*V zIh8xPCXRoZ@^wt*#(!*9&Ig^V2{lNID(dSk^VpP#S^GlnSlL2(BOJmJ$VkO&&m-_# zGFH6%9flQL!-)TJRymDkZh{X+&r6%bL5YOy6@yfr?-IL*Y*oPd3B{Kah^+`4vWY10 zCjvu$6=80=QI-4hF@xHPy&|}|yemsQubVqy&~5#ZhgRmE?*G?l%?-q$OMdDzZsKWK z_YAdHUF=6!^2w=5NlEJV>y$;Ogvk)5s{2R5?~{{~2bT4s1Md-s&Icr0Xvq@(0+~C7 zP72cg9CLh5wg4i}jl#Js!UjUmtJaqWe#FS9=-LEHgl;+bC{f$%6hts9yA~wq@V?Oo6 z2ohVa=Ds+ThGZ#_+zcNFm3=!xPA;rKp<+6h1=z11>fCh_6_6WYYUthMQIIMxc>);( z3Kytn7keRi-*^@;tn5YGVpN*L|tJ z`OMk_yK8^(5s~q$JY)Vv1BkH>)ONQK20?~*WKp8ciehSL0G*w$e?bCx z%-maph^tz(1D#(4V;3_IQP}2ee-wgc!Ue{IGVwvEJKQmhyMr6Q$CZ_s8CB%L5pn zhpC+Yn4u`QNM*jt77@K|L&$co$P?ZqOhm|`?}jy8)!ud%q|ghW2B4UeW?3$B^keDK z$qu_f2upT>Uie)eKWm<8MMlX?rh=n7BPI`{w%)AGw-Dx=IDIP>%$8K3N)PGYI-i^T z+H;aViwJ0y`Jr#lNglQTG~WGN&__qtPF7-YmqB;VKm(3rvaU~Tc;wUG+2kSX_~dM6 z$o|6uLco*l?n?JVPh>(`BG$R?@;szsA5B0sfmG-8DZ1E~OMq?RZW!8-w?G~93F_3O zd$%EHsTGr0tG?Puez75n0^llMt}2Ng|G6~$1F)5Hv*wGHGV6mmk6 zPbK6eQYmcWfw@o#D$V;r>C%Pcb!jVtrSM9|mG5ZVHIUrOjv=1Vs!qi#@90ZX5IMtS zG1~ks7*Jp?R3nL1)Gc$E1oPb&!eGh7Pq|=S6%i~T{kU*t3TaTZhi=M@0Gk2kh~7Z#@erPXpPi!vE{0LaGc&jEPQUg*?SF96&~L#VQM8#abGP zY`KVYMiBo0^G_k|p%;J&?qhRMx6PIv67nF65S=ZwIQKsm>VU7GUsw7d8f&fj=A#O& zpOswe!adDc5HgY3((gK@{tG&L?(*|9w7OCEvUTCH$!717NgA;0n|Jy!!>5|TN!I}i zj8o*4v&GQ$YG``AI&mB-pY)Ft$zy!Ld-mFxS^XlS$b0{XuR3-jeHGGV~}s`fKX7F7L_s*W2* z{8+*R=RBmBRy(SG{fa^ccrry=7L%mTOVIKJRN_F^P7m?mFfs08v4Vs9fv1qWK7 z?o;F)b$CY=a>6K|$VY16B(xx|&Y&L^$@-tWX%CyomdMEs)HxY_aTV*lN zuFxe9iPvS07E8byl*rdh8HOSs2oCQ^P(QngWO$SQ#y{~odtlDuyn5<6t*C3k#qNuZ zAeax9s2Dla8WE6&cjP){;e08*uTq6Ig z5}Ox2c?ial1H^!@y4Z#Yp^NcS@WL5}RKm`&2`Gj9u8aD`@W}~G;cv$A=FIp(bbBN4 zW`VJlt2wHv+t8&D1uQO{cuWOzLC?RLrp?|YG_{~HN(!g$cWFjw!t6*vvK? z!h#R=#CW*ls?ZINMl9WJzilqQZOjNURDz&nq}6;ex&b4fqhzA&eV)ZC7T5vc#<4y~MKY&=V5y9Sk&lztj%hz#!e~3;p zBVX-^!l&=<1?B>3&EAlLkI4L@V9bpre4zo+{+#LcaWtaDJIb?){Pd#|9SX)KedE$D z+}ae}HV8A>r~Ez;GhxAjmeqXARJ%5B}c87 zU)sGqn@|4ECPt^ohE|w^GF?*BI~;7RbKPPYn_XhSFtK6IxES-rVny-rseCpj3&n=H8?=5KRQ!)U@{$HM(dh3mQ5zAduC}=anZBa z&U=b(KPTDRb%qO0>Hq}0X_V_S_{YcZ)~kEvB}X5MruBEx&$1+I(Ai0wNI? zUql?Loz+&*1n`h9LaFyi+KG3UB*2GHykW$DM3izL$4={O+Dv(jY7)cE;fA$`6z=fA zu9|7DvS}b2kC8FAqzb#*^*sE!2Bw)kq!`P*Cto4nY5Yb4R~V0`7k0D4>x_Egsa_tg)9>obY%|?`R&Ief_Q#SVm2q#qY`LFgVYf^z4YP;Qhw&TppRK{+DJ9z(jgk zUv15}CWz>d!ljnTYPBKuiol0`Gn+p~zyu~tXhMH=WS!;$jGn^rGLMm;;X~35*HIi5 z%x8(;BW0yqo*-@~P=Ar@VKVX?OZdhV`r`K%qpcG1(<52n`0l4@yav?! z!<@^>SG0IDGMWu@VOhI$2{3bA`aQ#(JQ|3VEnHr%^DHRx@3*wg>BS3DkY0cCjcIOj zME!g*m>?jmS>z`?qzn!HiUux5pYo|sZCmb|H<$)v4Z5Vd@N38@IDF|M3~#CtY&W)a z7`axZ!&=FFf{rTJK+ygX(T81wx--yr6hP1;OIJ@7`X#9|M`56WIC!?uzsPCBRqfG? zmg{U;_}LBxo@8TcDIf;rV>IysuZUkYL&)YnVEhqxbAoAcB1}L|Whu-Q$eQ+`y}^&r zNxHQl+h(v4A}F|xOGSV1Svabb!A3XH8H zaCf1np#VCOoC?&$J2Jr7rgT<1J!{GS2GT~A$axRdlWjawIN^>`M>W=RW%Z`H8Zaj= zcN&w}Yzqg;Q)>by8q}dM$_dlv`M32iJ(9(-sckNV5Z!tkZptaX);bThFmzS}`N|*W zYNk$mb!6mQ_R~;c#*Bw)?*OzGns(f@!-t5kXy`U{ni%xuuHT08o{7l2Y;Z^4D{5IQ z?v$_pRkGOG&hpd=H?nQ;_n*FF4rlRK7Y*81T}(wIo#E=Ej2Y?tTf9n z!oXh5J%f&X22YmDhf(NyOgNiI#o`>eHS&<_>64wWY9B|+$2^m9#Z_&Uvviiqb%AME z>7iU!+7O*OjK~Q!cALzUEy}53!>zEi^YuN0tYu7FSIu!g*=IxK z!-StY7lKsi!Onsnsx2P2pa9&yiD)$d9xA<1#8kX$YwrGey=FH&Q&8w#Rlu0oxYTK_ zFH)q_M!`K16)PbRch&f!A`fXQ-thN~2lNK;LtXHBb@_LWt%w-1VtQ;21wj`py!E3U zn%fgj7FsE6f!?!6lYe1>n}!8E^w(2UXfJdIDo4&xl$dk4;5w{_gnYSbp0xwG!wQO( zS&4Zd59ewi=0MKlg9-_~gAR2dCj2fb{ceGkF zas!tL0BfQ~B7W0=@6&tN;)5{GY*uJ)_H=sFC0*gfOlvUL0~ zXH&F>hgu3RcHx16n8*w&%wv{mYFBg8!gH;&rXL2*W*v*uH=fm9jRp=wc~6?G%k6&? z{?g|&E^C15-@cVp53PNU&Yox5B_Tsf2@PB<>~9s5(oyivV{9G@!oeHo^=hVI?xHoC zCp2<8gSa>R*e_g|p)D=5U8g<-MXzkBZ#bOMUr-iU_KXH=7|PGM*#*4gJs!_$;-+^@ zUwUZ}jrWi~`ERb*2AUT0)X4ge^+&b{$i*;q3C#G^yyo;04|AB+sQHS<(z_u|I((xY zSqsSfzo#at(|0hw$yRwwSVHR;gU`_lbm>{#4Q9*xf%jH1Sv=XH6#a5YK!`DCK?F|p zYlAi_DimAGQ8x{Yu`1`I%8IKqOgU}HEeU`a0m8!zDG}457h=a#+&aVqdoYL-#z7QA zuRXkZ1c8eFzw+gOJk;+SKy`hEE)xMwKA2Bx1aR|bEi62mub=rH6Sf9~w~#D0r~mlb zA9=D8HkD)50Ug#gdyHxg{(#P<5Ur~sr!f`JET#))Gvn_V4)&w9jtftC>y3jZXkibu zPbA=H9;tF>XfswHP~f)P(7*c{buAqrfP=tRlhGbxTbMVmY!gp5aJO140ttBFHZ;^* zPvv^hkEhsp#VX_@bjtpJerjk)=8<aYEkmhC#u$Om+!n@m+%YM;5e^YpWMR z2(dG90GgB^GRD8Zb$A&Ge47R%_w7(SEQ0}j8_Yg?{b0RO*4w_T%QT!vAtz8WkG zrhDYl6%eqbe-ui<6efY`+9O@uLg*W2%cU3bq+7@V##~kVzpKTFY0{$Cb_Rz~PSZ$w zv1D_;six3*OM?xQtFEDuTtUw6Tgba0WY>(Q#f?x&^o;i>${4nxT(@T+Hznes9S#rt z5B^jnZW4sKVZ>>jV+d6Y=&`0k`&@sU9eODdf`w>Z&4Tpg#`}W};%{XluB|;D*AJ>} zB5wF|wtB()xq(X+F3$2VHBlg5W>A|@Trp2Nqxg6fBL@MqtAE0 zp-peKM|VN*@>|u;JJOVqI|eiF;o!aRatJo7D9BgNYrp}WYZ|8nyGh(O7|X{74F*t! za(*2xwm|=MO2n(uExG0mTqMTYNnPX}VI(1U*0nrEWzJ(%c1b?8O#@b4&z4?6;$2{! zf<4dl53h*cjF#hU!uKqkk@jYtk_0w|=n`J7Rf4}JvcNpX5iwcTC%mZ6G$$3G(tBZ! zbXDYD)G1LfG6akNmwO5W z+>EuFDtr*y5XtHqE+ou?Js{r<;MvkVwqjZ}2pvT2Xu1h-zw~k`YS%nE(eXgkvKU>A z|6o73nzxg<0UcopdAS(oDj@harcqxm*MQmm&^Z6r9{SfsCEt5UJ2Rv!G@x`@+CY?? z4Hb{PXP}sGjr{P9qu#ZO7wYob*bemXKMVB7P4QjF<3NR*4|^f9PCO7sDyLi6n63AY zoCQO^x_ZcJo6__2*l@6pC!Bd?{nV4yQ+#a%;0(R+kC&!)Et6J$LUskw|MWxs;zGiB zQ1^Dyl{ctrMtqPFHRrWyh3f^KTxxb(zmEojLwc$(&t6@S6GjTZS^pih?n{YqhcYgo z)WC`g>G4hj^kJUh#4V)4pR>3yuC(Z-h|unvac~=F*!fJgB29&6ct<#aZZDknDO+5EKVsr25#Jh85*I=Uc?=E;lsUsz_;na%1bVExAnO5__NJb_M z!xtL_CaY6g^i~D|{w+izhPng!Bw*FC?2=hb@|~XC_bkj{!kGRAdfab#;mA*Ajy)CX>T0gto|}g*tz8 zm98B!o2Sb$gE-3CtP8h zp|n;Al%>m|SFwkY1+}u)j(NLB8M)F5AG`l!#G9b@8i&Pf!D?0@+sa zBJ4Tq^pb`xI>U9!6?4cq2WC4#QV3ROk6G=i6L?$hGKT=dX_lruQ`?x4zOTQgbht{e zYICtjcg z8V#seRmNoGCUm%llM)G8*C#t{Yu%JGusXY>m`yt5q`O|zo{K7iNL-1Ax@s2e4MOhy*LGb1jTRds zu&!m9YU%)5Tn{(aTbRSY!ls^EY<^)H9}X&YdE)0@k#phW=!L=Rz&%Fst54GlH~OKQ z^YtNiP(ZT_3^#3J+6ss;9`uy#ZXz_8@NFc#BI}3!IlGL4?h4U0Fz2Px0UY5otm(bf zZ{-K1!Yi`u?wm(h{3z(XwPKf%U3!*Pl6Ul$JY?#KXY>bLrQ<86?N7Vgmrv0tt88Vr z4u;f46~(eS(P7Raa-x7Z;OWZf_Tb4f*+Ms1YP)6C(XMN>4n=P;3(dCn`cVj+0pX6hKNduWHx6yb+@5GtwbqvVRMRTeF@I1d* zsa!KhY&(0NVB#W!*oQDHIwlcO7Yqb`+2!QtZdbr{J zzd5ewBv0fe;UNLzKgZ))erVADR7x7<(tl0Yz&fAH>Sq}vcPBb)0-Q@Ov7srs8g_Y? zwdnMeLT8P+h=q){^2itidN9bPxK!?EY>AM zWsBgXpv=K5_xD%Km2Eg|w7Hq<(&&D=H2S=d;&X+q9=ko;>D@(q@+^$eaN}8Ura)#+ z49<0&AfSrPlepj)2{2&E0=V(Pv_*WbsNV3If zz{6wvy<8O6%}qWc8-nO>1|`sM6&`A&!M_BsJXKeCQ;MOnlskZis?gZ0+UtBWl?{7H z#Z*OwTo7z9#d*+53_^COVN-zNFbB<>~+6-~5`3Q%_wDM%-t-s*f5`>S`quU0JBD z6n2jJ%QwAErUAX`fB`Fy<06jhv;^bBHzSo4Qj@1H(Xn$;Vnrs2fZc!_(_G3YE2| zB8KU!OP7_Q1WzG;$53Qh!5YBqTtqJpziBSaMfMb6TT~A*h;-WCM8Qe1$^}L$@&?BV z$fZnD*CKgCvuyx=E>InCvWfVG8*)!hYa0d28$igc6vUXPIB-iB#O#uQLXpfEdu3{O z3#s>q@Sqm99d7Hx@=Kjf>6T}SoUOv9VPqq#N>Z2Li{^MmuH8n!25uwyEXZ3=k4bt* zuWBU!8cihj|7QlA&}BoXlra=%>2B>hcJYP*bEK1CT2HML6JVC4F;O@v zPW1+5OUoEkfu4OH5?GhcZhEL_UZkskzA+3C_;*CfD~NBM=o-asn` z8<_t&ESUXa*LCFgs5g0Sqi`QjhP3^1S2t|3u4+Aagrh1Le(J%2&pI9Lkq9(06tBmn z>gw0z6igP0SBC2S0ULy3a2|n*g3fQfv;#~zhMVj(();GNCM|$j@Q7$OJPhRDx~xzy zm087Bm9qYy_7C!H25qlK0Utq3!hbSg)M7KtpUl(dqH5;Iz)WC^Dz%g7Qc`&fpV`95Fjyx-^c z{(Wx0e^1>yW#)Qa&)4(uc-$Wf!%&_P^#bv+Hp0&`5zHbdrqOPD@1$Lhi2oZq*UK&j zjRXKlo||+$b5*&Hg%KPbqj%8W(RScfY96^HUz)OXC(WqV5(Srv3($&D!FHLWg5Q;j zU!xJDw6%GNF;Rd0ggYl-DN2L9D-|ss(@VhOxs_h@nYgnTjGalmHc~cGu#`uADU}TB zu;ZGTg$I*ud>oYP5=Ds3g|Ks@JI)r2J|p_0RMOE9cc9s2g(80dO?OnEAabYc?H={i zadM^S3gy&M+D>@U`N}kq(3O*dG*oU4T2d~TlDf}@Ro5SMpIxeDFq1P{4>u4(Lj(@7 z<=fZubgFD&Ij(OEj06uY(J{+gyeltBuQMvbQT?}NNe|eL=DExC51(@sYw@J^RTAU1 z0&fv@4Q?f1JKxEZuB~Dg&4-C1TXyFKTGNhpRJE3Y{~GOc_?hL!-Ro9PozAD4ZZF&= zLMai0YYJp85TD)?~pER@J{E~SlEonX6|0y%%0aiU9z1$=VeyTScwkvVzNX2FdC zq=(;yD0ZhQtS}C+{k^KU`Y^eTm;;v(a^YDwY^LyMFlC8NnBY%o{qDgs}0krSLWY$3yc)~E>!ui#zy z2wv5ffy46Ym4kv+j+~m!+aCQCr2t75R)BZv zfOwF^J2Y(76!`_a9@w*58Bt}(XDjJZHeK%td`%d}l^P?=nf4yUyk925GFq*(jdXJ8 z__PpmOM~t{EsnrS4tcv1rh|vYZya#ryPNC5BN(|>){wwUKUerLR82mtnF<$7Rr^(hU72sAA6+4W1SDuENl8Fsd+gc2<+Dv`hAAVNh z@@DuKP?HZ;v5qlhGtoc<057;2QGv@gT9Mmmr~~DT@I~da;f}3nS9Bl=@2dQKO2t@Y zON;ljq-LUEH>#N~+i*0X7hhLky$El5n&7YK!13{h$&Y8}I^*92E79SYeK9F2=no%i zZxmrFLiBv;3>jU}iL_|(>B3vw)dEyu1-p>=ES!ctu$u-?P73VHErEZq6nmd+9%>Gc z3QRZ2wLq0ofvh_|s9Hq*>BjE`{*GQg>K2d?Qi*#=J z?xZ>8XMZwVI46F7EpgI-f_;Rek!*GHBC6i?XsH_c+%+Ml=e40@7Bg&=W)Kde=#8lm zWeq+&HF@J>=2=l87tNNxGz4ckPmfD2YK{{XmAXXXB(xRg&2uV#mett#kvcpmQVGX&qM~O zV(8akdgE6*Xv%Aj9n=S4gw~GOE~`G`NrGx8HgC$-h@z;u|+fT>AP zg|m&9D`FD9lN5>rCNkSLx=gjWN%TtX{x;xSvUDHd7qdRAZRlMSu;M%~r$*o};>HxP zLn#rK_U8t}+RiM{-cPp;?c~U!`Xh|9$yMd3!!Vg>jH*dB$+bQ&lom6MzhW2$eDWmA zYyKAUBz(o<3?}1!DF4&D_|Dd>&_;S$z$j41Qi!W?MB#fzuZV)@|Kdsl27!v8hIAAkJE+YAkGfC6 zi@DT81Qk7SWSNJrV4B|S zwE`24;4xR%QGC1b6llSJ3ejFyuPDW^HuTk*x|BlH!QHiWv+->loSw6td{U{A5T*!8 zAsPqe*WKZmx$|9n{ow?q593HnnPD>sEVn^Sq*oRzAiFd1TK1I?Vm?LBX#i@#3haVc zKxYwaaZu*4+>?xSYu`eUjDfBMTB}o_V^GFxGkzGu_^RqjLL6+&?Qvd&->n}Hp{5Fr zZG$MD0Dx??kuU&m5h1t-9r4xzmd62}_q%@LhjcVD zc0(h4P^}x~(_KS=dH3mM4Fl=`#701SJ&o@OUa3)7{)c1CEgv<|ZDx@RAjiCY>(;T+ zgbSD2VF_NMUG}-8u6@_Fc#nMWn;OUcvl%@BOG!%r$E=l6o424?NR9Zj!eZmZwCFLFjMvstJey^%Pl}O2tJCC2IzKuiDR33A{94#31g-- zS>t%<-BR@`i}ZD!=*+*|d*4$;)TAcUtEx3LfwVLZzcMIGykwFwzibB0c&)5d7W?R$ z<8vkmM<)kSJElsHWmb!pvr4hW`=ReCybUG)6vv@cr9kQH5RT<0Cyob36BcZab#0=rBT5a5f2D0`d`eypj)MK}H(lN0P2JbG_CRw0DDZ5k^EuR8y^RMy>gkqI>-LnRf|U#H2asckSJR4Q z0))K62;*u4U0>>559My)*y^^kTqVTB zs|$34S7TrY`0*51`kN6c%QS=(5zHK&V~nIOmVB7!8w777;v4aEv@f4Ou)+{Qa%ljK z+HQ#6=OgQTt+U9`j_5#@2%RtVxfaw9HvjFG7T4ycTQXP8PWRhMyOubb2{{m8>Il}m z&nEqX{Jy!*t#BtG^V;q3iE#P|bU-*RX3Zj(5oFP8^1vP%Y`?)f7wHzS_&H{t5eU6S zLK-Z9Gb>`~U~Vv;u9q-)DaroyBPw#lr{e;Bq!G1M%Nl$X3zW#=4_onW@c6+FP<_A` z-uDy(Wi!40Jli$OQ5@El8bKcoyd4`aNA2|wfy*v1ke+!0{pn%H#LR=H=K`l~Ef zM)X>#*O(|rCW&LQ*RcdIAB&zb1?Lp(|J7cBsnpc}^@sYJi%z~F>&}XP1s)56H`+rs zf12C@B$4^e^xO{gPzTMB^YV<|_31!t9(?8nbF4L({Pb&p$b>@*QX@Ep6h;Srn(lJt zQV6W-oDpe2eyo$Z109Mr-Mq*LmFYFMp`>ov>R`s4mP-sD6TIZ4+Wtx*E>xxxXSn=_ zGYkNc^*}R{jdp97mQ$A~2I z^ljh@-9m!Vvk8zed+-!f6o*yTjm?J$eC!iv>RdU*f%r9Btz&bA4UM11Jzne-CQp0Od>lF(0O11e%Y{$Hi%_}Ym|?jphU+DwBuBbPD7;6T(!~YzK3dTqDqq| z1vo@KQIsH-mAZUj7RggDzboavMY%tM?3L2z8v(ut$Xl>r#D^)Y%=Na>WVG>>o*m>I zD%iEz9W!KWluzi@6779;9iA$xj+-gu8Y>6&T_7vQCULOPKbT>E1FN4*U2B{zZmt z7O4^Pa^!ptRifurhLmoy$y+Red@*G0@XKuem;F}&G9Ar^eFl)WV4eq4!7Ez)Zh&q9 zfaYl7jd`r(aO=cjzkZH@kYU(PX3LHTn$J_aU>Ob~-W&Xk&BXsh{tnvL0l2(?zvo2U z3Ho~5;VpaOzN{J@1XD0f1y6vBbdOkPh5;G812aLta3?PX6hCW&*W1DfhRANI^VUa1jeaE2=a#g zxSfnNBU<~7X9{3al6)m;@UTkM4V-ygf2hXfiHS>Hm8-&Q%&lQ<%z`b))N z1)GD7fRNmA)f&#;(tLN`t-`{^*|Jz$#i>EV7G6`G$AP74A%F1?FChP){D@_zL?+}< zl&`bv34t&xAiUvAvqi`kYE5iMzhdr=1La-WN5K1ToQPUd9G9uJLl^d+U<*Zuhpk$` zf^pa>n)sb&TtYkfUT0Q6;gc`@6dlBf3Iv3u{F-rwn@o0zKwN&3Axa!X^TFFC9s@8S z9AY2Tqe;ph8A@SIAVNEFo+J$x-Nt!?zN*jC`2`66&=J0r6Ad{XwarJ5oq%+K*WdZl zO?hC^SknN$!irpMgUMAh8} zj2TAL^~eo@S7^u`!5_VgOTEhHEt@caqPSGUSpfouqtF*?$D@K7-kD^kDb-Ql*|lB8Z* zV8$Pn`t0$>V)j*)3BKTP-!838EbE6|(}3qoJ(%~>{fUh}GTmY8LmIdcR$Azttdq`dajzEMWa2U<)H zdE$ZQANw6oGJsb!1svm_7xhB&{7nQugpppB&rCjJP!We(4~by3iRZ!u;#F$k1Ll-8 z%_rEjfj)yxV+c$3oEZkeX0wIi4Bdf^$18Neg6c?ncDT$yw;aOGs0`)1K_X4s07+H! zkqLTPs4x71+svM(j?G}R0D%3JoCiA&5%p-P53Fe{fU^fLyXUajsDag}Ea2lX7KCtG z29OCSzF9w>sS9gQA^)2hztT}{IyXt2Z^nnX;OOb3Q&Uuv$v4fl6b+4FyAOgwFc9| z?`|8}c#rUadpbuD(XtqiN;Y6o0Y)?!P%P0cH(C8}(Zp@7AfN7?v=zQo@x6sY^49mw zC(vE(?f6}!&un^VF3y-+-SmZVW7VtwuTVNAW}-Sb{zW?Q&Fh?xympI+fhrwywgCM) zl8XuuxWt=-{47D6N0GBg5cbms-OGzc1!vnCDRXS^)EPQDS;e|Gw8vI03J&KgjH)0>42#|gj%wqk>VUNEXqTh7@?|t07Tp%Rh zk|6n$E;T^*q_K=1Hm1VXmKOnUuq1UcELABuGz!kJ1@#NY>40!$0FRm*IekI%1q|U9 zRE|z4((|&O@u>rWGrED^_JVc-u9DxfXIq0xqJ6&%oufy7Cx-JGOx+aX{&v#uXgT^+ zo$<6x8n=9q?V8`P9}sr zI5&Z63ryeDT>iD64f4lK%f{Ql!014s9sGhOUZq=j&@L&6|EQ~Auu#Cy$jnHZV5r-G zU+18RIEc`NeAGq?!9MW3kz0I)-n9H|P)xQA-)vyS2WSuk*=K_y^J!$y}CMTi-!{BxcKq(h0I5p(x z!s0j^EOqeE#XbwcSb?- zkcY9xQsM8^W?xpdwB`BLoY{6rj>+Ps^LtyBU!8OJYg198I3&N0J1zUc_ zsq*rrTfQywfjJfq@fduGB1pj(Y`}nUn0JUffZ_+x_)eq|SI>v@;|_piI*`L&M{!K{ z0!TRx0t?~4Bm%wxnnCL5@Qr&7;o9$IDH9L3g78o=P{NbDEU*j!63i_?96W`{>&-sA z3!9r=>H)L`c4Y0i>)0 z)kL;{5mQEJ9+OXIO1F{M+>>TNruGfpyGZXG?n;pT%`_JLbZs^bZl~uMxm{!jwmTLa z^&VM9JxuI48~ax|N~QnuE<^~H6*w5Dx;EeKkI#dH;^0})1bJL4PPhNoEDnz90*`z1 zId@Xi+Ug9pFF`^B?P^xv7{)=HjK2DyeWVPAC0A|zpU;SVIWT}H4D!VjR;&M>%qlv3 zW)u^$Xn$Uwaf<*<#^;7|-c-4%sq$8RU}x}8zTwhAn&)8F6_T)#aTgOL8Eh)uW|gCO z5h-1vTzW<|nj@QID(6+1VJB$@WEpz$Dm2z))7E5X0Q_*d*8RbJiPw5qhB{95`iD2} zh$R$YZ(vkwE|-CimeYL%>^OSh7ha`em|g?}kS!IvSlM~D;+6>z3r?t>BrjyP9CZzV zofo)P6XNFF%IE;}zWOLTK4WC>r^+V3nm5lfXhK+G`xH(m1$iXA(;vVfj8WeMv%jmb zLq+JIQG5~(o6O0V1_I+B-eYj5$-^A213uJlS3Fic#`rBbGyvYW#FH^P$#9NF6frfV z%?obkZn8^MoE!G2oLjVewDyAj=HNyaWclR}qa-ibGIQCjT zg5b$q)&Me!4g~&V2OdgpV*Uc$ZI2O=aqrLOqkX$q?ZyeIS9~k+1%faKuJRd+VxmG~*%RWaG)e(ADYC+!OJGt>6kaEvZ6)7xXQ*AsHPta(qLgMRjtkue178VKn8DL_zT5-x)utE!mGANvDC z4&voD;P`Fs_6&#pDMZ%VH_X>z$l#Q(#&p0F-oN1qJ!(* z+flC>A3i1XB=H9()rKBoRwVDL^{qr7@cuD<(O4clEYlBkGIPiQDj;jG(2u`DVKq7CwlpdGmz9byO5_b3cugiGnvLz1fBn#7d-HitB5Ecxxg)km z(wj@)T%lB`m0@&BDD}4j0C`0(VIY*YPhfd9y*)Dcn*U0-5Rh#V{F4koXwCf(r~GW^ z?q8jqG-6ms;qfapTX+OhJnJk4WYO$rw@lKttA67cw*$pTr9=#~g#cv#90A)$;VnI1 zbdZO>{XBjt9-e^*Ita&ra+C(svjiA@o|mItTa!!B3SQG>1u@OKX)>dQK4k#?D&&j2 zT03nWSS(-`9Kx z2_vs{9^ghN-eLzzMNOLK`9W6C7LKv{(dp%O$yedk^MHO^gyMj@eb&gsFKia!a9jGA z*{W}xjz<=dvcNjyU+hpztkDmi>d)ba&`*>hfmVSz(=9|ZLQ!%{8YSc>TcjszoOJ|q z4XlHWr|VdkBW)vo2QY1fR2(cw@)mo8wllhqh`K;u3Sz334UBS7a8aRgE?FZw1#6>j z_{H2d7&rkGrVo!QbDvvZ8V4I*SfUB3J{eTNG_-=lpnwJ@<`}#-iwDn%7E^YXaUHHQ z|Kv=rt`@ILL+6jl!8&g>7@Ewc3E|HNiYshlS)6zoeU3{5PH3HG8=H6PZ6n=ur2Uh1 zr1uTWz$ID8rM6i@V-na7bMh$&iokf14|`>PsZK9Y9C3${!9#4fm^6#LwVnJ-T({Et zHXr{oq+{OaOLG=+$SfZOGXt9mz|da?y9lhKoiz?w7D*9EAfI&5BaG<-(&wY>%wzQH z?&!aQE0hv|LjfoE+y8X9?Hcd_PLAw*hPiNWs#tR@tGD4R}nMX$M*4nHEc)sG+x$TjTXfK5c_J>?dM_u3=wO1d5KYvPo}(xGopV(GgBTxa z1dB7zUn=0_ta;T=uAhy1lkM+mSal$Po3@s3N1HD_IScP~qqlCPj@eT^7zO}}ySp8X z7m;RxfaQc6%sPlJnJuh$%)^}>vCw!_^3F(;i?)#s!*Y%&2%l5g3~LsDxAqE;q4&0b zkCVjbV9Uk`B@n~WrN@}<2lXZef!?l|yz@`$)n}O}7&Z2W`_gm%-3Y(L(RQgLETtk_VlXb zIY#>1OcN=-V0{-&Y7)!wUr*;NJ(BSW9>N(u-e!seiNJj-N6YQP22MghN_;DQJ>e^U zH?-n7-NIIBQbslE$44@TL>#xioU-M6tkL&JB0krgIVI%z$o4_`)gVFTF@V-Ldp6Q@ zD>ML+F>qDoP+0By*gPLr#z8=a1lLw7vG+Ss8Uy?s-~rj0O)X|l$}g`vQlr0Jy7hjLix>T91E~&p`gUWUcHdE#y?`Q za>;Qx4bH0Ig7u?u@B#1-Yh0>C-+_LH%rH553&!F__(A6H(Y~I~Kh81sZuu^{W&<|L z4PM^Uq5}hACEFlNH-y~qyiREM$w&l8F}<-wXAFTsWRl3@@q~!*bysqAX+@pY~i0oWc5I*3w{*&Z*bx(j73c`q8v0P zwZUrg2&uc@QA{KGbwIW%JH?Kl4F*35t%(QCILf6*q7{yqvB*3lde;Hj*`xZ}jyyh4 z3q_P~bLey(sH!n+Yx%(BZ=CS>s%p@YHs-b~${9?|D*^q_q%Ktf4q;ZfF ze_u#B-UKiw+}pZ$B_ zN~aJYopsqV0VDfx2&{fb3aP=`4j5o+6+phcZV0@n$((q_{+P!R{Je2*Jehj>HBEEx zuqp1Xk}`;hLpH28AA~LNhEb;$GFU}d40k*n!VZF$tvzf4#seM5R(u3{Wcnc)0KU!1 ztX0p*Cucr?&!Nixv|#b=l%1FfiXPy1K7>dF&3AoIq~x zT*=c{*2-k}2W9yaY<{D@u|BJ4EY7w*cO?OX-Vq5Z{oIvGOD_1?y#B1E7qKx$V2PvH z6|yW^ESa?&t}%e37DMWf1Ky=C^xSKz{n$bR?mx?Chk?-%!$<~r`AG&mM_>$%D}^xb z5FtMtFY@gN(A!Ue7lZ1f0-cOt{+niF84m-c2kp*;Q95w8`0xNGE_j2V={sB~I07MM z0vu@((yMI9kX7h?CUVUJ3*F)m{K`c@*IYoa8Wo6h*RZI582)ul@5%srDsWr$$zk}VhpEyeM9<wmEhc6p zS>6f-QnWob-asmB(6#3PiC(UBicg|~xW|>=ewER(f9-pyE>VPkKM>$%=qUDz@Ih0^ zFNf-^R=Z90iY0Ov5G=$B>Vb+tDPUqRAX!XTb+9rD;xG_F>#cZhbVd&knda=>?zRf(-UN0i?7BAb5&4 zSpac6mTm06_V04)RM*@+H2-@<#sujUi3uzp4yS20EqjX>valmt;G`iXw?R3eWl;g` z)>oY`3(2-|ziRmaTRnyNj_5m3LmYD>*YIj>#l@EHB+Nn`w-;`Mg5Hg$FM%&3)P`>gtk3l~)z2Nu;*Tkp z59&gdQ>_inh$>W6M$M74i(PrJtz4ivx@Zy|r4s!#CFonLaCF zvApsRh4as<*5Ihv_4ADDid#0@!(~@h8sPORr%J3>W5ZvV0tk?B48Z&aA75R9af+U;z%T>@SweLf17dcOlAr{M z6${@>ae(!jDytj~GctzPh7m*#9ZLdcaHP}aiI8!!1dt?0s;N`PdTsaY@Xst%5-<&y za9(jwTr1FH3U+DPGg^*9&csR$;J{f|A&KY*GCIKZ!6Z5xu1AIB@deAIr{8MyTa@fFE ziRpE;ncn!4l;tCXqJzHZ`Isl**#KGKCdlU2 zR;#PvKi+nz>@(->?dSEE<-{p+Z{JnQ6zhQZU)ID*`eWf(x##8yZwB>S5nh`Dl?BWn zP!2Z%w48A%e`NxvSPTLaeDODbPrvd5>5Wl}Wg@E2<@8lUDEoA=SL`nwYJ3-?oh5(21?}<~#17lLA-C2)64)K6 z&;47MoTqxKa8b5@R96-XMQZcvfNt+rvEua?iTLyF_f?1cnIA;acO^<^mZvJu?rxmo zTYq&=c}8`6Q@*Dv<)gZ$^UFC`uyLSkDnJf`6QgGF|A5b-X%{*%Vr;MndzD_ib8}(u zGtgj}b(TcCsB7ezZZh2@L$42db+OAi6ik1cIbjj9uKR`{Iordq0DD1JKQ@GgOtD5uk=*E-;YSzx>Af~yhnaGv<?X+H#Zyd!3_B28>OX2U@JpSMJW^8U2ABa+f7UCWQ zLyq9*C;NR+_qZ8ZY(6jdpS%I~IV^k7|0wH2&VdYRaX`i(!?3*6#jMgC zOx>kwVM+3I*f&c61h4`Ou~4d}=>t1=3hEYwTJJ_zd@Q3jJO&!uLkLxzrNO%QdKU>( z(+enM;Zl%|Ya{2>62S3z49nf9K%jW_!B%FT8LYd&yOyO|Y$fS;w{q!K($}>d9(b~> zx#wzrTI@i+pSaV}TUE4SCNeG`OzuWGY0&VtCMqB0jqx81Zfr7s=ZJ2%;W;=TV~nd| zwj?!H=zw$}yS;Gm;)d=Tf?5g^&_y@db||NerF((XWdO1)oM)pOUb8B>UQXj0WDvJ%$I>!@g=NhAS8CqLtEn z+>H3=2`Zn8n45kZU+VNofE}y>pv3M4n|gudO8qug_hQiY(P%W2N>GJTr7k^5DH`jp z0U}p?PVv%K;)yY%A?_?vce=qd=z{tuARQi)fm0|&eb=;^X5W?P!IZ+~C4Ut4@% zRKZ)Z7i=+!8B6eXB$Jld$2wGm=&O>Fo+TFnZXL_t|`%5nFc=WBX`fzHG?Aw=~oOLSMVwc0;K8p9Ho~FmoL@TY- zM?*I?#|h9=Z=g&arB4F4lt(yJWvMHeUJ+-16l6=`7ER;BPzMK`t=L-5gBJI#_8uxI z>GNaoTPnmNYDYW#rr-#^?629h3&p0;#!#vU3qKzZ?Xu7lY#D+<>mG4O3BG+<>%j9E zwyJn$`!bQ_L3=}wx}%cU_tUB&>p^|}cM*1)W>n~Ic-IfwYmN8(Q~=7$9HDe>+nuL0 zuh@y$n3|hHfQ$Tcj$nS{P#}ye{L_db*0sS^<%0;sG)?$L9L~6uOz4TIDc>dSH_%S? zJN|W81wG0Rcuj+e?hvnO#x8Bu+rmAn=KO%3^Wxgpfl=&%&umG|C#-gLfJcuaQ%1>% z({tp|8l^wgQ zJMOl0-P2%S%z7VN4i<=z=RM5%&*>qXTBWUjB`?Sj zGtIQhQ4un>l|)063sgWZPm;`F0okq1QSJ6$ZCkknG7AmFa-r05499qn7KdGt3IbD1 zBep?CB&n}U9I`qT03lg<4f6$z+fKl6BOsrbC@X9skqmUh&eTg_LPjrxFK@_yTd#p`WI#fq^aua~H*opeE#ob3 z4Z9e0%1PsiAtDcl;^FUKm_+E89NUhX+|VBKqAPIEF>L3GyUt0R!%O$3&C{DQKYcSBvSO zY>#sd>oQ%fo=aM6nlQZ_*2Kke%au?z1Pb;{Tto*y?>86dIas}pZ$s4sDb8$h--D{u zc56jIo!#All2Bn}C%SY1vNASp$X>9;tywZJv(Npp4G`Ri<}Kz;wU80Nc9IsOg8hr* z?BOzw%;j+tF@ZCvR3Zhcb^H#k21^)OVOl>y@0kP+7^^{r9Y*%bPZtwahCF`e$TDa+ z(mFI2I09f(n$(FPCywuGV8vM7%)XSkJV^~RU%RCx)z!&9|L`ns6|&6(objgen2 zfK|tc3K#?-`BIP^)?-q=#JiM0zbBt}4oefARXk`K9&+h%9u2Uk&<4|xO5EV`6{(lESV9uF$p7fH zr)EMaZ{SUgdDVs~nTJ)P0A+q_Z`ZYZ81Et2lxZSxI_j57$d=&QfnUNbg19aqj{Z}Q zeR8-N0c|fA#dj9ZO)2z2thO`#5zqt|ntOX;T{pG$r)~f((tF74IR(4$I|xAVus{oD zcy0Mb~Z(Ui}(awO8of zng~iZ56G>vbXr#UtP5o9@T=oBy>%rmLcBg%#!h1yS0kXZ&%(-9b@LzQ}qjYv0~oAJ~^)^g#nZCRSaKLyF_zImEXNhDg#NVf8PThT)aR<)$*jR7E!KIMBrZ=%b2hk1~T+{ z=^YzOpxe*c4NWl%A7Z<{C5``=A*DktZB`FS)_^F(8y^H*T8`Y)nFD6Xw)(NzXoRDB zmRAKJ1vg9pbEMv&DRB2<_sB@UP7pf$3ky9f13JG|k>5IAsxtTWOckpwohOPY*2x3x zjK0KN^5kbavIRrlss}#MSg?Wx)KVERlDRb)Yb?(py@SSn=wgWQk zMNFvUTMez4P-M1R{RGx3cRR@3HiYZX_poyjceH}Q&M6~VNAF-8C-A-gU7-E`X%Dac zX+B%k5mdGRYsdR8zlLGVEXC8qW3gzpGD&@y%>&pn=W8Sg@;HH;9% zv>$3D9`l*iv<^Uu&VkB);-MZSIgA83u*&Vv?gzI&_{IFKU|qCBM16>ezdZxg0}y(; zTKtN%=K!TjiJdH<@1!La?=EwEo0Q)?>D~xIu~t z5Sg?m&)vxbxQ0+VgH=?2Hnvz-Qld9YLCh+r3jSM|{au_M7uWSXsQ=YW7JtFyQ`tOK z-|Z^LFCC_Rer@kQRx9^AFFTSQbI`G9TtC6L^Ez6scc6pRDZnJQML`+kSYZq7;=WwJ zuj%~BjL4V)!$=t8L=>^vE-XXqIutx^C09)Wd~G5)d5^nm9UF96xspSFG{ERiANeW) zHjyB1uKQYsiou(vH6Ff68;#!R2d|yl##2A2pREq*>mL;-c`wkqBf$HzegD*D-&Z?R zkY1fBl%CXtLgcyx_H^8pJ&;U9KLxW*lDI}c-jv#<3};|S!XH_ZpClvP`sI}&^Q z-(J`}9-<%46o_L`fem)vDRS?F!*u_l9l?_lMel)^N)|m70$T{ipO?x6 zRb8|g+HuG7sMi>h37-46_ulTsqV@zMnM8eYex_-0{oQA+cU8UL9{wL4+S36_f;+Uu zlQ4iC;|NsS&WH*{&=H4O&btgT1MR9cn1+Uq`A62K-XLkT_(`JbSw8`UkwL=yr)Y5` z5ao?PaN=oVD3NjBfb6bEkI|9~5)X-_{Z;-(?CE9==KTbiRkJ!tJ)H!%(pRJUQgIU< zL@%IQ{y~SY-bz?#0O#;WVYYca-6xIc{fkRN2nTwXo^+ky;6i9f%}9egkW&a%Xsz1GN&YH{lzd43Nw#z-Y+B>@J78pB5GM2+)Cai z&X4+UaQI&Z*^?e>eiKoSl7PECZI1>X^#;6mf7pu`2ZESty&=58Idnm#PA`DtX?x>@ z{GS0T(6}4^I2%ignXFa)kL>R(xc-?~ZWfqr#{UJPI$%N}1>Z3B;DT_Jb|0ie$*`+i zA7O2{?gIV%(ocW-407Ao%7PX)x8^_<0Q0lh+qd#_hm|^@r*MxUz7<1p%zAprbDJb} z!MK_uj7ig&%3D&Iemr9w6G&J-+sSFG)tQ~^8}#GNd3tas5`sbMxDWz|u&@)4NG77J z)FaBNVndylccBiPcyRL8<9QlFelVn+6K%6D~T6Qk4z@meZ@gl(bYi#eD~=Be5Xc}`g14?bOCU2wvIz>$Y~chjoKsK7Hz z=alFq_)vWwKtYa-4BBl-2`%}bhy&3CJo;>GS{*}^Lhs5RZE(5~MLKnc5V)1qnwur6 z>&=z@S%p>KUXxHB)wl}^uXjKj0vC*tc7J?!lO0;Xgg%gMJgNtGQMN@96-;WHQ3@`4 zTa}HL5)I=JWo?v(rG1IU3O9_g1dEq( zlTE60l377(c~bq0L|eFG*)ZB`^u|-=JHt~0z{)nj%5NQk)s1#1wI z8#qy!P#RY=Xl8xui}F9${q3;X$zGkuR`vVwHq{Xu`v~wWy}~f%WtMUtS+xlooI;kQ zm5Oz7jRus-l~xY~b)wuyTKd7mlhpo6k(C;2M&r)wY4}0i{2>naI)Udz{5cqMxBmND zCnT!mgN2x3Flea%+l+mHCoQkuuv(JGmiJuXiD10~iw_4-(o?b*UA@)6pGK0n71jRpHxChq1@sn98?QILpT`tWtU`(Y~*HRid@uEEIPQ5Qrg zA66ALz^37$AVy<*@NZqv{xq4T+X)OmVBYU4NAEuUwuPMFi;gmk+tIrFN>r_Wbh~PN zeTP+9YqV(F_-xGBd-AV7>@qEu*P$U0d1PD%WNzU97Gm$@P^Eg13|wKCd?xBcG!5@l zOO5;m9ri*RFgPHSS6WWhjtOaNpAMD7G#F~rcWBQ%wQv-VdKn=A!R3Lb`dMK`(}ES# z#fw#0HP6yMKwS}s${$1%>WVtjX!T}KfxvU$a7XHp&W~g%w$uWHBE>Z(hbHVOhw@|o zn0Hf^=zP%pD>G!t1nk&}sa{&FRef-se|!(Ek5zImY!GV)HAwg4Vls zi|Uf{S_2whO9C32*>lyf`E37S^~CF2p?|L;P94q?H*K|2cOAbg{`SW@bz9R-U@(nz z<;2VNjbGK*I9u<%*<fbL%DL)5wQvqKS@*(-b6TIKht z(_5n6@2q|idgpG;<0%2d!d}9mWux@Ap?IC)(N-;v>KW6;dF%oBL9x{D`VKB9R~6Fc z*elruJ!0kAXkOdRp3~}gK_QZ!4=-Ir%FmkvYOOpn`#QSUwLDRA+T&{KGhF(bZhCT+ zf6&#vqgn&<5;^L!VL=bym2at z=^e}-IDcEB3YOoU>wPeyYj-=K63SqL`1|u6$}Q=Z)xXL;aTnHdA;9Gr4@+VA4vKye!Cmsy7-2n?mz0hSJi1B zWRGLZfv!L!#4Vi`dvpq5iGJ2iu=JCqkv8`6@qf3}g&ebvl**T0i8u5D`@9wD;@AFn zt0L)CiA#F_e3s`FW$5mjo=0Ci{blg!bedXK^$f^su%oZ-t-1LhE_rcpS}⪼5X8? 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 z zl%_ZK(Z-2){q1dFng@$#ndHbw)`*X{hhzG<>Osvqt2@pX-k)O9n!zUbZq=2Hb^Yta zTu9q@{P-^ib{Vh%Rlvc{Wfl~dA>8c z$D1YZI-FM(V~SP;EiN4(c2GnsyMOnF za6M?q3JLs(WY%Jibv3_%T+H*0YZCh>3e+b{d4#35bmV@QJswF0-wuf4xj@WeKQ$yt zw+2UA-y4y0(;$Aq#!jut`Zc~Qv))A(9w4>`)1 z1vguwkEjYIg8D*YFgQWtUcGuDlpeZelkXaG#7JhJ#au9CXhE1ysHu50kz(A}F;rap z>bS{CzQ-83CtPCk^HvL$--ShzGctrzva?A*Y@Ocd*@R*so&_n4tm#UvwdOK9~$iEK|&zonxAuwj(OKQQub5|ysp=G1`5(>*==uPKifrt%Hy5!#G z9Iub{UnrXbG!mIW}>e$s9d3g z9HXErtkFwfg52vNjw4h`&>~Q`WrEBXu;?0{vsNQ73aR~N~ALgG=KSRm7?MTrp z>1@HAcWiryowfsIJ-eSaS8e=~kA%!*#SBD}Pro=}hWX!#j_8*!#DoMkBU=PUqEfD! zQzhRTEx`47IVV3jl2aN>>L)+{W|r%W`yB$HH%~1!Jg0@WF^S1aJ@#b-g*#PQylY#; zrZxQ_GI?Ze2#Ug2mTi^*ilV|&$~Mj|i(()Or&@WR1Jpc>R+G)H_qYq=xVq#%p<;({ z-XEnI=?jMPd&Q;bt<{8xZnsg@k6QvUZ1Z#JD^sHwM5#u*v(#0>Lsk8pQdIUaa0@eP z`lzOE=0%ws-lxewJcXL*QJ2bo$UYY3ROm>R{N)Dx?Xf@`e%MeW%10P2`{FYpK30Ns zzFs~MPekuQuDyByc&1C8`FCSTlGArAnj@5nS|N$EkIbg;v0&8qTa244M)N~+uM;xp zSL`9g#ys_liOwKX?efjDDA^1{tF>-HQch4n)2n$Z5p`>w(B|ftWFqHLfbW=>oSnlN z0HR7!HWQ~vDRd~)hmKC!5`z7p*+`la308e!H_5}t0PU?~6sFtP`f2>#qaRQ)r%n9H zi(yG6UxCJ;ws8ebIroA`+*9jNDv6MRR)v1nLL z!(rI5iaDrnO9buNzCexSv`H9}YiIOlyz|v0*|plomIb zZ0PzPpzEd2&`Z7mY3p>W?_)TQ($ClDTAv~K6XMd>C9KC!7edw}RHoOih7irRd;o>) zsvU^7zi$jZ{qs`vV<2j5Su0SqwtWz;)ZaJM1a=8a=GvDV^m`V+GHmw^6=-2=8L`N4 zS+rHCC8pqQvuKBYB;xxQp9!$;1i7VtqZjSTU>ok;e({<00i(D0o*4|uuD@o;x2=Uh zTgX9(SuZ~GeTo#5q5dhP5H%i`_Qhx9CcvucbJo8@Dg>B3;8I?E=9BgVy=b#Pgze1| z3xBUZ(Dwv`iSxy0AV_^A3@8)SL;Cg0OA%trzlv^;c44CL8#9aU3bnkACT$9xYeB+A zf_)5KwB}>MF!KWrXiHPUhwFa{*~JZHhEXrq4?S$J)qw74cN4uo0J8_=35DF23)mfs z=Rraqk~*)BTfpFCgG1F6h{^z%9=Rv17GNwGX`QRMGxaQ9NQTQL`^K&rfY%(~yz0>v zWE7sy60eju?0cL1RJ-I_SO|T_i&?l*~?!;B6s@ z)bUd+bd;^2sPP}aXHz1<2=BESaDkE#AgZcwMs0X;c!3Ry-u*fOl`u#R#_vLJ7hrX) zx*nv-K2o?Sm488zaN&Yw9j7n2G9H0K{G?nUPrx50ErhbA{a`o3K{%f*Qtr+x$R*u>dWVz*-0*P5jJ~JTE`UH^@OB zlgJl<#-6%m`-KX>Lz`xwD!o&`>4bcNA)4i3iG>V5!Ynt}YJ-Ry^H-^R{D^8W%e_#f zkXQmKt$)|>{PqB`W7axrxit@W&M}%+$h5!~GAeK5W6-S*x=*d8c&zf=Hnc;_C7}@7 zG7N9s1OHKbGIq9egQ}wfK_%c}%tLzgJvKB-HIm#ky=M*s>l(RAt!}GlaspmlgvFE@bl}lO*E$Hvypewf=_e1rx!{&F`?d|^OA9M}XgnX$g{y*RK*3(EE(7pv)>ww*9c--CFI4~(*!K-2z{-?-_VC(Ho z>~N>ujyv=exD2cW&bxqq`Cl23;>%{Qs+bV@K>C+$c%g*Z@Zu!N$%@d2DP=*BP@h|Z z1j>1b1{wfdAs2b~6UoNvJV&sai=Iz?ryQ4%cO-F~Ay0s3%W3&}mYs*-3^4fYWLl-P z!~3=l9`;@&?HLdh65Mfabq{z3-UzL)JavD3e%UoM808W01OV%=ngrzuJWF0HcN6|; z*n4?S%^5r|J)K;0JY<2Ns>bu44jx&q3l2#izynXtry{Dv*t9p@O3a4-1GqV{N~C1x!_0z48AK6 zC4CSQ6^OdAI?rx)>hWQJ;AO|l}{D-mS^4bBK=^SEA2DXkHN%uZ@MExSvm`P zueu*6e1a3fJiE}h&gb3dD|w`IB6eL>B!QzNz$cam+{Zfo%7P%+`#KZSi{LLLi{9C) zh<7(P!4D$qor~SQ;H~Gr3EbV_U6K3X)?4H!OK{)Q#^X&F4;buR&>jqa`rYl^4UP#T zo@?odR><>ls(1E9;j>0exbsL^it``x_|g(}fOC_)=kAO=%XC6ExxGwStCyJFl-CLAjBtA52lr_bU(T{2rfCuVkBYa3|XUy;#9l_C{F zXTi<-Sq^LWA9noPv5kRRikL?8G?0;H#EdI;&Hw-HY%APQ@^#*I0Zc`!1%wqz!+P-s zSZc9lY5xxF?-TCZb8#!vZ9BSwj^!fk^SJ4Z63ol6>WN5B1o%^JJ% z14VoNGiiUi`)qZ3EKKE{klPk5S3cg#ZB2Yv?kS@GugU2AV?^jU|M*ezkJLdTpKNGY zHu!L+d5$9YIM_YFNXwTK%R^BLXScUG4k5;8v6ptiUr*4E2mU(lkMUKiY7=@ zlFvPcpo=rclg#m5TN-6rj1^5hn`_=64Q4MB6GCRQ%Ktmh@RO)~XxvDr9{iY$XEe&9 zEkBF0J9{n*9Z`Q|6KW4xbSj*_zkdT1XXfhE4l#ghXuSW&FVvr=!dCQ4s*NZ4KUM6Z z#LAx03DDn}BHF5*_y(uvPLzu21hU%!TKx|TY6|?dUFvNuVGlM8e*c$K8Bu=)>)xSC z)O0~;ihIQERBk>QLdDAROs#P|Gohd{^L2F z8{A;7A}T^>i=b&fxm}nzm`!JD@ZAB1@zs1AANTbwm)tiQ@LQQ@n`X@(xtaV%s8qv4 z?Y((~6q*c_jx#CzEG(=K{MTNg_~VB{b<=4pNe&->dqUz#)VA^C(xR z?EITK<-AlD|4B-8I}7>=3s@QsvO!49r6^z z>L=NJSiti$ScGhX|4Ne*!w;WBs^CzzcD*p;jG)>$l|9Dj%Hfk-oLMh?+B_K5Gng83 zRljug0j*KMC+G9s-sdPzKbQ6UHJ-rsh1>FU$HkR}noxgsU;9SW z6gsha?YceqCI+HRjP_U|RXbGcAAFESX$eJ0HMVM{G1k2D^6u?`qjl0>s9QR$AEmpI zBMbY|&iweF?o5$CIq2IZFL5gthh18R-+k5_gX!x~4zw8XT{X$O^%Ie$O?L_W5(y*H z2{q`Baz(z>mn8YTNjdguZ>}nQ@A3-PqLe2=laY-edxe4T+$qqx1sMAax9x*Dx$e_) zVc4&OrQjHSa>i;{48Oy>9Gswr&%7TyX(#>kI)etQ%fTVf{>V>SmA<3a zn&^(zxE$HVw5AMvOCjDUI5|uf9wncSBWljR`$?zm0c}weh2Go8q+pu4dLHKbD|P(D z(|HHrS&I-7=?o@6RAhgU+mG}k5S4k~jPmrpiE#-RtR{nSxbOq@L#ffNWDV(Jbi|l# zNu>ZyKNqKvVO=d}hFPCXkn;5wmID*y)?t8~$Ait)Qp|6ATvoZaf_pyjtmhNn70{_)HT?uaJV zjVmu&Ii|gkvvHW5v<~It^-x{yL_6`?{H}cotZ4OA%1~M6G6(~K5hPoAt{WYbUujHq zhB<7M=`HW0xBO8D-M>6z6g(Duk_h^sa254*r7_yUA2u8u6H@%n_MwG{nJ`5p=}wzl zG@!}{twA~jV0f~!nElDeJrPK;Iz0g0Y~$G}Us)e&l_|L8okm9F;^5cagmQ1NLQS;l z(vCvX zX-aggnDkgBrM8}>San0t|%zWV;&L%17DV>LQkIVt(_V zkuqsSCw<@TS3P9R-b|Owb9};Slu)n3Or#drw98|buQ)$V>7<~!@G-mM@rWsrJ40kT z7<+bxRUE>D%_*EO;AeCYl2#%8MeY1IHV<6Tv(?OO25AcOyM{s1EjS(UV&w7abUhbh zR2TWWmPNwhwt~%&9I>rNqjvz6?Oe}ndgpLl@Pmt^D815}2V2)JJFjO#3DiRB1OLgV zIH|cZ2Jo|2$)qTd(N^uL0awORG?dY$;AL5KJib)z(WL!if}NeVRgKPHC~%4HVkB?- z>C!+&|2(>c@#zw8BD4`YczSn?+_NIbF{-a5_fg#}yB;x8M88>jpub9Vl=axdwPvM( zyM_t#xegI)DL^8o5J!N#GM^NC=Un4<0urm{u{6D)XR%Wzmft{DsYwxH0lfL+c?wzkC{~ubfdI_Kh&Fak%%`G z%;6lF!)Rne@@Bm;Ks=j;32R?jEb9Hykcq|ZT|>3kYpzcWZH!R+I`o?Q7VX9;}?RqOR4L@|~n9xs$8@UVPcd}Q5DPV4tY*~!h>>3XC0;dwCWVy6roLHk>Vfosmv zgG`YpPTIp{8pI*1X+;qgQn&tPJw2!YROW`74ifVv;Fx(6eA~E1j+V7#wpt1)FSlHA z$>xP_Dk^8(!^3$Wd7C#a28G5%-r&zEb`t4X%9mOr%G3C9GK!JBHTcqni%f~e7yc$h zUC1?t!OMW(pbm5PYp+9*x;Uoo&e+=98kBuqKK>ISEHn{arOTP!c@o`&PfG<4sdvAX zL<@Ko)Siapr29gWU58!*g!BMA#aaG=#yy*nZ+;o}biD6I4v!x7a*uzYrw!=)8oEvcznG9BWBf(O&er zJU=qNE$FQHehTtd+u=w{e#FHJhASphjwMqeeL-W`f1B?1pFF}AR+XuuK2BxvCDJaV z!`XqMTYq@FMka$O-*z8wG5BoVWleD)LpU!+8@8n6>!=5BJXW;V-E7&s$!*ACA zn=DC?%}So}O+ETAs7Y@F-1#}yk1#ktp*&b^0#OF6Q>O;eG$U&c^UV>jFP26d7*z&5 z{lYeE|IMNPT)Tb6ov?aoCOY(dG9pU#U4rrF*rVpgiYH+Qioa5ee`9o%Wd0A&`n#SR zax0Y516xwE$|9bnyl6OT|IZ|?STlN{vqZnk9r|(~j&#bmIA!{SNPtB9f1kx^62tw1 zsR`<2Zk#rQ$z`z#T!QZpI~-2@H|zQnWdD^GIq|Prn~Yx7wjI>gNTVE7v#k7Q((zyE zpYd`r_7M4xugwdN`PrY;rCV-j0eAlu>HoVn)%v=s9F+S9!uiwbjVgig|N6#%lViTj zPnGiJT1keH^55b9-w9eE0Px(w@bcXSpi2bu9i|gmKe#!*68mLkE5B~lF90>8x;o?9 zJpvpmTY2k%a6uofzFDd<-7nN#LpB)w^`(cG7a9@4exn~L%B(XM2P_J{bdzc3LJgY6 z(4?P>9`Oyma}xnXN$MJi?w*vs0mPh8N|qH93Z%49A)G_AY3avT<209o9V@H!0XFe2nRz|K&f<@it<6r&50VqCAi-3k9EBq@xl@XkUH#GRTGzkZ; zELO{goA=zSz}wIIdi(wM(BZ_FUxL!v_USz?(zq{R7DDFuNs5h=h?N;Hwn&M(^t)*W zaw(~t4ny9|JJcqb2t24FgTuI5ImAwX^TGcZ;10&a8yX2WQ`ff*TMCPsq4QSW5xly948KiBQ35&1hzcup0^RmCO4lbBBCbKQ=jVz$Z`g1ZZ z=>PNvN?k5E+iG-}QfmaO%DRf=7dQL5Rk}u+&fbDf`WqWkY!B6fJQ!g?V*A_7?H322LGA!nG=Orp-D}>5c>U zF~%11rYfeff^Q>{WdZUG%P6)VHTf%SoW!b|%!FF~mL+F`8t8A!t-yxI!ihF&2&Ish z^S@afI;oCHbJANB3PpF*S288s>KbcBeg}OT)_=eImpjP*Fyt-U`C{aA?XEFc2=$`} zfAjY8lGxpW#qO@#Bf#WCL~$~2F5!Dmv+ zefiOr)g&~g$7mM~H^AT$pDA5sJ$r|uy?f27_effBNlJKPJ5BdyI%x{iJ#>3%j&4BI z@{f;fLofFi#~2eWxXf>oX(7M7sRx&Bo|Q(oqSJfA7XD?Seg<{U2cj=p5sB*pV}7l8 zu5RpbY{QN}H?W0Pm@@S~rU2I%=rV|2HUN7W_zzIK)&E0wavIcoX%)v7#DEM2D!1ZV=bH`Rgb#eY-t$U=4^lU z$V?!q2~{OKQ#B_-ph&Y&^yy}3SK$i^KfJg=C%Qe#U*n+(cGNW`HuSs=t`ZfKv)C;= z%#WN!ljWB%45fGur$NsUi6d@u%HIMmrQwLy za{q!{N7!2GQ_63qOp^qIRNbMTF8nygfmIGlplL5b1_Te~NyYfhHum}llcH924~E@6E3z6>ntR?_qq++N9O@n~$o93GO&FguD8K>PME?6FLJ<^M92 zBpgpCv8p_>V??mw_lm@vkAqi%#urL&a=(M@;Wub8C*hV)h+Ln2P#5kNk0mi z%HnaXC634)VJ+I>Rtx(mfV?&vDJ=TOZlE?eEVAbfcC2`%a_#B*(zl2atF+QqG3l)D zkTGdpOh=6_DF*}d+0~Il6Xq1ILuNETAD%Nbt@ej0Z)^3_M6J5l@_k|n;Dm2?(3MG2 z<@O*M9mkdx^diXWPqDvY;MR${2O(e(Tta56sw804Lu`t{g-};T!f0)qPmOHucB;M%eI)IJeSQg zoi;_YXjQQBM)k$PGo`_4~N{is^8X_q~`MVBh)z*Dt=5-Cr zHd;OG8`P9@mFShTPY$pigd@iK_Qd4ClaNQsQbhBZ!bd47&;I@XpbV?TU?pt?I;~O` zEu2=efHE3JE&H)#Ggi8zL}GWiLOx5lvFw;H*2+phe;JzV=H;VZ~V zRI#z`u00S}*PM1qYmJ1t(me-6^%yW>-X56iCSIdSgn16T4JNzLe6tZl#B$KF8=1w{pcZ<{>6e(N+Nz}hUY+3%O)@PIC(uQBa6e&7Ri z>ru^EmVUv`{=#qTXKYxrlNz-Lg;3?FTC;KdwZrPS9yoh3D~+~t25_ZC%n4J#BUAMC zZsQ@4@Y{S7h_e51@wh7Ww+=l6kHUl^VVv&SsXMhQ+@5!EDp^?1hm($B)5Rz@T)@=f zqtKJUsU05oZRqWLzmpb_jqVAEc*HliIn&F4m$e~UVW-A5D$KGRpLu>rM1rJ$MlSZV z)FF2t2XvrZt}eziqG=*$5T^3ax_^GEYGY{Z1$)9P|2nU z1a^A6*6z6EnMWI#)5f%P)b#ZbP0hS2%6Jx{;qGz2QTm_s69JM(Hcirj#VUSx zqIoIrX=wv*T~XE+deM_2uR|QCZaLl52R|`z`-YBRq2~-e6CN8@KDjtj%o+na22Q!( zMH811{E*WykDD8-qgTH>HDvq!lbLMaDLFrIcwfW zNux~7<0WDH481uURq7HDaG7fvZn}46yFXm93}ffEnNc)I%V8zv{$o3CGtc+TV*Tju zAM~JNYtw>kb@y4O$=`kNGEairbgqrEUoi7Pvqtnq$r!tAH>5aKscu0s? zsaPon`T`JYSv8H4n^lUlv95=iXY%!WsO*0@Mpt%Luim8O_*(=#cPHTgoDeDFpr&;v z`^xJ)b^{&#VbA#&#J%+oX<`MmOt-2m1o;M8%NPFgL}I^hc(J<(6L7E3(D%l^!As5j$sM}Y6*&T>H+_s=D z*PL!x|GhbBs9Tx*=-pz_3p=l+GLFFwen@knZHSK`(*e>NHaP%%sNpoT9i6j zmG8w$W5WhI*NE0f;qhj94eWO4e84UhK&b0YpSFF~!xAjd^~r1X>X6Q|vk0>Q)u z`pMcZ(~j;4V`erJQ^bv$!CkJJR&W(@i6RtxPx?ZK3jn~=b!I9bb+>Lgp#G3?!yej@ zU|NY(K;xBOxtTD`oqAX(ZVd zUSGfcrc=MUCMsB3^TWHEU#JE5O{#W_JKW;JGpMYDU^|7R^Vql5z0ur(8@R+;_DYt% z!Xv9o&qfCgNoQ2aWM`xmoyjvAv##&w<;9G@7Zxfhj7vjk9gr`@Hmo`|CEJZg3KC`4 zGg|l?Z0At)*Je!PDMw&<7u0%Vj=}f+RBpy8b9{T!o!z4IaAdA{l$qZy;?HkLsJ9U! zI@7Nnn0B(#%VCOHq91S3&)NC0nI0OtJ>yydSI?8vPky+m9$~B6!FxwXFnzDk=^gwm zyKDpzWYF=6hoYCt*Uz>6gis`k9D56b99#N7gy8(WZlix_<8r@`<3H`BxkEat_6|n? z)0+Bub}i6tCehW9x!cuR!4K)q97V>rtBBe5Sbt85F=ZY3k{#=$7h7^@GVQ}Puaur| zA0?9mxU;$wPV4m@x*c0b&}+A}mcV?#W?vVM?40V$A{Q)ytSW95+4;xUE(~3O_?8r7=5MBP_;_``TKi)PT{$7ggJ6nFw z-gCD2)^9fyN9{6nXk6mA3a;PxP0n#qLGCC#YDlmfp-hi;i8Y_HH_g##KDDB4p`%Uu z=4V8%@Fp8*osf4y6v!_0{jBH#*7XG$V(~(xJ=2!ggSTb$w8^DDo-g@7{%n^sx~j(J zq2X?QeYe8CBTQEw+X&%jmE9?q-L-p1aS6@34ec$v1YfxfUDl}x6lN#yh0sh8KAT9F zqY09>YCX?mq8wr^br^@xkY@aa%i2}RMPVZCdpAD>wS*@VRtIB_0-{~lmdmVqdXXjk z6P4d!Py8Q)v5OW%w)sRF$d46X7=b(%tBq36TpjK+RyKQ__Tt)pM z`gAk1{Q%4XdWamLNIR*I5q)3D8N1-m^?o7p8HSGlfF^hqQmQ~inY~%fIhKmL$b<#% zcWMD;8x%^6(w!xWs&1IPIV6ntmqJ1m@kc7ePWSi5t}KyP$K>Amc~o!RfCh$zj;><~ zs=w6Mnl0H!jgD+cJA7jJStVwyiQJ})Lt>+5)1hqlIeeS@&U5pt_D zd}BU3{LtEJyrl}0I4^zCAayN%XiI?P`}hzUZR!aoj@0dj#)^+zSfVoRGYGDE9;Ri6 zqxTAbg2Z|+CYyZc4lBgo(( zd%6H$7F8>%H}?IPUNy32h*xH|odKDd`48mZeSX>iz$Fiobt(Kw>;opZ5%jcHQO*?7 zJsQIi6FdBs$nWgOh9V&-1~U+|{?Lj@zvuAviPacr5)gS}lhNWb>oE=BEg zaWvegB&+|y?bqJ2af?M}e|Fe|s1SHr0A-GfY3QzYw)juq=wdK!j^~vF&zThMLlt7L zBVae}HByXwwBkiX8zwHbC0tDY$@K!*2Y*ap6SlmrR7zz_pk2S@-Hm=cV<5~P&1dI= z)u>(lj9c?ctp5ax_lS}(YeU_R=Y)DPAPxhgzni*uNj`_DYV4(|?Z_dcXGA29V3zOM z>Mz8>sLFn*OjDjX5CWs0^f|_%AQ3(nkV)Q7glJ;wohbB$;f?X3Fz^>|_dpH>%>3UQ z1AVN9yzMAN9tL^=aOkcB@T8<<8E%`SV)$Q0#650G|>avkQ=XL$gTB8lNP|gEWY(Q?eOADeY0m_ z%y$|&JMyH9Ysip zYd4h;@Ba~m;!>;(%ut({$o|70Wp(COqW|a9ixlvvg6lUW3A)hd6e^m#UT$#$Qhw)F zX*EfZ^j~YcC_$xiI~9dtu6K6N%9#h*y!3`tW_)oTDY&CelmGw#09a%IivjGFchRUU zsMNN`*<@d&w^CKJQI(kAxF$nNP0yj7-_ zNE7PG)oCpi{L{3QnXcO&>C_@?kp8B6qjji3iVhB0O@R#_EaaG&hqN~GaaTu7@+~LS zsWeagHFC#I{@cbzqom5NqoSpL`JU>B(@y4rIV7t&J*n0mMzWj6SUQNdfY%_mDDxEO zp3Ddav<{}X={ERNLxj{^$3;0>Sn0HNEfV@Ty*Zoj_fpgg_Qi8 zUqAx#rb$mr%?Knc`{G!(yN9Nwuy8JGDyxysCyG=BG6DtxQb`ess*i~^tF_5#?&$6yBkpu9S)S@aXigB)Mgt{1FjrYXk$O_EPtW{)&6eG>X06m1 zn@)%4vBohxJC{KmAA|a$IiM8VRC3&~h8P(kzE~Xk%41>{3Sw;Rt!XQ)Oo=;DN;vh} zaQ(H<%FhO8oA)|pkWa>h);jeCc=r!}3VX zkXOr}kQl@wj0G*e7RA7*b(v*rFi3_ofyzxODYTq;l^4VN{Ew+1de>)82ol-spp+#9 zZBApuvx?jNQ4_i_$*Rz|CyHqqpYvi>H5idy;j1WVVnoIwtyEx2(PrgbBPwi@&D8Aj zWk1R3Bn9l)LSqw7dEW>}WY8I940SVSD9wfElV>JT zzC9i+1DqW&KEa3$AKuGKiLBHPN2xE0YJcftG8wZW-X4*LEn#6@jxgM3rcM_Ef9A;W z<-t?n`4oIi8tR~c9*cX*j{NY@bcLaUKqV9*D6>*(-cq?g`t3UY-e96Ge9cJ!UTGF^JbKwZ_IU1_fW4Kz#3&NJ=^n1 zU$wuu5ByaGST>A!$bt4O~Q{Iql2@CM7#@YIgFiLPQh4h~zMy3ckjmHn>@>@(}YF_qms>!FFD;FN6^ zdPH*;v&rD>b0aSG{3tIIF?8t-fTpNUh~_~^$qBYAyaep$$gIzm0jrTUelz^r0x|Es zf;H=IsrRPklgv4Jb5jT7&$y=)n`z?f2#kc2ABsc%X-AvE@KtT^>Xew?<`Spy_|Etd zScp8%i1a}#z1F7tV^!sa;dg(^#JH<%;xZYhMjo?>RPp1wfuOWd;T|{%5W#5! zDd2PaIz>~*Od|b-fO0&!esNJs_IZYKR6T_LYkPwh$O&xp4&pqLMEv5N5mE`fYhgVS zE5&cLeTm(rxO5)#-6Xt2TUO-_ut{_l%1rQG+@e(=)9?zRm8l+IBxa^K3I4uu>U8qq z0g@OtySGHZ@_ft!gbGb@94KjzHx>8>i*&?)Vyd~6w2iBMrmqBY1qEL#piXvrfvgNc}nX*n6GVr6*g55KTo6;ng*S z?5Ej{)fomu*@@32V0_%-8L@t(gWJBxSmlV1-C~uGk#&(AO$=+?w|bUHvipktaIJY( zASOL~;6g4mHp6DXzx1G9xuT}u4t&%HZY2HcbM6ZDE$TI%AOneJT6%dQG2bKdzLR=6 zAifNxEDsX{4h%(%LtWZb2DQoSS=WrKRZrt-fOo6nTQLmQulyO>{^J}*h~w|=pTm>A zZbpoe2z`jsL@@eLqP;m-cWyFk(p#Y6b;*$`28tOSrDDICWas$KI_QJ4>Wo>WI&{Hx zxqwBY6HoS_te`ge@2R7Sxg+OYES6(r=%3UI8Abik6YD^tN$$-N!mwHr!oz;{&mi&; z6chto203})o`@;N_j~RXI{LQx>Gu|SpjjZnUmx?41m_HQiGK(6bK05a#;%9|Dp@|E z_#C#}-uWmB4(JCGT$?A)IgQ$F?AL@~(hATc&rL5!tPb$#)$Ni6wx9wQgU;<&rEw9I z5m#Wi>}$ha3sSk4!;N3aNQd5LA%hW;K-IG517PFq?8)j40~S5Zh=u@q=f=iWL0rV- z6?y|_)ldw>0m`>{!b`q88I$$P*;{i>45ex*MzQ=YE^hJqn{Z!l5~hszVjr9My;zhT z_wJ_jNNwrSHTH*aaZpY=JTfO!O6b}fh;}W7kEb$YcAzRdNK{j(YiqRGHB!N7?y zeR%_l$V+Z3;5z6(V_N?xL}>vfH{0Q;eu-Pakc3G*Ru3dt@1mqFrpG_{X~le-`!(&nYxE%nydRxMb<@L4m`~vzhlqu7>HCwKHWW#QY8|h`U!2oK@ozil z18iXZBe2UruF{7f?;55cT?+!ta6=;cf0GG$zimXwJcqYJuqMTlCho}BVbMx0<5qX zPmN=Z7m=KTesbgL9v=i%x@)W`^yibG~bL*Mm16eORaWBCq?gh)@UebVKU%jK5~9)l=3=OPzVTnwwMofH}qw~5{U2z;WS zuhyod+{wQqudoaQ$Qkr?>Z`jGOd1@GX2x?clO$}AOvs6Ntp5T72te*F^eXfFAyS;| zNb@lTmak#~powZFc1na88aLa08kfA%=lImEbEiXz-xB#A@1clzAAXmX{v@$_Wu1(#Z4P`BJ1uN`8NN#xfNbG#Rqyx46tttJ!Qy%NA7v7^(`^a|j+(g1&E?oSnXBpCKt{>2c(xJ$>8Vm%6# zxKbFHPkSq@yc=r`RgX?;wz&Ao`+QqK5dWa%wts>)Avw^_K*!0{XfVW<=;{`LFb(N3 z>{wg-y{8!&s}qpc)zt$1U0muIMxnD{``%Yl5RL4D6=*KW4?L&}Yo#)(9I6FF9#;eh z(KP^^bgITSVNpr7fdTcXPZTAyR_ZbOD&0>HBx^5V^Yjh~sV0@($-IMZ9?4{LGWj!I ze_EiqQ1M|aG~0V#a{ZJ1dY{yrB>s$#%*H(;eHJ}nmA4P+EPWE6hg5$mV`y7k2n7{{ zcgNjHJixCVj7GjbLmdpZ;r-$UX^-bw^Q+2od>`qTxE~xnK!V%~bb#W1;h?X(lECb* zZl&o>HFjkYJ^ic%y+REKO!@YdEpG+|2bT+e1KEMBL;-C@0e^6`MQ;wtRLhYPmh|{G zuqU4_x8quLExCc9-FU$|*GlRp?1?u#Y!hX20Y|Jnjhaqoo@|?)_jnxEqFtLbThBeXpH=6684|NLFd+s z1y%6&(tw`6c&&ymHT`Vdn?;$E$m?`s4~@A3w*I@`PE>!3vp4Nl`Xl-tN(Ckmf;(I8 zPQO5Q#BYdweI~wRqrNGX+OlMxa_s?irSl_33r{-{ijRJ5Zh``#(~jXo>>ab3SU!W# z))r|+!g2J<=%2Tj6l$$F**Y@PXh5e#8P+m?S+s&G9OT~(AgF$kW+d8<9FHS+F?cD;7RymS7{r~; zHFOLMBb52ny5(tZNm_ksB8met-lG))mK+{XCn4ZwGk4jczFZilaEZ-^SiJ89#kRJR z!fK^u$DQo332e$O(3`33GKY0z_w>p2&&!LRPp%C&H7mk!zL_Qi1n$-Crv+f9uvB4! z@Z}{+oM}}bz}1kC;osG%{axFX53@(&2UONnxd$$SK03zEjIV}QvxFhQk*}DV*N=XI z)b5Rmw2s$3Bdh=fONK&WAf$m0lUMo9!uZ|)@~=+dC#m0C?h+&|!LLIq7R)soe)Y39 zXcQ2y!xAj`0qtM0H&mm`SxPm%%fqBbTQiuZ)()_vVR)KH*O4Vlxd@gR8PVd6VXKt# z4CZ_eeZ*epW<7Z`G)8n8EdN$srXypiQT=J{52KAa$phBh*Q4<5kA3Z}IlNLm#4mhH zz!Pw`J>-o%)H6X<>QC#Tpx^EpoQLfG1SW)ePM=ki6$C`OtDLrQw-EKbDg!c#E&0<8 zjH^(QGa-i3zZ8+P1pErsXN!zn`USG6N4MQItrAPv>M0g%JfusB(Bt>$c5Z5apZZQ5 zYPC*`4-3Zrq(C#w{~#1K{ABCkG2l2cwoygYoL*P0%f0Jk$OF4 zlw7}>^kw+-td3!A!@*O5%nrS{m^&{ZgsMRr*rXRubIhpVJMln98hSpAqgi8~79~P5 zxV(3L^;y17`^6l)4s3}Kw_pGOa%P9o?yb!FMU>sfih4dRyZIH+%*G$zdn*3)lZ$w} z5vY(g(%|a9i@F82k@o>Ky}?Ty4}1YUH58=t&f~>~COtd$+bL5&0omMOWfj`#lmiG~ zb^h%zt@O9e2+|wcHUAu01b%Qpg_GvY&EK|NWz`QAm(ABqZHxbZwD6+uxx~;t8rn&l zM2<=1%?aX}ss2-uGsUo|2o?&-^N(KtMx_?(w~`(#Eb_W_x}Jrwb8qeytr~{dm+kPo zgUu5Er~WVHnOP=fMMu17EKcY}oARRKs4crDbl?W#%l&&ggXOffr%=c^lyBrQHb|hv z8o5xIMCHYu?Vb+Ikoi&r-BP!T{PO+YJ}{s`_ISdwCZYmvM`S}~e(!~L5l&cZHewr| z!8*%i_rDIKQ}ovT`cQ8}4K?kik!xBql9Rvk*NKx;eNf-@~)e? zWbB?6g|A(+g&>~^7hKEOZVU=7?P!({!LK=ZwYw!dNb;X*{KR zj~JbtE;{x7;Va2ETB+8714R;1k_&^!x$A%A^!9)gnS<$2%H(0e4Um*}&78}ybNSCw z5!gOGSFC)os%ao3n9kDWS;=SWev=cAMLoDc0000LtPkBRJ@XgM*Jbb2x6Et=2A~Ug zXGruZ%9&Z$qh&w~ru80ab<%qE_&2c^;WhjJNN{`A5S3hhEf|BO4p_l*jc7hzrdbl; zXdJfDm%E%=6geTh?E}vZt0@Vv$fnJkRP<8`YaUccCyf~7wHg3YC!wo_xD;_*5n(e& z8V76-y&-v13kpAnB3J*nn*#3-6VNrHNDT1MJUYdYmWHp#D>_X$xIjHr%K`u#DrqeO>vV1; z!E}OKmx4u5z0}tHJYYWyPbZ7V<9PD9waq3k|H4XAzVpajA2OSDTJ0@%V>qeDgdYK! zDII#Dq9T1Bafkt++iDw6zI(Ti!2s#H?D>PDrdYPTT)5et=(}rO1MkEh^H~olK=uN7 z&NY5ZapHG2j${!Yn3Tej041h}n}U2PXHMw%bg&~*rVSM?IiZaV2UWKtKiifK@f2>5 z*We7TjF4j|AzqtX{l3T!{V*P87`OB)5=2+}RLbTEoJ61Z9uBE8+}phTpo%c(P>Ufv5hMdu{zVuaz$~cmt8A$huxRKLn7DmE=(iMo}9ZfVf*3N!D;f3 zuS^Cn_=qwws_ zw<`ih#UzANc^cP{ZRQk-y}W?PJ1D_nce!=l6(_BVzHcD!?vZqhu6EkkjuET&eDD?! zU`|x&Z5ysfaktiI&NX&#Sg<#q+giQQ0gO#conW=e*(H>_!At4Rg>a;^_@XqS?$feM zDSN)7zBxedTn3yzt=BL5TD^N=Tut}vm;x6rxPZ}wVFlw^$dvc*IcwdT=ir3Vw=W62 z*NLS0T1)N?F8fg0UY?hR*^W`S1+vzdju4|s~gJ6`fB%C@^s0vIyi*7+_l-;LFS>1 zoGQqI9RxOS;KG4h)B#kR8b5zXzGjx+A_Hea@62?litxDCj<8~w{QP6K$o9dQ1L0H= z-W}q&du7TDe^>jcD_6;Ls3z=UBqB&xErhfSQfu44dqM`tKV5I=wDc?b@(_RVe0)zN!f|zMfuC0Tnztm5J5UYA zvrhi`@7D=?al{817~kI~)>d_V)kc(odTk91Jc0SId9bK~Ows3+%iZHxmg^x{#L1)H zK@5Sek3@b{ksPY76wzZ%WWFmcUJ%pUs`Wy5txB}gal8LM)A0l8E;ZBy`V8nDoH4{V zBRY6S=UoIg5H)n+F|qC$LDrgeKeV`Nx3;JMnq#)OSYwKR_iKLT5B@J{sZCxUGy zwB%{fD*}IT1K;yYv{Ib3pogsH@!FU4Bu_MO;c zDZ%=6SuyyV$%?uFNmDzPW;sX$h-wurX5zyUkJ{FR)f=<2?D@qheiO$x()@lY0hlq` zad^(XDM!!*it)^*g39fj2SQI)TFdR(Ih2=!p`U`mpwUa;D&a;XjqPVLH_DKVcuS#U zDI)oqn{w%F-^tRA9o}L(G?0I*>l*(D1pBsy2|hq9 z14*`;)Z*@somjyDGzJ498^T2Z7Px1fi-C@?8Fw%htzu9HS!pW&PNqR;YJQ=atr!Lg zN;f*_w3s~=3M@bOV!la0VPQ8z!mIq)YgC#3C9{uQ5PCa1LoV);VQcY^I{LfAvIcWhysSXCXJ%Kl%J$b|8t57M}BfZ6vFk|66F$y&=FZZ`7h z9XT5G>AuV+@2t5iS#XZyJ%sw{vx2~g^V@%hLQ!tAIaC`VS#)E~;rCW?+fij%JfI@r z?zyLSe+YA}OIw+hUI2l6s!O?=vDBgXf27~dqWAMCCYA*x>_aR#z zuD;8H6ALBkAmDJeS*>1bY@)=TBs^@)sentZ5aK~Vac7-5OwJsImp9`k^rC-ARt2>L zoWj?O2r0tunhW=cDWhKt_y@WC(iBAYtkTvV7gr7Pes!+cS?)^`qHl_WPqWVNC9=$6 zYD32Oc}YqIop6hxD6~!qt(K#Y{AuYg14&-gZ;wDJF$0UC7R$NB@rk>@%&ePm3R=t) z^P?d+$~m}qz-)+?xMFtQ(>mM59}2_qJ8ov5rU^Ap@ku**u-b~Tr!Lj2eJV;-?yXxW z)KH?R=!y#?3RoEz(GfnBAGbk#8J0y`=e3is3;?esnG7FH7U@==L4gX+&cW~B_e8_? zErVk>L*~b82v{;{?oKsRx}~-=&0iCj{0Iv-aY5=;?TW6%+q{4G8$jhn4WoiuqpV1F z14@V`nUzm`<;EsL919ba*wJ_UzSedMr#hj?ST$h+Rbs;_84&8=u%slS#VN4M zg8rMWJ)=V!*Cz}S;B3HLq?n{?FTlC)=&<;qWELtr z1LrspIuRgzY7t#jufq!PXg?fVe1QK2`ZQtR+EF?>GD(7O##MIW7Wu{C^0Bm!y8Fhl_+`-R(SOQOBCgzVGJAN zNe6Q{O?juYHsy%joWDU2z00R%i|1blk1iMtF%js7!QdtcPQp@T(HlPiSHo3Nwt z%ijJq5JdSKM4_%rCHkZT8MhYKej{Tq;X zm%lA;EWhEq;7!Gn--(~~gsbV2(9R^=){-qDsq{P=A+(oD(>AY{5|u~IjIFNVcSx+hLPq@H1p?t* zOF7N`Zz?AaYZr;4c&EGks8o(AEg0x=U5p}(Uj-RCD0A5Ui#m2U$pBG!)CqK1ZFfjj z9QpWuL$N^NI1T^)Xa9+h+CQxq{r{-pFvDlhjd2GBjg2yuEd~IKeE#6nr>X82(_=-pDXfR0 zztJKb8fFj1ZXEP_ulV09Xey^evOWpCvHx0mL%-wddW5e&80NWLO1E0)M64Rdu~PFi zUzxm9xkWmkb9O2+*s5+XV#ht*`;gi}eIJiK_IwIwxuL0D z)1{DBYvE4U;qI_X|1~y%eg%AZ*;(uv8K3(&@#!)%6xpuG6lBz!9SN zc0=;_tEoE)Xj&a95-qG=fb+SRj=5q6)9m|xw9J`IrAXbd&m^z=yZ6?;vz8j!@&f(q z|NfmjTIaR0ln^cJh6P`E8^jl;YjH%zG|_<+swiaG2E8F&R{J8E7muSB@|5nQN6pHaBOG9KdrJOlZuQX>*KffTN>g}TAjwD*(?Zd zY@H`)m?Q4KmcD(i)h85{(bXr6hY(IVC!Jvbu%&j;%BWn1zUYT~`XyUk#vp*qQz)dK zwO|&aW6qR&&MjmB&o_D%Th+gE?pjv0Kfq?luu|H>JXU@GZ{PEf>Xw4H-~3~HKLZE; z|72r%rbwvfeRx|W|74^ii)ASh&_^f{FVun^*VsLcs;^~>l6-b)wY(4UhBUM27e&O} z8P}043-2^H-L-qM2!xp!TrsLYLn00X`dw6>8_5-bO{?#fKUpJ5(==6=^Iyx35 zsEWA1PKEB6AH#eqTOgI}V7Ocm)=KXK1`Eh^gNlOXVoIf4LNZ0f5mX_*2fdMJdE-IZ z*rfMP8DFOEDV2`xQlNXAdvdSLra2T2NEtI#bfW3+ zrgD$D|NP_eg#!%lR!qM2gw>U$e6`FcIVSzH8oss%&D)1_H*X;#q1SG=A(1JEq){yO z7J$UMt)#C9t2#P5R7KkppO2||oR7YleE#7zJI0|6Gz`_dQUcWShl3h}$zx9K$rq|s zj*@cPoBtxroyA0(rL=O*^z5T59rF@%%wy__L1jfHZ1DY%;(Jx6r5$?*E(r!=1c!=d z*e$kr+nN9kRyS>IL-Wuh&qKRGFN$s9?@;qI?$KBrjZII^|Jx>rfk$f1iUp~V_?_v~ ze#{dVI(>f44tPbiAqVb7pL-o*emC8ZG3m1S``|IbgzL`-*e_Gf$)t>P2E_IAKp237 z6BE&dn1h9^I4JLsi6ibDB04*P#u)Uk9&3~#z~-|_D#Y220wVyowBHoZr8jPmVAlSd zOMO<}Bc^IJjk!_qV)k>+-a`gM`FKHu+=!orr(&_ZYi^-owI^kJrT=Jtk}}=Q&$Ml?A5ujF)q#( zwYIfu5ZJU|0VfmPAh6eyi5LIAnM7lUYWfOhuQ=pQb8H8k+zyoY*6&a|8%{izURV`6 znQZD@H!h@Ih;sM(*$oKT!NVk@5rfg{9 zQ+njjgPk{H*$Tyz%K@s&Tq<_|$+KP~6f@ORHFqWxj)8oJs)TVyJ8S8cAv}EmnPe(_ z7EecRYM9KSqGWqhIe0T3>>3y6vz?UnCdQfh$zi41s|We$I`2KIDjXgcVf>uE+` zG$r@E=3lcNeV>!Vu1w(3hHg`wz(A5ug!pwUG^=t91BP829P=e=air?P%^i1Re07yTA;Js8}%yPc>iekVS_r!FCIMoJ(D@=T%?}gj zMVF{$XW%Vbu1^TEF8jjq_tjBWJlFvw&C0(rn8@qCJA{l=MgFdqFn;~hxFN7vjE|2z z_uown8W<8{Y_Ad~mUxdld|3-HfO|9-tqxm(J|cxxXl>`EalSyT&d;zsv-?)$ZXt6`JWLJzmUXT`}f zdWo+g5(oWe?0nyRDlR)YZ?@cfKn$?dy5WMbx9Lt^Zc%YjMO6+18d+{1%NDBnmEUzg zDV3ZPi*JgQU+7g^1QUdiBQr&b_egN%Q4-HU*L(=1Fe}`r17rc3vb)k@Ow*zp;_Dy;;|;)$g9of zOx*!ox-nEegtR4r%N*Ui3h@iqd0<}Up4QKwRX4X(0x-5z8WLtUhfsjuT@kQw6d+w` zCw>XtpYc%O-SBG-t)>3(!5+wl3jj^NU*N` zeH%=?<9GdKq-)d=LJ?_ZD@g-wGT9umetg;)i|brN;tGN#x=!xSPdxmSr*V#)4?7n! zg5DE>P0&Bwme?(xWD`RUqEbuC4{?A~64eUoP3{8Y+0ynC*{z;EdB+6oMsi8_ov$^u z{;yY}33Md-u5jlyM!t>A(+wEg_TNtNL2he_1K&jy3&4CvW2WE%9nn>!6O72FdN6NH zM*lM1U&6tyo%GjQxtXU>Guf?hbj|r*R+kQqTT?s`O1ciePzg`24e3kF=;(ymut5u( zLqUqvJun|VJ(?+m8{jqnPP(>H()a~i$_?ADg949Qe$EWFjSV{QL0!-K2CJ8#Cmcnw zkzCeA(4+TG)$VT(^8+1c{UFvwWx74W+hpVkvsge2*ABG$;e(eTkp(RV>%u4ck!#1J;ZOQh&~UPRpV<*%u^~9`i_M53cK;o&P>6m z!VlqexnZn`H|3adOboUcZhq0|g3UUks4)vOC6{@WPnmth?1aaz44PuoYxp#i%Kf;{ zu3H{joOD#cCiQHpvg><7ku`7Eue?>bElOvo?jZ`l9#FjYjc^G2IZ(vo%8Y~Clv2QnQr&zsH-c?uc=!f))6F6Qy zZC28Q6Sj>}l0pC9T5}-1Y9ek8oO6xX%>2ZMxN-(9#Jba_maMt!@Pt8PLrMEy$p#^6 zMDoo0q+Fo6%feApfO;SEZ365wNdl02(mb*aZhClRAQ+(Xz%AKInUH%Ij$w+GgAJ-N z#AaJxukYE>R@z;riI*n8nUu(|3YO?_3*7p*18-FN3`{jgmb&v48PyB;U4UPo22Xglot5 z@yQJFN02!YrQf?Da$MoC%+(_i(bx=%Th-jsyyJLviAc2)Wpv#kiF4uFsyknngk-uo zoiV0S-)03h4iZZN1~a;}JnX#sldAh2rjyfgd>ac ziWNDFgS00=&Ncy3S~w~N&e?F#c$-TS4I2gaQhFGIn{RTNkcSaB&#G>6C?g{u<&8sC zzXYDd`1!qCr$vmAv_*x5jr~PFtXF)C99R<=4l9_;ybA3(x1hed5ICNrPkz9l-pd9m zi~IQ=ay`QF;069bhl%{Po)tEy(Q*B6I)c!(3NF{ngz^WDO3ab|mqO$lAgfWfwvRDl zfB+8ezu>IR(SGGBy8P$o{j(v(HetJF(QCK~W-JpNO%g4-O=vAOSV=K1E#^@;pqRY@ z=vaFapI(#5xi-%U#GHa6w}WU5BG%8@_~$vT=JB)%BM!|nh9gQti)=1#Mtx<-!49P? z>WTN{Us^khL2Y1Je@TY@o|cMw^Rt*LPY(qT%ZDc&^%BaIWRQK!P4#eFP~NpT9VEv} z-%pVXkOAZ+YBU!>p&64|ynrlrBORPaeVCQF`1 z$H7XipZdNgiw+UD86W|4>ZC5%z7WVnlOocMH>h=lov92;umcjdZpNn@p@=+R1r>;W z*dIP5x~_VCr`(kFbPSTqRzWO^fkqu+?Mq2jGsZuDJ3|5rGz0_bQbMebvd;D*>pV2w z#vVC14$#P>g?*f>M6te6v4@fgCWK+Gv;f_$D3}gOj$FXt%m2(1u*i0u=H*eupXV|2 zI>(T;`PP;&4>9LuEnq_ATovHj`uWL`Ief{zhL|fI5)`Lo0CB&;x5}K+V+P#>#qRa~ zEEP|eIY0h@2qLewJA&Nb-_jOG=>mN(hrj^|A#+9nAgYf0*&fuVvNH6vq{eMPPymh#%yhr^WJ-y&2)nyNE{@m=MJqgT=wgB0&CVxO(6V#^D`22px0Yg3E5K6PUR0Ryqf{*+Mc{j?`5KidA1^j~fevzwpbx*wC} zBr)Jz1O=VnpPMT~1-@c&%?B7m8k(HHl4E;k;qHKsI5 zNKJIPO-$k|Ky??_xW8~Im_9px&#{T&3=aRxVabl#$9f3PjN7XUF?!+J5U*B^Ql@%BBD z$7*GR_8Lijm2_bG=D2TpEU9H9xF8)2S9{>d-Ie-`_0rBT5kPyou+51!N2MaHPA17TzZHY%PHk>PkceH{9BC>*{GT>0Lmf7$z5op zQeqHBRIgFegh%*?hqB%w|TSl5uh<8 zF0prkAY^Mq22*TyDUSo}(Rf2l0CR(FAh3w?c~8FuVWK>wxs0qnEm3_|$dDdJgN7LZ zK9yBcV_5Y;iEfsqLgjZyWkcIVz)sI?{I)U3RYwd_NU;7m+o^6j4rtf~03kcxH1Bi+ z@bdtePZV}KtV6%Zx7M4U6pJAy)_x5KZO+tP^H`>moOMN#5Fy!sui^D>gWq4AWl-+u8)T; z_*VkF<41jdK#40C&*Se&3HfA;bBuiZzU3wLn{ML-ZO%W&rV;MvLFhsx)*AZTQ4Zuz zJ2`<3KH&T4;KvI9L*^GOk!1l;apFw3hdYP&&bX(1O=$GkHI%*|_+ki|NOCGX4{KPx z>Eu}B;+)fC9UnTSV-XdWQePX$hdDMBY!imh4Dy4OBDr~5$gj`^woNJA0`3W>4i#5W zw>{K6b>*3txm$O(A9%|F?-=mNELLdcaZ$dS2Xvf%m3gPneC<&RMolF#4zviR0Wb6g#NdlL zfx^MWnR_Jtku%is%D4iJ*i--tF!)O77(!u*76M+gZV`h*k~I)csd&mw%rVPypXwo)``F#i56;UQ3&%);q(t zDvy`pXpXO2>a_!+4()e4W=NppgqTblX5GM{Ifb2=G=DKrasj%NJC%k%j1EiFaNl?C zEkFTkC@)i0E$|d&zde`1E5!YbRc%zBWLx5f3D%KUHi& zz+YpH%`zg+9#z;ICdM^-y_ge-W9p#nn!`D$X}IN{;6pa8yRp}DmpK8u5Y5{xzleUbY-F)F>fP^yiINegy) zLPhnW3t4Sh<2?2*cOCj$UHVQ~naUErK4u^gnOLaEWm4S&l4(QO?WWE?hI1j`ctJK+ zUu`U2*BxjdaU{p#(sdXO(@5vr-uveB{k;Me{Ns8CvG_GXsi@iiK;kEC#Yo)9TVpTQ z(pn@*?Mkzos$VVOBXE6;h(0cyv0o|@B}_c)!!I6z0)v~RLQT_Xq=0Rb-8DFH_0xTM z`veuk79PC5b_v(jY>5Hl>vDo}xGbcNsy`X_@#Tt*kY~y4T^I8cO(>8zLblF^G;FTg zBo$r&0ezSHbdx@HubC_IlFL^$pio}R0#yUJNwJI^erBsRCw3C7h4FOSeWW!mswM@m zijDr5w}}D%#dWWFkJC3oXK^pPoreDOxIhf$E37yrZweRK+Pj(sbb$L-Z+8NflV@*m z{ylIGKe3l!3r5a4Khhpj*bw}EbrQm#$blAe6NLWhBz=`bUj9hXr7 zOHE^iq^N(LagL4W{S*Az(7>xc^7Eml%V58Np*`xkCAFgNQLTQT5HTU-bv~f*;~>;q zMu0-24-_v9pFEKn;S7XFM?9r|YaR~|-t#*mVRe#8RLtHQ4qjZCL%-oo^XD__pymY` zHg_(kr%3uLR6*z2=DU;qFHrs43s(zZc$&y1gtV6?ktA%`#J&8QHns>+gl{cZwV zdH>f03CzK47JCOz#2HJAJ*4J3F?2`nJHVFJ@sAFdhQ${CkVN8;utf{0Z=!-N0F5x}nV1ELS`{`d330raUM)A=0^cZ}&I%8Ni{ z6j{OyqdyI6sVO> zWz<($r*Fu)BGjB42iI$rrP)kQ8&ji#RCH87nJggWK@TS>dv6tjLCR)Vm@5?qlAzd8iJwVR#xk~`BZV_I-j>sc{l_2Q_jh%q zlWaMKO+QtvK392FErmbNr=wg)B^O$)C>`m9nQ0=d+KA?Za%*K2DuX}ElrdUVzeU*%3DEi$!@w0#?zTNR?o6A)XOeJlf{^Ydu2Dg^01IL4sWy`P2@*$3u@3@Jw# zvR7qwi^89zg01PAq)t6{hWEV6njXot_QCS*nyYF{?B~8+eTa$vr9+5H{L#No=GmLZ z?4v0;A)qN)zCdHeQlLJ+I5&2(YZ5b5QRRDWAtm%ESwfV7u z$rJkLM*dG~&JJ4{GP)Og9;OrLZiGR_!wPM)uY3nRfNB#GLZ=9>fcL=g$~zB6stTuH z`Hr-|THTO8^F0%5W)YRSC~|TarUd%Bos+Mv{U&eD@f);`UQ}4jCnWGr3>ClYp6jk2 zCRx%D6Zghx$b3@?0GJu6=|s_e6I*YyOszhFO8PFi59pAiuS5<>ZapyhS&jP)wfSxC zKpO9$?!_#sAm2!fv>j~$cgdWfsmT5cv->`8o0KrN$qN1XZ{X~uK6#TwZ&(Y*GFHa+ z4<>yQhG(aJKO!CTe_?QV==m{?c+gW6c<9nr|GX86_(n8lT`$>U%!%O3CwlviP3@{i zg;FylCjkG=h2B)MY5`KUtHsdb^;Ij@^Qaq$CA9Hgw~6PVDJ-ybdMo0M8$$>3UKua! zG51)wzrT6I<#%Ga}kG+Fi}k)+Qs^`DE92S&q=3%;!$gKH8QeFnur{+GQQ zLMg2$kv#TSvf%gwvaF`s%)oSu{8){GXH$hseKj2HLjVxRv**b77EicH0a1xh9sp3g zPDw^H{SyYvy`uH@)+JinkBgKqibAAB`)BTw6FdJ-38g&$ZiZfTT&OMTg zW225V+pFf!I|diXprm_&aAdUDF3MSW92K6a$%#he8nb7waPi92MxR}p`Uk6Bw2g;S z$AFHiKJvuI5ZcdIeL;L-3@G2OZ_+Mf6EH6WtDpo&PDEzbJ=6U-QxevD{_xj7hx$lAWX>6>3Qu>785^gG=qcFfbQ(ba2vXBLC_FsAr? zD$|NhTPb_}lSNQi?^(m)^WPkXxrK{&As5kV^=X>egUd6^`(x2TRgRBbZBcxCsqbFz z8tWDZ9J_h8i+5)E5egnI=?pH!!58*<-Q2izGUo+rISC?oCFhs0+=@;OITR%xS~$eS z!T3aU#7p@=r*4 z`U%!%Gy{jmEmml1$Aj|C0IGU*iI9e_d@TbN%EljCnYEkW5PJe~dNSl8^_<0aJ@zA& zy~DE2;p0P;MT5eo$O>HD)1?D3xVui47eX9m@3dbTy_mL}498-odsy=U75lLu+6z^2 zM~*1Q#R{Y2^Dh+D0}X_tIPLdUiM!356j^d6ZgEi!=o2*~EUelt2zECOA<_aHjFXkyu&+T?!e?I&g?#Y5!-VP(klRuSlQ42h+< z^?Xr(I1-je(n!a3k@o$&nYW+KM>pZU#3t3`dYU<(hDXkC1fRW#A{ru^8lMXlze%`k zc4bkR*HrfSMZ$yBzqkt9IsJCH5J**uM?9@o-@VwI8ap*tfnpvNBJ?F+Xi+qMdA0yPJWtC2tr?NCp&M_n>-t6y0qrT zg8W+L52A5jq!y^WuV60q2DpzCB1Pr1vwcvFq+2uHM##6)=QT*mCRQ=u#A7V7%;JjG zw^UY?CQpWxL5we=AB}s9TUj=VZzFWRmOJ&$eH`Erw7k+^7Q$wc(=zmoDqp2aMWrA>;7YvDk0WG%keRzP;>39j-ZAh zMpLFY>MV0OE+S=Ai=h?I?IU?oaLJ9Ql5!~bljIVLR~#5&%^7OG*PbmCXFQHmo_R12 zqm-G`{NN+9Nq^**6T=NughnzRAEj^On(y}=jN8RU`t2H;XFzK_riol}A1HX#vN?Eo z2+j&#miFcEvlU%OauB@PZMO_Eyu znhIT!tNQ4|f3+9m_-n^W7>34IUCS{jeun}8QnU7*8E>#u(w)&*l|typvI)GcfYLL1 z!t$LZAjavS2}yFlVro+EJE0RU z|NMUC*faRSFri8wo&lm&9qt2j(g)~#eB8Fg)9MO`B?6O!g72-DEJ-G#g(@mwhGMjO z=1S+h_o0@etW&1ns9Ndlyd^1Rmp>{g^?k3TL4~0C&~-(ol!kmXIxJ<|F}FX|Jrd3E z)-M*Jpya~MP?G#2C4Pj{wdwzCF#Ayc<@kuKQfa4)(-K z`$R6-?B3&U{#34Wl&|q+m(rsShfuqtPia^8) zB`x3bP%f^-JfwAdtJ2tDkSR6_;;yZ&zQYLx&H#|d=YY(&FcS4UO^5rIPRD(8W;YPQ zF`cl?5LL)W$f2Dt;!!zRvq9kb2L;)k%aLph~vGLi%I-1 zxSPiUNX**GaQ)iZy*@Q3>ybL?yKs9)8Z&)T{18NpmtrPe*n(E<_vzD1iv7E=_4N$E z7b>Odh$$aPRjjWY)P^I0ukPRK^xC#e*4l|E#(na@@!Ycig>&@Ch>TKe{slrisr=&= z^q&Or-k%bzT#ZK~lp{_yPSTJZvXu0v(2*u(1v+4&lC0`41|pg^EC>@mQrvKv@Ajnz zUgLFqNcJSri7I}GFC_c0p1~hlUH||Bw4<*ysIKbM ze?D7@vOwT+8_Q@+)wU(C!P|1$@ag|yGeXY$;QGa-L;M4PAj2i{{rO;E8X=hR<&G02 zxf3B0Ztr#YiZLiESVFEGhoBN>_FnAS6_M$Y{O z&$c6-6EWg4!Y?7E!RtP+Yr4MoBnrnB@nU2KkXvEVw{*+ujFbGKIRMxtV@Em<<`~jv zoJtdt{7}fVa~DkUG^hfrTcS1LagaqH&rVmmnAD}8a*88cuiVGdYwunpR*N9>1hO$7 z%-nw+9A%ZlM{>8F#u^iBNcp-gT0@#V(Wvv(X`4rDeYX%yns)MI#m6Ki`}_^A#Yw~% zF}|~!@Q|=z?$PPc?WC2Yvm(pU<0v+h@8k=p4jxT;T|_B;eGFtRv?M>21U*}CHdt^Z z=v|O8P`}XGExb~8`~ly9F%Y6A?Hl$Z<$wx^gIy=OujU2 zEjUr>lU8J8!7wBFpJN*Ce-J-KUrf_xH+`n{Bzef;7*~(%4;Q1-Q~67jmNUiV7yN}E z)Jqlu>y^4c=^3bOM!!V!o4Ah+7NmJHu}KI;49*gM)0j!=8Ay%r1BD9DM5L-k(-*`) zDVnXGdoG@MpB*19?bj@`!y=4F=q;qr>0I#)Z^jm1@oi96Z`y$H%=?UfKZcyuE%~@w z;PzHB1$Ja#a1v#1;2^k57JjC)_W!#dxR>QY{-9a&^%Rxc28V3Hk8M=36=X3pDnZT| zj)mMC{n?wI_UIUiE3JAGhpG5zb&A=o>-)*>*0nVF@XJ$s-6DtJel|gUoMFE1LCggk zijZWslzmq5N9RBAo}L z%v+#k;DPn31UT-#eDu<3CIvwJmv+!cw#xoLR_>TuyVqb93k+LN6-ATBAVQLtdOPO% z^%uHLTu=y}Tu-eM&CJCp@it7pONF_C$0qY%yuWQ5?0ONnN9U{6XxTA7dfF!Y<_BYj zGp2cvE(LWikLO_4H0i%u1ArSk-)(>P43F{=d#d=kh@Lw%z;$uDiE&}Iq1@%(^ia~6 z8&jDrEFOEl$M@w|KAcEW-(873aT%e&cmI;1t^|SN!Pi8Edve--2IiH5J}v|R@Q`ug zBpP1!mX6A4N3J;;bfffEdIw&K}t$p9{oW$ zy1cTziz^rl0GwSs-8JPTDfJBuDUn71Fn|K@5H0{PxAbsR(USe+&O$*(iqZp4^uYOd zI-dbR5C10FPmwtr&QKWP5M5)atQ*~JP@^Psbvm9y0Y?t$YdZ!c>&hA6{vq_@4b z4;)XyF|CuAvppQcJqQ5L+1lI#?kPT4ht|{D(hiOv42SNnttkb^BJf^7xBUmU_y_j1 z_J!*S08*}Qe(v_RcAk`smQ0jfLPCO+^4301)}Eeh>gJY?=I&OMQm)Q!<}Q8!@MoJJ zxB#37+fu@f%+Dpn&(Frq4mbZ_>AyYs7uA0c9{ToAic^h0`V2%c^mp0cynmOu<^zD> z8QeCn|1Pu00Dy)F03clWyNvc7Jen2`0QIB)Xb-`|c(L>JbbG?V;p5}OZf|YL{$S9* z(*H>C7v+Bq{!t(MLw$eQj#Ap%#@x%vlk&l+maa~&Uhb40ZswNOlx+Vwi2u)w|4{26 zda!C*+gQ6>yTGf`g^x0O7h8CDyI9$K+Pk_?+PnN$J^X)U`-ct>@Xv7#2e^fI0G=s3 zfHjH-K=y|KC@vZRF@~?K41oUXH)RxE;Ni;CquBg&+`}>a{2#}EJAf15f59I1wv-RW zQkvS7mR|1O4;X$;JRA@JQ~(pe1Bd`}fCgX$*Z^)o01yTw02x3LPy@691Hc5Z1ndB3 zz#Z^`?^hfGL;|tEYv2u#3FHEWz8~hY(3w{QE4h{#$gVVwJ;0ka( zxDz}Go(8Xhcfn^60D=Y~fKWl$Ac7Dnh#JHYVgqr91VUmUsgQg~6{H#R1u_X)gX}>r zp@>jiC?%8)Dh!o}>Od`_&!B2*n6>2)zi?2pb6Jh=_;;hzy7Vi1LUAhz^MUh;fM7h}DQ4h~tRg5zmm2kcg02 zkVKKxkj#-hks^^YkSdVcktUEfkS>tXkSUP4k!6t$kzJ5OkyDY&klToGb{uvYc0cwO4iXMMjtq_!PB=~hPB+eXTnH`=t`x2%ZWwMMZZGZz z9wHtCo&uf&UMyZE-U!|iJ`O%FzAnBmeinW!{u%*P9{g@MwUU=O}0l)KrTV!1HB&oOZo=-Ee1jc1%~GgweK&lvL= zr6@0S7IIAx9#|7mhnlK29gjLe51l94oW(DyC)dk}OzX~A;NeTrC zwFq54;eX=!tggfYP!U=^?fNlr<3$vVm3Qct7;q&lP_(sI%<(jziBGCDGuGApvwvbM72 zvPW`!a{h81@(A)u@`>`Z3St7WQHrgg3@shyy`sKcn^ zrPHO0scWqJUiY`2v|f_lnm&hqfc~HXv4Oooqal)^u3@p^nURdq8>0J(>S2NEyKeJGYR@64xcH2(eF4b<&Udlex{@6j@A=lxzqncxh~3yzDU zOOGqLtFP<08;e_{+uE}y&)z&ca#wOM_5gW2^=S0OgF910UW{H5UhCds-dWxkKDs_1 zeX)IAe24s){i6LgpUXZk@`w6c`ggscdJ*zsJpdMv7YGWp2gmP3{49C6{Z)~98MM<9R58*E}}g0QKWn1Ec}I{+$d<2ebh)aPjp7~U5r)CKrCl$ zYV1v%W!%6ku2<==?&EFZM-uoGa$Y08c6mLMD4tlFgq`#}>3gzDa(xPAN=(YHH>Pj; zQ+ZQ!(@@jA($>l7LrF3Ou6W34N&7%vr}tUJM%&5L)S;ak5!-OKE18Ot&6D#)%(>SHP|(* zH5xRIHz_uCH48V_eP;h$(n8ge)r#Mm(1zL;-ge*qy#1`htz)m#w)1CurKXjMZdQ6Kk0885FBV2HJ)9YvzptQcbY$5@LsrH3|>N5idn{5PF*2g z$zNq!tzP3_Yh9OKANr>CZSlM1_uU^JKW;X{e`5Sh-K5xjzs0@PvMswkwqvxjvFo;b zy%)KUz5n)r@!->;#Np79{?W#<`|0NDJ``jSiq~9{#Hs2}TE!{ic-@}yE<(*g{?wDo1CKwfz=k@CC_wE3EUaKTo~f2 z0YsskCr;?qI$gft_i$*r5(bNi49A|)+XQxu$(SUwd(-XCko6mTOuI4}^wZ_ew?dvH zE#?HuW%4LSK`_jC!jS|4$%*qG&STBgAw-X(z1o`kshTUtUtO}Xr%i9n7ZgKD6Sv`? zk_sayWomVQ!{mHiug`s$NkNZ6*E=FU7FHQ~8e5sDiEEMm3`5Cgx9Mminv+NYcji8r zyrM?XG#hOa8{Klm%u09XS2rbp9IP zmeGhbwFp3+^+ZfmKR_@y7X^gAS^Gux+?N$yzA?FpBtwttcH^K*u=;NGFmvSTZP2*r za#Qi@^1ZR%ykYlpLG*Y4$&J@WTC!Ch(*xxz%rGvAD-suEI)B$<(dY76> z7QcHhE8WAt>8Na*6BedWJ3IQzj^XXU>-Hug`@mCT*i{%B*uY0$60cjB2r zzPDAs3+_NW-D8le-`BbFm}Wr|n5-0wm?5U~u1i6|t{W6f_ZOR#UhB8Bg^x$MEooNI zT3Gi*?_6I@CIsmmYEJcQSL2)mH;4B$Q{BAx5`q0Updi+}H}^&}t8+dFX*Gv?5P{=< zl`Y%%$kEG3)?+&dibor}mUOeRx*&@$njP~A-7I`c+)Q8jXIW!`i66UmvX{(mB|6&?brqM`&5h?|k7q zd){BQgRY7=Se|-w2rHVqdwyMbCo)_-eh{&(av3ggNbo`Lw)l+R+iF|f2d!@&Ed$H0 z=gI^;VOz6cin?2kRUY=j3(yDWQZ}#6X4KJ$TN)qFt6gs;&_#D$l6yHiX>vZ~UNBQjvv!tm7`GD8H7=ar(5}vmVcIq#u{=1Y~ zRlWVTr*fGFVfWh}NteT46nLq+^#6kFtVmeZ$YNw0QPzBY0Y^9Or z#w_O~K@~frLt%Z^hSNWs|MY4V_?7c3;1Dw~HQ{$r&&$HZvy`I|15yKG<#Jns{(Iqg z$%40^!rHQJgiph(Ceh8p6R*mwaKuPlHtN!94Ge16QJ^uWiz*QB7hwf0Bs)-b-aTGsv8O8Z?W@8fX5F ze>x6TW6YrP$$?2tpQ#eM@HVEXpY<;7j{8n03vXr3uj$*@1hyT^Hl@25^>|xkvh|yk z$@Bs@I>}aegD*Q4<-R|5x`D+MyulGE{~*U6{j&C%mZ1&XX2K@*tiV>-?Feo^#<=7o z|E|l>{ukZVdmS_A^xwM0UvRP&u%l%>@o2w&`*ZdqKA6}qp^fHB$NNmTDlCt$NaC!eQP3kR#@tJiQ~Xxn?>jxfVzx0&(q+DpY{}Ad@4TY= zljAEGI+~XA8p?9`_S!wLDIsY4CVzCnZ zoI<;VlSEmJi^MciK^zkj9VkgS z{NRzRh}9MXth_v)%pu~*nL3+|0os!CNnONCwsAs{xGPQRQCZ1Hp28oQ9TSG9&K%@K z4WQCmeinh_HkfY80-svfZ%3;9qt2~lcW8vXQJH0?C(zBPA>TKSi&RrRq*2vKs0Kj8 ztZj=jg^1snhc1^S8PYX(2tEz7xJa;*o=5EEWp?^z3i2%NsHX3)THD?An(-HsNq&st zKpzc$zlOOPgn-3%N&~&`#tTy1Jhb=b>pRRa^e;j3-o*1Ij)}+~nny{S+dlrSpw}E} zG0}kSH2wOlB?zgK_ppSCvGbB{Ir;Ic5p!ZDN~!2qzd2rSvKEzXAn8J{Z_oP?Ag%M< zWRNwftdv}oM@YE(1F%U|^JHqFU>ZHx%0^0{ewq?HV(F-#_!-q6YiER{`qchu(^ZhdzL)XvzzUtd#S+I7q`QT!yFfR4fm3N<{muo7Y^FA7{^|T=U-rmfQ`st;RU@D@8 z@trWC46BNU@SMyD7m%Q8}e?>nx zXl7dSrucHXRG;Xt(l(aI_YPzg?tWJ&Ivnr1CXuob?nO75U5F#Ian{^ig1ka;jH8{6 zl>^WTow-mz-?8FM+Jup*@oLQqCCi{LTtD@qX_uNO`MQ-6lGsTs>PJUmp{aDBy8PIs z9>J6Pk?$3|iH)XUj8HU-)h&xDzekbg)Wx_y>rH2n7KXf32T>VK2n%m$FlOCg7Sl!_ z5hbCe2A>{z&TwCYcl0`kxUx|HOZP(p_PJ8PD@33qzm>ykev{S;Wd?MUwA$fWU!=;v ze#elS7{G+`GStB)$qdsjPX3jdb1sdzH+;7%(Xn5x7qW?6+XST&0HCe}0^{N0{S}}d z^>=81hl}80AuxdP5FPrrF&km=q(rTi6!S64## - - - - - - - - - Kit - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 54d2c047..3cbee8a3 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,18 @@ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - SPDX-License-Identifier: GPL-3.0-or-later --> -# NextcloudKit V 2 -Demo of the Nextcloud iOS files app -[![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/NextcloudKit)](https://api.reuse.software/info/github.com/nextcloud/NextcloudKit) +

+ Logo of NextcloudKit +

NextcloudKit

+ REUSE status +
## Installation ### Carthage -[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. - -To integrate **NextcloudKit** into your Xcode project using Carthage, specify it in your `Cartfile`: +[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate **NextcloudKit** into your Xcode project using Carthage, specify it in your `Cartfile`: ``` github "nextcloud/NextcloudKit" "main" @@ -23,9 +23,7 @@ Run `carthage update` to build the framework and drag the built `NextcloudKit.fr ### Swift Package Manager -[Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. - -Once you have your Swift package set up, adding NextcloudKit as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. +[Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. Once you have your Swift package set up, adding NextcloudKit as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. ```swift dependencies: [ @@ -40,16 +38,16 @@ Then, add `NextcloudKit.xcodeproj` to your project, select your app target and a ## Testing -### Unit tests: +### Unit Tests Since most functions in NextcloudKit involve a server call, you can mock the Alamofire session request. For that we use [Mocker](https://github.com/WeTransfer/Mocker). -### Integration tests: +### Integration Tests To run integration tests, you need a docker instance of a Nextcloud test server. [This](https://github.com/szaimen/nextcloud-easy-test) is a good start. -1. In `TestConstants.swift` you must specify your instance credentials. App Token is automatically generated. +1. In `TestConstants.swift` you must specify your instance credentials. The app token is automatically generated. -``` +```swift public class TestConstants { static let timeoutLong: Double = 400 static let server = "http://localhost:8080" diff --git a/REUSE.toml b/REUSE.toml index e4881960..1a43b371 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -6,9 +6,9 @@ SPDX-PackageSupplier = "2024 Nextcloud GmbH and Nextcloud contributors" SPDX-PackageDownloadLocation = "https://github.com/nextcloud/NextcloudKit" [[annotations]] -path = ["image.png", "NextcloudKit.png", "NextcloudKit.svg"] +path = ["NextcloudKit.png", "NextcloudKit.pxd", "NextcloudKit.svg"] precedence = "aggregate" -SPDX-FileCopyrightText = "2023 Nextcloud GmbH" +SPDX-FileCopyrightText = "2024 Nextcloud GmbH" SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks" [[annotations]] diff --git a/image.png b/image.png deleted file mode 100644 index 714d467ed6c5761c45087a8dee6f251cb4a9670a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 493199 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
    x~U%kKfqVesM^@$u(Z!-tBqi!fU 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�sCA_=?B*lBHYo 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∾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
    =lKv%zuZufXnmrnMBg{buLuM2K%& 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

    qJzLAQu}4>BT*IEpq}sW{N#{c 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

    w{?}_ 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 Date: Mon, 25 Nov 2024 15:57:42 +0100 Subject: [PATCH 082/135] Reenabled build on macOS. (#109) Certain APIs used were not available on macOS in general or the currently supported version per package manifest. I added necessary code switches to make the code at least build again. Co-authored-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 48 +++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index d24fe5ed..921ada6a 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -49,7 +49,11 @@ public class NKSession { /// Session Alamofire let configuration = URLSessionConfiguration.af.default configuration.requestCachePolicy = requestCachePolicy - configuration.multipathServiceType = .handover + + #if os(iOS) || targetEnvironment(macCatalyst) + configuration.multipathServiceType = .handover + #endif + configuration.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionData = Alamofire.Session(configuration: configuration, delegate: NextcloudKitSessionDelegate(nkCommonInstance: NextcloudKit.shared.nkCommonInstance), @@ -61,29 +65,49 @@ public class NKSession { /// Session Download Background let configurationDownloadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionDownloadBackground) configurationDownloadBackground.allowsCellularAccess = true - configurationDownloadBackground.sessionSendsLaunchEvents = true + + if #available(macOS 11, *) { + configurationDownloadBackground.sessionSendsLaunchEvents = true + } + configurationDownloadBackground.isDiscretionary = false configurationDownloadBackground.httpMaximumConnectionsPerHost = 5 configurationDownloadBackground.requestCachePolicy = requestCachePolicy - configurationDownloadBackground.multipathServiceType = .handover + + #if os(iOS) || targetEnvironment(macCatalyst) + configurationDownloadBackground.multipathServiceType = .handover + #endif + configurationDownloadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionDownloadBackground = URLSession(configuration: configurationDownloadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) /// Session Upload Background let configurationUploadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackground) configurationUploadBackground.allowsCellularAccess = true - configurationUploadBackground.sessionSendsLaunchEvents = true + + if #available(macOS 11, *) { + configurationUploadBackground.sessionSendsLaunchEvents = true + } + configurationUploadBackground.isDiscretionary = false configurationUploadBackground.httpMaximumConnectionsPerHost = 5 configurationUploadBackground.requestCachePolicy = requestCachePolicy - configurationUploadBackground.multipathServiceType = .handover + + #if os(iOS) || targetEnvironment(macCatalyst) + configurationUploadBackground.multipathServiceType = .handover + #endif + configurationUploadBackground.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackground = URLSession(configuration: configurationUploadBackground, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) /// Session Upload Background WWan let configurationUploadBackgroundWWan = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackgroundWWan) configurationUploadBackgroundWWan.allowsCellularAccess = false - configurationUploadBackgroundWWan.sessionSendsLaunchEvents = true + + if #available(macOS 11, *) { + configurationUploadBackgroundWWan.sessionSendsLaunchEvents = true + } + configurationUploadBackgroundWWan.isDiscretionary = false configurationUploadBackgroundWWan.httpMaximumConnectionsPerHost = 5 configurationUploadBackgroundWWan.requestCachePolicy = requestCachePolicy @@ -93,12 +117,20 @@ public class NKSession { /// Session Upload Background Extension let configurationUploadBackgroundExt = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionUploadBackgroundExt + UUID().uuidString) configurationUploadBackgroundExt.allowsCellularAccess = true - configurationUploadBackgroundExt.sessionSendsLaunchEvents = true + + if #available(macOS 11, *) { + configurationUploadBackgroundExt.sessionSendsLaunchEvents = true + } + configurationUploadBackgroundExt.isDiscretionary = false configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = 5 configurationUploadBackgroundExt.requestCachePolicy = requestCachePolicy configurationUploadBackgroundExt.sharedContainerIdentifier = groupIdentifier - configurationUploadBackgroundExt.multipathServiceType = .handover + + #if os(iOS) || targetEnvironment(macCatalyst) + configurationUploadBackgroundExt.multipathServiceType = .handover + #endif + configurationUploadBackgroundExt.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackgroundExt = URLSession(configuration: configurationUploadBackgroundExt, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) } From 4e0a7fa39db7a5a5b86e8f56ae4a3ac679307183 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Wed, 4 Dec 2024 11:50:21 +0100 Subject: [PATCH 083/135] Track ocId in NKTrash items (#110) Signed-off-by: Claudio Cambra --- Sources/NextcloudKit/Models/NKTrash.swift | 1 + Sources/NextcloudKit/NKDataFileXML.swift | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Sources/NextcloudKit/Models/NKTrash.swift b/Sources/NextcloudKit/Models/NKTrash.swift index eb002578..7e554652 100644 --- a/Sources/NextcloudKit/Models/NKTrash.swift +++ b/Sources/NextcloudKit/Models/NKTrash.swift @@ -6,6 +6,7 @@ import Foundation public class NKTrash: NSObject { + public var ocId = "" public var contentType = "" public var date = Date() public var directory: Bool = false diff --git a/Sources/NextcloudKit/NKDataFileXML.swift b/Sources/NextcloudKit/NKDataFileXML.swift index 2225fce5..c01d18e8 100644 --- a/Sources/NextcloudKit/NKDataFileXML.swift +++ b/Sources/NextcloudKit/NKDataFileXML.swift @@ -642,6 +642,10 @@ class NKDataFileXML: NSObject { file.contentType = "httpd/unix-directory" } + if let ocId = propstat["d:prop", "oc:id"].text { + file.ocId = ocId + } + if let fileId = propstat["d:prop", "oc:fileid"].text { file.fileId = fileId } From c754b599f57c479572d17813145b2839bdc8a850 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 5 Dec 2024 16:31:07 +0100 Subject: [PATCH 084/135] Dav pagination (#111) * add paginate Signed-off-by: Marino Faggiana * StandardHeaders Signed-off-by: Marino Faggiana * code Signed-off-by: Marino Faggiana * fix Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 15 +++++++++++++++ Sources/NextcloudKit/NKRequestOptions.swift | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 2b0650fa..87107a2d 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -411,6 +411,21 @@ public class NKCommon: NSObject { for (key, value) in options?.customHeader ?? [:] { headers.update(name: key, value: value) } + // Paginate + if let options { + if options.paginate { + headers.update(name: "X-NC-Paginate", value: "true") + } + if let paginateCount = options.paginateCount { + headers.update(name: "X-NC-Paginate-Count", value: "\(paginateCount)") + } + if let paginateOffset = options.paginateOffset { + headers.update(name: "X-NC-Paginate-Offset", value: "\(paginateOffset)") + } + if let paginateToken = options.paginateToken { + headers.update(name: "X-NC-Paginate-Token", value: paginateToken) + } + } return headers } diff --git a/Sources/NextcloudKit/NKRequestOptions.swift b/Sources/NextcloudKit/NKRequestOptions.swift index e7bdeecd..4b586a86 100644 --- a/Sources/NextcloudKit/NKRequestOptions.swift +++ b/Sources/NextcloudKit/NKRequestOptions.swift @@ -16,6 +16,10 @@ public class NKRequestOptions: NSObject { var taskDescription: String? var createProperties: [NKProperties]? var removeProperties: [NKProperties] + var paginate: Bool + var paginateToken: String? + var paginateOffset: Int? + var paginateCount: Int? var queue: DispatchQueue public init(endpoint: String? = nil, @@ -28,6 +32,10 @@ public class NKRequestOptions: NSObject { taskDescription: String? = nil, createProperties: [NKProperties]? = nil, removeProperties: [NKProperties] = [], + paginate: Bool = false, + paginateToken: String? = nil, + paginateOffset: Int? = nil, + paginateCount: Int? = nil, queue: DispatchQueue = .main) { self.endpoint = endpoint @@ -40,6 +48,10 @@ public class NKRequestOptions: NSObject { self.taskDescription = taskDescription self.createProperties = createProperties self.removeProperties = removeProperties + self.paginate = paginate + self.paginateToken = paginateToken + self.paginateOffset = paginateOffset + self.paginateCount = paginateCount self.queue = queue } } From 7b09fa2c9e16fe3d6ab0a433874e23b2ce971065 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 11 Dec 2024 10:58:27 +0100 Subject: [PATCH 085/135] Terms of Service (#112) * tos Signed-off-by: Marino Faggiana * getTermsOfService Signed-off-by: Marino Faggiana * getTermsOfService Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana * code Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana * signTermsOfService Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana * cleaning Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- .../Models/NKTermsOfService.swift | 72 +++++++++++++++ .../NextcloudKit+TermsOfService.swift | 89 +++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 Sources/NextcloudKit/Models/NKTermsOfService.swift create mode 100644 Sources/NextcloudKit/NextcloudKit+TermsOfService.swift diff --git a/Sources/NextcloudKit/Models/NKTermsOfService.swift b/Sources/NextcloudKit/Models/NKTermsOfService.swift new file mode 100644 index 00000000..5d8b41ff --- /dev/null +++ b/Sources/NextcloudKit/Models/NKTermsOfService.swift @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public class NKTermsOfService: NSObject { + public var meta: Meta? + public var data: OCSData? + + public override init() { + super.init() + } + + public func loadFromJSON(_ jsonData: Data) -> Bool { + do { + let decodedResponse = try JSONDecoder().decode(OCSResponse.self, from: jsonData) + self.meta = decodedResponse.ocs.meta + self.data = decodedResponse.ocs.data + return true + } catch { + print("decode error:", error) + return false + } + } + + public func getTerms() -> [Term]? { + return data?.terms + } + + public func getLanguages() -> [String: String]? { + return data?.languages + } + + public func hasUserSigned() -> Bool { + return data?.hasSigned ?? false + } + + public func getMeta() -> Meta? { + return meta + } + + // MARK: - Codable + private class OCSResponse: Codable { + let ocs: OCS + } + + private class OCS: Codable { + let meta: Meta + let data: OCSData + } + + public class Meta: Codable { + public let status: String + public let statuscode: Int + public let message: String + } + + public class OCSData: Codable { + public let terms: [Term] + public let languages: [String: String] + public let hasSigned: Bool + } + + public class Term: Codable { + public let id: Int + public let countryCode: String + public let languageCode: String + public let body: String + public let renderedBody: String + } +} diff --git a/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift b/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift new file mode 100644 index 00000000..1ea602e3 --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Alamofire +import SwiftyJSON + +public extension NextcloudKit { + func getTermsOfService(account: String, + options: NKRequestOptions = NKRequestOptions(), + request: @escaping (DataRequest?) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ tos: NKTermsOfService?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + let endpoint = "ocs/v2.php/apps/terms_of_service/terms" + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + switch response.result { + case .success(let jsonData): + let tos = NKTermsOfService() + if tos.loadFromJSON(jsonData), let meta = tos.getMeta() { + if meta.statuscode == 200 { + options.queue.async { completion(account, tos, response, .success) } + } else { + options.queue.async { completion(account, tos, response, NKError(errorCode: meta.statuscode, errorDescription: meta.message, responseData: jsonData)) } + } + } else { + options.queue.async { completion(account, nil, response, .invalidData) } + } + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, nil, response, error) } + } + } + options.queue.async { request(tosRequest) } + } + + func signTermsOfService(termId: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + let endpoint = "ocs/v2.php/apps/terms_of_service/sign" + var urlRequest: URLRequest + /// + options.contentType = "application/json" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, .urlError) } + } + + do { + try urlRequest = URLRequest(url: url, method: .post, headers: headers) + let parameters = "{\"termId\":\"" + termId + "\"}" + urlRequest.httpBody = parameters.data(using: .utf8) + } catch { + return options.queue.async { completion(account, nil, NKError(error: error)) } + } + + nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, response, error) } + case .success: + options.queue.async { completion(account, response, .success) } + } + } + } +} From dd022b98c98f5e7dcb45af55ff0d839b87463399 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Fri, 20 Dec 2024 10:46:19 +0100 Subject: [PATCH 086/135] Added capabilities to manage share download limits. (#107) - Requesting share download limit capability of files_downloadlimit app. - Augmented WebDAV metadata requests and responses with optional share download limits. - Extended NextcloudKit with methods to manage share download limits via OCS. Signed-off-by: Iva Horn --- .../NextcloudKit/Models/NKDownloadLimit.swift | 33 +++++++ Sources/NextcloudKit/Models/NKFile.swift | 6 ++ .../NextcloudKit/Models/NKProperties.swift | 9 ++ Sources/NextcloudKit/NKDataFileXML.swift | 18 +++- .../NextcloudKit+ShareDownloadLimit.swift | 90 +++++++++++++++++++ 5 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 Sources/NextcloudKit/Models/NKDownloadLimit.swift create mode 100644 Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift diff --git a/Sources/NextcloudKit/Models/NKDownloadLimit.swift b/Sources/NextcloudKit/Models/NKDownloadLimit.swift new file mode 100644 index 00000000..b3722a4c --- /dev/null +++ b/Sources/NextcloudKit/Models/NKDownloadLimit.swift @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +/// +/// Data model for a download limit as returned in the WebDAV response for file properties. +/// +/// Each relates to a share of a file and is optionally provided by the [Files Download Limit](https://github.com/nextcloud/files_downloadlimit) app for Nextcloud server. +/// +public class NKDownloadLimit: NSObject { + /// + /// The number of downloads which already happened. + /// + public let count: Int + + /// + /// Total number of allowed downloas. + /// + public let limit: Int + + /// + /// The token identifying the related share. + /// + public let token: String + + init(count: Int, limit: Int, token: String) { + self.count = count + self.limit = limit + self.token = token + } +} diff --git a/Sources/NextcloudKit/Models/NKFile.swift b/Sources/NextcloudKit/Models/NKFile.swift index 1da7b9d5..dde57ee8 100644 --- a/Sources/NextcloudKit/Models/NKFile.swift +++ b/Sources/NextcloudKit/Models/NKFile.swift @@ -16,6 +16,12 @@ public class NKFile: NSObject { public var date = Date() public var directory: Bool = false public var downloadURL = "" + + /// + /// Download limits for shares of this file. + /// + public var downloadLimits = [NKDownloadLimit]() + public var e2eEncrypted: Bool = false public var etag = "" public var favorite: Bool = false diff --git a/Sources/NextcloudKit/Models/NKProperties.swift b/Sources/NextcloudKit/Models/NKProperties.swift index 929d4473..f5482f58 100644 --- a/Sources/NextcloudKit/Models/NKProperties.swift +++ b/Sources/NextcloudKit/Models/NKProperties.swift @@ -3,9 +3,18 @@ // SPDX-FileCopyrightText: 2023 Claudio Cambra // SPDX-License-Identifier: GPL-3.0-or-later +/// +/// Definition of properties used for decoding in ``NKDataFileXML``. +/// public enum NKProperties: String, CaseIterable { /// DAV case displayname = "" + + /// + /// Download limits for shares of a file as optionally provided by the [Files Download Limit](https://github.com/nextcloud/files_downloadlimit) app for Nextcloud server. + /// + case downloadLimit = "" + case getlastmodified = "" case getetag = "" case getcontenttype = "" diff --git a/Sources/NextcloudKit/NKDataFileXML.swift b/Sources/NextcloudKit/NKDataFileXML.swift index c01d18e8..eef09d65 100644 --- a/Sources/NextcloudKit/NKDataFileXML.swift +++ b/Sources/NextcloudKit/NKDataFileXML.swift @@ -560,7 +560,23 @@ class NKDataFileXML: NSObject { } file.placePhotos = propstat["d:prop", "nc:metadata-photos-place"].text - + + for downloadLimit in propstat["d:prop", "nc:share-download-limits", "nc:share-download-limit"] { + guard let token = downloadLimit["nc:token"].text else { + continue + } + + guard let limit = downloadLimit["nc:limit"].int else { + continue + } + + guard let count = downloadLimit["nc:count"].int else { + continue + } + + file.downloadLimits.append(NKDownloadLimit(count: count, limit: limit, token: token)) + } + let results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) file.contentType = results.mimeType diff --git a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift new file mode 100644 index 00000000..448e86ec --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import Alamofire +import Foundation + +public extension NextcloudKit { + private func makeEndpoint(with token: String) -> String { + "ocs/v2.php/apps/files_downloadlimit/api/v1/\(token)/limit" + } + + func removeShareDownloadLimit(account: String, token: String, completion: @escaping (_ error: NKError) -> Void) { + let endpoint = makeEndpoint(with: token) + let options = NKRequestOptions() + + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + completion(.urlError) + } + } + + nkSession + .sessionData + .request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil) + .validate(statusCode: 200..<300) + .response(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + + options.queue.async { + completion(error) + } + case .success: + options.queue.async { + completion(.success) + } + } + } + } + + func setShareDownloadLimit(account: String, token: String, limit: Int, completion: @escaping (_ error: NKError) -> Void) { + let endpoint = makeEndpoint(with: token) + let options = NKRequestOptions() + options.contentType = "application/json" + + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options), + var urlRequest = try? URLRequest(url: url, method: .put, headers: headers) else { + return options.queue.async { + completion(.urlError) + } + } + + urlRequest.httpBody = try? JSONEncoder().encode([ + "limit": limit + ]) + + nkSession + .sessionData + .request(urlRequest) + .validate(statusCode: 200..<300) + .response(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + + options.queue.async { + completion(error) + } + case .success: + options.queue.async { + completion(.success) + } + } + } + } +} From f7985178e8554dcb837b915c9eb4b3696dcc0956 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 30 Dec 2024 12:17:48 +0100 Subject: [PATCH 087/135] Recommended files (#115) * cod Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- .../Models/NKRecommendedFiles.swift | 74 +++++++++++++++++++ .../NextcloudKit+RecommendedFiles.swift | 53 +++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 Sources/NextcloudKit/Models/NKRecommendedFiles.swift create mode 100644 Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift diff --git a/Sources/NextcloudKit/Models/NKRecommendedFiles.swift b/Sources/NextcloudKit/Models/NKRecommendedFiles.swift new file mode 100644 index 00000000..1e72ee04 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKRecommendedFiles.swift @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import SwiftyXMLParser + +public class NKRecommendation { + public var id: String + public var timestamp: Date? + public var name: String + public var directory: String + public var extensionType: String + public var mimeType: String + public var hasPreview: Bool + public var reason: String + + init(id: String, timestamp: Date?, name: String, directory: String, extensionType: String, mimeType: String, hasPreview: Bool, reason: String) { + self.id = id + self.timestamp = timestamp + self.name = name + self.directory = directory + self.extensionType = extensionType + self.mimeType = mimeType + self.hasPreview = hasPreview + self.reason = reason + } +} + +class XMLToRecommendationParser { + func parse(xml: String) -> [NKRecommendation]? { + guard let data = xml.data(using: .utf8) else { return nil } + let xml = XML.parse(data) + + // Parsing "enabled" + guard let enabledString = xml["ocs", "data", "enabled"].text, + Bool(enabledString == "1") + else { + return nil + } + + // Parsing "recommendations" + var recommendations: [NKRecommendation] = [] + let elements = xml["ocs", "data", "recommendations", "element"] + + for element in elements { + let id = element["id"].text ?? "" + var timestamp: Date? + if let timestampDouble = element["timestamp"].double, timestampDouble > 0 { + timestamp = Date(timeIntervalSince1970: timestampDouble) + } + let name = element["name"].text ?? "" + let directory = element["directory"].text ?? "" + let extensionType = element["extension"].text ?? "" + let mimeType = element["mimeType"].text ?? "" + let hasPreview = element["hasPreview"].text == "1" + let reason = element["reason"].text ?? "" + + let recommendation = NKRecommendation( + id: id, + timestamp: timestamp, + name: name, + directory: directory, + extensionType: extensionType, + mimeType: mimeType, + hasPreview: hasPreview, + reason: reason + ) + recommendations.append(recommendation) + } + + return recommendations + } +} diff --git a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift new file mode 100644 index 00000000..172fb58c --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Alamofire +import SwiftyJSON + +public extension NextcloudKit { + func getRecommendedFiles(account: String, + options: NKRequestOptions = NKRequestOptions(), + request: @escaping (DataRequest?) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ recommendations: [NKRecommendation]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + let endpoint = "ocs/v2.php/apps/recommendations/api/v1/recommendations" + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + switch response.result { + case .success(let data): + if let xmlString = String(data: data, encoding: .utf8) { + let parser = XMLToRecommendationParser() + if let recommendations = parser.parse(xml: xmlString) { + options.queue.async { completion(account, recommendations, response, .success) } + } else { + options.queue.async { completion(account, nil, response, .xmlError) } + } + } else { + options.queue.async { completion(account, nil, response, .xmlError) } + } + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + completion(account, nil, response, error) + } + } + } + options.queue.async { request(tosRequest) } + } +} From 92ca88f54f91ce277cf4cfb323bc3044a9562bb5 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 2 Jan 2025 12:24:50 +0100 Subject: [PATCH 088/135] improvements Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/Models/NKRecommendedFiles.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/Models/NKRecommendedFiles.swift b/Sources/NextcloudKit/Models/NKRecommendedFiles.swift index 1e72ee04..186b1d50 100644 --- a/Sources/NextcloudKit/Models/NKRecommendedFiles.swift +++ b/Sources/NextcloudKit/Models/NKRecommendedFiles.swift @@ -5,7 +5,7 @@ import Foundation import SwiftyXMLParser -public class NKRecommendation { +public class NKRecommendation: NSObject { public var id: String public var timestamp: Date? public var name: String @@ -15,7 +15,7 @@ public class NKRecommendation { public var hasPreview: Bool public var reason: String - init(id: String, timestamp: Date?, name: String, directory: String, extensionType: String, mimeType: String, hasPreview: Bool, reason: String) { + public init(id: String, timestamp: Date?, name: String, directory: String, extensionType: String, mimeType: String, hasPreview: Bool, reason: String) { self.id = id self.timestamp = timestamp self.name = name From bdfb63e480127e4d79f6f6d0fcf7d29b1c324f71 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Fri, 10 Jan 2025 21:13:51 +0800 Subject: [PATCH 089/135] Allow setting custom destinations in chunked upload (#116) * Allow setting a different destination file name from local file name in chunked upload Using macOS File Provider APIs we rely on the system to provide us with a URL pointing the an item's local modified contents. This URL last path component is a UUID and does not represent the expected file name of the item. The current API of uploadChunk presents a problem because it assumes the local content file's filename is the same as what will eventually be uploaded to the server. This commit addresses the issue by allowing users of this function to provide a destination file name which will replace the provided local filename, if used. Signed-off-by: Claudio Cambra * Make file chunks output directory customisable Currently the chunking procedure produces file chunks within the directory of the input file. However, this fails in cases where the directory is read-only. This can be fixed by allowing the chunked files output directory to be changed Signed-off-by: Claudio Cambra --------- Signed-off-by: Claudio Cambra --- Sources/NextcloudKit/NextcloudKit+Upload.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index a8d3ffa7..6438f082 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -96,7 +96,9 @@ public extension NextcloudKit { /// - chunkSizeInMB: Size in MB of chunk func uploadChunk(directory: String, + fileChunksOutputDirectory: String? = nil, fileName: String, + destinationFileName: String? = nil, date: Date?, creationDate: Date?, serverUrl: String, @@ -119,7 +121,7 @@ public extension NextcloudKit { } let fileNameLocalSize = self.nkCommonInstance.getFileSize(filePath: directory + "/" + fileName) let serverUrlChunkFolder = nkSession.urlBase + "/" + nkSession.dav + "/uploads/" + nkSession.userId + "/" + chunkFolder - let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl, urlBase: nkSession.urlBase, userId: nkSession.userId) + "/" + fileName + let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl, urlBase: nkSession.urlBase, userId: nkSession.userId) + "/" + (destinationFileName ?? fileName) if options.customHeader == nil { options.customHeader = [:] } @@ -175,7 +177,8 @@ public extension NextcloudKit { var uploadNKError = NKError() var uploadAFError: AFError? - self.nkCommonInstance.chunkedFile(inputDirectory: directory, outputDirectory: directory, fileName: fileName, chunkSize: chunkSize, filesChunk: filesChunk) { num in + let outputDirectory = fileChunksOutputDirectory ?? directory + self.nkCommonInstance.chunkedFile(inputDirectory: directory, outputDirectory: outputDirectory, fileName: fileName, chunkSize: chunkSize, filesChunk: filesChunk) { num in numChunks(num) } counterChunk: { counter in counterChunk(counter) @@ -190,7 +193,7 @@ public extension NextcloudKit { for fileChunk in filesChunk { let serverUrlFileName = serverUrlChunkFolder + "/" + fileChunk.fileName - let fileNameLocalPath = directory + "/" + fileChunk.fileName + let fileNameLocalPath = outputDirectory + "/" + fileChunk.fileName let fileSize = self.nkCommonInstance.getFileSize(filePath: fileNameLocalPath) if fileSize == 0 { // The file could not be sent From 19f484c639bb09071c0823479f2594115d7567de Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jan 2025 11:34:36 +0100 Subject: [PATCH 090/135] added httpMaximumConnectionsPerHost Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index 921ada6a..cf1b62a2 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -14,6 +14,7 @@ public class NKSession { public var userAgent: String public var nextcloudVersion: Int public let groupIdentifier: String + public let httpMaximumConnectionsPerHost: Int public let requestCachePolicy: URLRequest.CachePolicy public let dav: String = "remote.php/dav" public var internalTypeIdentifiers: [NKCommon.UTTypeConformsToServer] = [] @@ -31,6 +32,7 @@ public class NKSession { userAgent: String, nextcloudVersion: Int, groupIdentifier: String, + httpMaximumConnectionsPerHost: Int = 5, requestCachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy) { self.urlBase = urlBase self.user = user @@ -40,6 +42,7 @@ public class NKSession { self.userAgent = userAgent self.nextcloudVersion = nextcloudVersion self.groupIdentifier = groupIdentifier + self.httpMaximumConnectionsPerHost = httpMaximumConnectionsPerHost self.requestCachePolicy = requestCachePolicy let backgroundSessionDelegate = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance) @@ -71,7 +74,7 @@ public class NKSession { } configurationDownloadBackground.isDiscretionary = false - configurationDownloadBackground.httpMaximumConnectionsPerHost = 5 + configurationDownloadBackground.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHost configurationDownloadBackground.requestCachePolicy = requestCachePolicy #if os(iOS) || targetEnvironment(macCatalyst) @@ -90,7 +93,7 @@ public class NKSession { } configurationUploadBackground.isDiscretionary = false - configurationUploadBackground.httpMaximumConnectionsPerHost = 5 + configurationUploadBackground.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHost configurationUploadBackground.requestCachePolicy = requestCachePolicy #if os(iOS) || targetEnvironment(macCatalyst) @@ -109,7 +112,7 @@ public class NKSession { } configurationUploadBackgroundWWan.isDiscretionary = false - configurationUploadBackgroundWWan.httpMaximumConnectionsPerHost = 5 + configurationUploadBackgroundWWan.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHost configurationUploadBackgroundWWan.requestCachePolicy = requestCachePolicy configurationUploadBackgroundWWan.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackgroundWWan = URLSession(configuration: configurationUploadBackgroundWWan, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) @@ -123,7 +126,7 @@ public class NKSession { } configurationUploadBackgroundExt.isDiscretionary = false - configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = 5 + configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHost configurationUploadBackgroundExt.requestCachePolicy = requestCachePolicy configurationUploadBackgroundExt.sharedContainerIdentifier = groupIdentifier From 3c394ed0dc9ccdabd3ddf5169935c0d811ab3561 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jan 2025 11:46:58 +0100 Subject: [PATCH 091/135] httpMaximumConnectionsPerHost Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index cf1b62a2..61f3535d 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -15,6 +15,8 @@ public class NKSession { public var nextcloudVersion: Int public let groupIdentifier: String public let httpMaximumConnectionsPerHost: Int + public let httpMaximumConnectionsPerHostInDownload: Int + public let httpMaximumConnectionsPerHostInUpload: Int public let requestCachePolicy: URLRequest.CachePolicy public let dav: String = "remote.php/dav" public var internalTypeIdentifiers: [NKCommon.UTTypeConformsToServer] = [] @@ -32,7 +34,9 @@ public class NKSession { userAgent: String, nextcloudVersion: Int, groupIdentifier: String, - httpMaximumConnectionsPerHost: Int = 5, + httpMaximumConnectionsPerHost: Int = 6, + httpMaximumConnectionsPerHostInDownload: Int = 5, + httpMaximumConnectionsPerHostInUpload: Int = 5, requestCachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy) { self.urlBase = urlBase self.user = user @@ -43,6 +47,8 @@ public class NKSession { self.nextcloudVersion = nextcloudVersion self.groupIdentifier = groupIdentifier self.httpMaximumConnectionsPerHost = httpMaximumConnectionsPerHost + self.httpMaximumConnectionsPerHostInDownload = httpMaximumConnectionsPerHostInDownload + self.httpMaximumConnectionsPerHostInUpload = httpMaximumConnectionsPerHostInUpload self.requestCachePolicy = requestCachePolicy let backgroundSessionDelegate = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance) @@ -52,6 +58,7 @@ public class NKSession { /// Session Alamofire let configuration = URLSessionConfiguration.af.default configuration.requestCachePolicy = requestCachePolicy + configuration.httpMaximumConnectionsPerHost = httpMaximumConnectionsPerHost #if os(iOS) || targetEnvironment(macCatalyst) configuration.multipathServiceType = .handover @@ -74,7 +81,7 @@ public class NKSession { } configurationDownloadBackground.isDiscretionary = false - configurationDownloadBackground.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHost + configurationDownloadBackground.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHostInDownload configurationDownloadBackground.requestCachePolicy = requestCachePolicy #if os(iOS) || targetEnvironment(macCatalyst) @@ -93,7 +100,7 @@ public class NKSession { } configurationUploadBackground.isDiscretionary = false - configurationUploadBackground.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHost + configurationUploadBackground.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHostInUpload configurationUploadBackground.requestCachePolicy = requestCachePolicy #if os(iOS) || targetEnvironment(macCatalyst) @@ -112,7 +119,7 @@ public class NKSession { } configurationUploadBackgroundWWan.isDiscretionary = false - configurationUploadBackgroundWWan.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHost + configurationUploadBackgroundWWan.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHostInUpload configurationUploadBackgroundWWan.requestCachePolicy = requestCachePolicy configurationUploadBackgroundWWan.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackgroundWWan = URLSession(configuration: configurationUploadBackgroundWWan, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) @@ -126,7 +133,7 @@ public class NKSession { } configurationUploadBackgroundExt.isDiscretionary = false - configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHost + configurationUploadBackgroundExt.httpMaximumConnectionsPerHost = self.httpMaximumConnectionsPerHostInUpload configurationUploadBackgroundExt.requestCachePolicy = requestCachePolicy configurationUploadBackgroundExt.sharedContainerIdentifier = groupIdentifier From f79861556ae3f17ed5a10bfa404b55891420f2a9 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jan 2025 11:57:48 +0100 Subject: [PATCH 092/135] appendSession Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 6 +++--- Sources/NextcloudKit/NextcloudKit.swift | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index 61f3535d..0c875904 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -34,9 +34,9 @@ public class NKSession { userAgent: String, nextcloudVersion: Int, groupIdentifier: String, - httpMaximumConnectionsPerHost: Int = 6, - httpMaximumConnectionsPerHostInDownload: Int = 5, - httpMaximumConnectionsPerHostInUpload: Int = 5, + httpMaximumConnectionsPerHost: Int, + httpMaximumConnectionsPerHostInDownload: Int, + httpMaximumConnectionsPerHostInUpload: Int, requestCachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy) { self.urlBase = urlBase self.user = user diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index e6006501..beff29c4 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -61,11 +61,14 @@ open class NextcloudKit { password: String, userAgent: String, nextcloudVersion: Int, + httpMaximumConnectionsPerHost: Int = 6, + httpMaximumConnectionsPerHostInDownload: Int = 5, + httpMaximumConnectionsPerHostInUpload: Int = 5, groupIdentifier: String) { if nkCommonInstance.nksessions.filter({ $0.account == account }).first != nil { return updateSession(account: account, urlBase: urlBase, userId: userId, password: password, userAgent: userAgent, nextcloudVersion: nextcloudVersion) } - let nkSession = NKSession(urlBase: urlBase, user: user, userId: userId, password: password, account: account, userAgent: userAgent, nextcloudVersion: nextcloudVersion, groupIdentifier: groupIdentifier) + let nkSession = NKSession(urlBase: urlBase, user: user, userId: userId, password: password, account: account, userAgent: userAgent, nextcloudVersion: nextcloudVersion, groupIdentifier: groupIdentifier, httpMaximumConnectionsPerHost: httpMaximumConnectionsPerHost, httpMaximumConnectionsPerHostInDownload: httpMaximumConnectionsPerHostInDownload, httpMaximumConnectionsPerHostInUpload: httpMaximumConnectionsPerHostInUpload) nkCommonInstance.nksessions.append(nkSession) } From 670e3cd2fe1b96e550366f641aba1a49283ca695 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 21 Jan 2025 12:43:32 +0100 Subject: [PATCH 093/135] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index beff29c4..28fe7edd 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -62,8 +62,8 @@ open class NextcloudKit { userAgent: String, nextcloudVersion: Int, httpMaximumConnectionsPerHost: Int = 6, - httpMaximumConnectionsPerHostInDownload: Int = 5, - httpMaximumConnectionsPerHostInUpload: Int = 5, + httpMaximumConnectionsPerHostInDownload: Int = 6, + httpMaximumConnectionsPerHostInUpload: Int = 6, groupIdentifier: String) { if nkCommonInstance.nksessions.filter({ $0.account == account }).first != nil { return updateSession(account: account, urlBase: urlBase, userId: userId, password: password, userAgent: userAgent, nextcloudVersion: nextcloudVersion) From 935d4bc9a136e0876a8b68ba89d5afc08d9e9f61 Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Tue, 21 Jan 2025 15:11:23 +0100 Subject: [PATCH 094/135] Added feature to get download limits explicitly via OCS API. Signed-off-by: Iva Horn --- Sources/NextcloudKit/Models/NKFile.swift | 2 +- Sources/NextcloudKit/NKSession.swift | 2 +- .../NextcloudKit+ShareDownloadLimit.swift | 70 +++++++++++++++++++ Sources/NextcloudKit/NextcloudKit.swift | 15 +++- 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/Sources/NextcloudKit/Models/NKFile.swift b/Sources/NextcloudKit/Models/NKFile.swift index dde57ee8..422b1477 100644 --- a/Sources/NextcloudKit/Models/NKFile.swift +++ b/Sources/NextcloudKit/Models/NKFile.swift @@ -21,7 +21,7 @@ public class NKFile: NSObject { /// Download limits for shares of this file. /// public var downloadLimits = [NKDownloadLimit]() - + public var e2eEncrypted: Bool = false public var etag = "" public var favorite: Bool = false diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index 0c875904..ba019afc 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -140,7 +140,7 @@ public class NKSession { #if os(iOS) || targetEnvironment(macCatalyst) configurationUploadBackgroundExt.multipathServiceType = .handover #endif - + configurationUploadBackgroundExt.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionUploadBackgroundExt = URLSession(configuration: configurationUploadBackgroundExt, delegate: backgroundSessionDelegate, delegateQueue: OperationQueue.main) } diff --git a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift index 448e86ec..316c540b 100644 --- a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift +++ b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift @@ -4,12 +4,82 @@ import Alamofire import Foundation +import SwiftyJSON public extension NextcloudKit { private func makeEndpoint(with token: String) -> String { "ocs/v2.php/apps/files_downloadlimit/api/v1/\(token)/limit" } + func getDownloadLimit(account: String, token: String, completion: @escaping (NKDownloadLimit?, NKError) -> Void) { + let endpoint = makeEndpoint(with: token) + let options = NKRequestOptions() + + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + completion(nil, .urlError) + } + } + + nkSession + .sessionData + .request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil) + .validate(statusCode: 200..<300) + .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + + options.queue.async { + completion(nil, error) + } + case .success(let jsonData): + let json = JSON(jsonData) + + guard json["ocs"]["meta"]["statuscode"].int == 200 else { + let error = NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode) + + options.queue.async { + completion(nil, error) + } + + return + } + + let count = json["ocs"]["data"]["count"] + let limit = json["ocs"]["data"]["limit"] + + guard count.type != .null else { + options.queue.async { + completion(nil, .success) + } + + return + } + + guard limit.type != .null else { + options.queue.async { + completion(nil, .success) + } + + return + } + + let downloadLimit = NKDownloadLimit(count: count.intValue, limit: limit.intValue, token: token) + + options.queue.async { + completion(downloadLimit, .success) + } + } + } + } + func removeShareDownloadLimit(account: String, token: String, completion: @escaping (_ error: NKError) -> Void) { let endpoint = makeEndpoint(with: token) let options = NKRequestOptions() diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 28fe7edd..ce7b8402 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -68,7 +68,20 @@ open class NextcloudKit { if nkCommonInstance.nksessions.filter({ $0.account == account }).first != nil { return updateSession(account: account, urlBase: urlBase, userId: userId, password: password, userAgent: userAgent, nextcloudVersion: nextcloudVersion) } - let nkSession = NKSession(urlBase: urlBase, user: user, userId: userId, password: password, account: account, userAgent: userAgent, nextcloudVersion: nextcloudVersion, groupIdentifier: groupIdentifier, httpMaximumConnectionsPerHost: httpMaximumConnectionsPerHost, httpMaximumConnectionsPerHostInDownload: httpMaximumConnectionsPerHostInDownload, httpMaximumConnectionsPerHostInUpload: httpMaximumConnectionsPerHostInUpload) + + let nkSession = NKSession( + urlBase: urlBase, + user: user, + userId: userId, + password: password, + account: account, + userAgent: userAgent, + nextcloudVersion: nextcloudVersion, + groupIdentifier: groupIdentifier, + httpMaximumConnectionsPerHost: httpMaximumConnectionsPerHost, + httpMaximumConnectionsPerHostInDownload: httpMaximumConnectionsPerHostInDownload, + httpMaximumConnectionsPerHostInUpload: httpMaximumConnectionsPerHostInUpload + ) nkCommonInstance.nksessions.append(nkSession) } From 34d7b9ec3b44d7fa9285342ede6906b491e3dbf5 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 23 Jan 2025 16:51:37 +0100 Subject: [PATCH 095/135] Update FileNameValidator.swift --- Sources/NextcloudKit/Utils/FileNameValidator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index 3c21ded5..52dc05c1 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -39,7 +39,7 @@ public class FileNameValidator { } public var fileForbiddenFileExtensionError: NKError { - let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: ".\"%@\" is a forbidden file extension.", comment: "") + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: "\".%@\" is a forbidden file extension.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } From 9924a988f90e7896caed10e325d0bf8278aea6a8 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Fri, 31 Jan 2025 17:44:29 +0800 Subject: [PATCH 096/135] Make NextcloudKit Swift 6 compatible (#119) * Define NKDownloadLimit as a sendable struct Signed-off-by: Claudio Cambra * Define NKFile as a sendable struct Signed-off-by: Claudio Cambra * Define NKSession as a sendable struct Signed-off-by: Claudio Cambra f Signed-off-by: Claudio Cambra * Define UTTypeConformsToServer as a sendable struct Signed-off-by: Claudio Cambra * Define NKError as a sendable, equatable struct Signed-off-by: Claudio Cambra * Convert ThreadSafeArray into a sendable struct This also modifies the behaviour of its methods to be sendable compliant by using locking instead of a dispatch queue Signed-off-by: Claudio Cambra * Make FileAutoRenamer Sendable compliant Signed-off-by: Claudio Cambra f autorenamer Signed-off-by: Claudio Cambra f autorenamer Signed-off-by: Claudio Cambra f autorenamer 3 Signed-off-by: Claudio Cambra * Make FileNameValidator Sendable compliant Signed-off-by: Claudio Cambra f filenamevalidator Signed-off-by: Claudio Cambra * Fix copyright header in NSLock extension Signed-off-by: Claudio Cambra * Use self createFolder rather than going for shared instance Signed-off-by: Claudio Cambra * Make NextcloudKitDelegate protocol sendable Signed-off-by: Claudio Cambra * Make NKBackground a final class Signed-off-by: Claudio Cambra * Make NextcloudKitSessionDelegate conform to sendable Signed-off-by: Claudio Cambra * Use the appropriate nkCommonInstance in NKSession initialiser Signed-off-by: Claudio Cambra * Make nkCommonInstance mutable in NextcloudKit Signed-off-by: Claudio Cambra * Only provide shared NextcloudKit instance on swift <6 Signed-off-by: Claudio Cambra f shared nckit Signed-off-by: Claudio Cambra * When using swift 6, use a task to retrieve screen scale on iOS Signed-off-by: Claudio Cambra * Make NKFileProperty a sendable struct Signed-off-by: Claudio Cambra f nkfileproperty Signed-off-by: Claudio Cambra * Convert NKCommon into a sendable struct This required modifying the caches into standard maps. Unfortunately NSCache is not sendable compliant. I have made the changes take effect only when building with Swift 6 Signed-off-by: Claudio Cambra * Fix whitespace handling in FileAutoRenamer Signed-off-by: Claudio Cambra * Remove leading dot for hidden file filenames in autorenamer This is seemingly expected by the autotests Signed-off-by: Claudio Cambra --------- Signed-off-by: Claudio Cambra Co-authored-by: Milen Pivchev --- .../Extensions/NSLock+Extension.swift | 14 ++ .../NextcloudKit/Models/NKDownloadLimit.swift | 2 +- Sources/NextcloudKit/Models/NKFile.swift | 2 +- .../NextcloudKit/Models/NKFileProperty.swift | 9 ++ Sources/NextcloudKit/NKCommon.swift | 62 ++++++-- Sources/NextcloudKit/NKDataFileXML.swift | 6 +- Sources/NextcloudKit/NKError.swift | 16 +- Sources/NextcloudKit/NKSession.swift | 19 +-- Sources/NextcloudKit/NextcloudKit+API.swift | 14 +- .../NextcloudKit/NextcloudKit+Upload.swift | 2 +- Sources/NextcloudKit/NextcloudKit.swift | 15 +- .../NextcloudKit/NextcloudKitBackground.swift | 2 +- .../NextcloudKitSessionDelegate.swift | 10 +- .../NextcloudKit/Utils/FileAutoRenamer.swift | 33 +++-- .../Utils/FileNameValidator.swift | 68 +++------ .../NextcloudKit/Utils/ThreadSafeArray.swift | 137 +++++------------- .../Common/BaseXCTestCase.swift | 18 ++- .../FileAutoRenamerUnitTests.swift | 46 +++--- .../FileNameValidatorUnitTests.swift | 18 +-- .../LoginUnitTests.swift | 21 ++- 20 files changed, 267 insertions(+), 247 deletions(-) create mode 100644 Sources/NextcloudKit/Extensions/NSLock+Extension.swift diff --git a/Sources/NextcloudKit/Extensions/NSLock+Extension.swift b/Sources/NextcloudKit/Extensions/NSLock+Extension.swift new file mode 100644 index 00000000..9cb023ac --- /dev/null +++ b/Sources/NextcloudKit/Extensions/NSLock+Extension.swift @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Claudio Cambra +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +extension NSLock { + @discardableResult + func perform(_ block: () throws -> T) rethrows -> T { + lock() + defer { unlock() } + return try block() + } +} diff --git a/Sources/NextcloudKit/Models/NKDownloadLimit.swift b/Sources/NextcloudKit/Models/NKDownloadLimit.swift index b3722a4c..adb5d54b 100644 --- a/Sources/NextcloudKit/Models/NKDownloadLimit.swift +++ b/Sources/NextcloudKit/Models/NKDownloadLimit.swift @@ -9,7 +9,7 @@ import Foundation /// /// Each relates to a share of a file and is optionally provided by the [Files Download Limit](https://github.com/nextcloud/files_downloadlimit) app for Nextcloud server. /// -public class NKDownloadLimit: NSObject { +public struct NKDownloadLimit: Sendable { /// /// The number of downloads which already happened. /// diff --git a/Sources/NextcloudKit/Models/NKFile.swift b/Sources/NextcloudKit/Models/NKFile.swift index 422b1477..a44e273d 100644 --- a/Sources/NextcloudKit/Models/NKFile.swift +++ b/Sources/NextcloudKit/Models/NKFile.swift @@ -5,7 +5,7 @@ import Foundation -public class NKFile: NSObject { +public struct NKFile: Sendable { public var account = "" public var classFile = "" public var commentsUnread: Bool = false diff --git a/Sources/NextcloudKit/Models/NKFileProperty.swift b/Sources/NextcloudKit/Models/NKFileProperty.swift index 1da8f4d5..1df5e71e 100644 --- a/Sources/NextcloudKit/Models/NKFileProperty.swift +++ b/Sources/NextcloudKit/Models/NKFileProperty.swift @@ -5,9 +5,18 @@ import Foundation +#if swift(<6.0) public class NKFileProperty: NSObject { public var classFile: String = "" public var iconName: String = "" public var name: String = "" public var ext: String = "" } +#else +public struct NKFileProperty: Sendable { + public var classFile: String = "" + public var iconName: String = "" + public var name: String = "" + public var ext: String = "" +} +#endif diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 87107a2d..ab7e2590 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -12,7 +12,7 @@ import MobileCoreServices import CoreServices #endif -public protocol NextcloudKitDelegate { +public protocol NextcloudKitDelegate: Sendable { func authenticationChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) @@ -29,7 +29,7 @@ public protocol NextcloudKitDelegate { func request(_ request: DataRequest, didParseResponse response: AFDataResponse) } -public class NKCommon: NSObject { +public struct NKCommon: Sendable { public var nksessions = ThreadSafeArray() public var delegate: NextcloudKitDelegate? @@ -82,7 +82,7 @@ public class NKCommon: NSObject { case xls = "xls" } - public struct UTTypeConformsToServer { + public struct UTTypeConformsToServer: Sendable { var typeIdentifier: String var classFile: String var editor: String @@ -91,9 +91,15 @@ public class NKCommon: NSObject { var account: String } +#if swift(<6.0) internal var utiCache = NSCache() internal var mimeTypeCache = NSCache() internal var filePropertiesCache = NSCache() +#else + internal var utiCache = [String: String]() + internal var mimeTypeCache = [String: String]() + internal var filePropertiesCache = [String: NKFileProperty]() +#endif internal var internalTypeIdentifiers = ThreadSafeArray() public var filenamePathLog: String = "" @@ -133,56 +139,77 @@ public class NKCommon: NSObject { // MARK: - Init - override init() { - super.init() - + init() { filenamePathLog = internalPathLog + "/" + internalFilenameLog } // MARK: - Type Identifier - public func clearInternalTypeIdentifier(account: String) { + mutating public func clearInternalTypeIdentifier(account: String) { internalTypeIdentifiers = internalTypeIdentifiers.filter({ $0.account != account }) } - public func addInternalTypeIdentifier(typeIdentifier: String, classFile: String, editor: String, iconName: String, name: String, account: String) { + mutating public func addInternalTypeIdentifier(typeIdentifier: String, classFile: String, editor: String, iconName: String, name: String, account: String) { if !internalTypeIdentifiers.contains(where: { $0.typeIdentifier == typeIdentifier && $0.editor == editor && $0.account == account}) { let newUTI = UTTypeConformsToServer(typeIdentifier: typeIdentifier, classFile: classFile, editor: editor, iconName: iconName, name: name, account: account) internalTypeIdentifiers.append(newUTI) } } - public func getInternalType(fileName: String, mimeType: String, directory: Bool, account: String) -> (mimeType: String, classFile: String, iconName: String, typeIdentifier: String, fileNameWithoutExt: String, ext: String) { + mutating public func getInternalType(fileName: String, mimeType: String, directory: Bool, account: String) -> (mimeType: String, classFile: String, iconName: String, typeIdentifier: String, fileNameWithoutExt: String, ext: String) { var ext = (fileName as NSString).pathExtension.lowercased() var mimeType = mimeType var classFile = "", iconName = "", typeIdentifier = "", fileNameWithoutExt = "" var inUTI: CFString? +#if swift(<6.0) if let cachedUTI = utiCache.object(forKey: ext as NSString) { inUTI = cachedUTI - } else { + } +#else + if let cachedUTI = utiCache[ext] { + inUTI = cachedUTI as CFString + } +#endif + if inUTI == nil { if let unmanagedFileUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil) { inUTI = unmanagedFileUTI.takeRetainedValue() if let inUTI { +#if swift(<6.0) utiCache.setObject(inUTI, forKey: ext as NSString) +#else + utiCache[ext] = inUTI as String +#endif } } } - if let inUTI = inUTI { + if let inUTI { typeIdentifier = inUTI as String fileNameWithoutExt = (fileName as NSString).deletingPathExtension // contentType detect if mimeType.isEmpty { +#if swift(<6.0) if let cachedMimeUTI = mimeTypeCache.object(forKey: inUTI) { mimeType = cachedMimeUTI as String - } else { + } +#else + if let cachedMimeUTI = mimeTypeCache[inUTI as String] { + mimeType = cachedMimeUTI + } +#endif + + if mimeType.isEmpty { if let mimeUTI = UTTypeCopyPreferredTagWithClass(inUTI, kUTTagClassMIMEType) { let mimeUTIString = mimeUTI.takeRetainedValue() as String mimeType = mimeUTIString +#if swift(<6.0) mimeTypeCache.setObject(mimeUTIString as NSString, forKey: inUTI) +#else + mimeTypeCache[inUTI as String] = mimeUTIString as String +#endif } } } @@ -197,12 +224,21 @@ public class NKCommon: NSObject { } else { var fileProperties: NKFileProperty +#if swift(<6.0) if let cachedFileProperties = filePropertiesCache.object(forKey: inUTI) { fileProperties = cachedFileProperties } else { fileProperties = getFileProperties(inUTI: inUTI) filePropertiesCache.setObject(fileProperties, forKey: inUTI) } +#else + if let cachedFileProperties = filePropertiesCache[inUTI as String] { + fileProperties = cachedFileProperties + } else { + fileProperties = getFileProperties(inUTI: inUTI) + filePropertiesCache[inUTI as String] = fileProperties + } +#endif classFile = fileProperties.classFile iconName = fileProperties.iconName @@ -212,7 +248,7 @@ public class NKCommon: NSObject { } public func getFileProperties(inUTI: CFString) -> NKFileProperty { - let fileProperty = NKFileProperty() + var fileProperty = NKFileProperty() let typeIdentifier: String = inUTI as String if let fileExtension = UTTypeCopyPreferredTagWithClass(inUTI as CFString, kUTTagClassFilenameExtension) { diff --git a/Sources/NextcloudKit/NKDataFileXML.swift b/Sources/NextcloudKit/NKDataFileXML.swift index eef09d65..d9c567bd 100644 --- a/Sources/NextcloudKit/NKDataFileXML.swift +++ b/Sources/NextcloudKit/NKDataFileXML.swift @@ -7,7 +7,7 @@ import Foundation import SwiftyXMLParser class NKDataFileXML: NSObject { - let nkCommonInstance: NKCommon + var nkCommonInstance: NKCommon let requestBodyComments = """ @@ -307,7 +307,7 @@ class NKDataFileXML: NSObject { let elements = xml["d:multistatus", "d:response"] for element in elements { - let file = NKFile() + var file = NKFile() if let href = element["d:href"].text { var fileNamePath = href if href.last == "/" { @@ -577,7 +577,7 @@ class NKDataFileXML: NSObject { file.downloadLimits.append(NKDownloadLimit(count: count, limit: limit, token: token)) } - let results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) + var results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) file.contentType = results.mimeType file.iconName = results.iconName diff --git a/Sources/NextcloudKit/NKError.swift b/Sources/NextcloudKit/NKError.swift index f162a261..5d4db76d 100644 --- a/Sources/NextcloudKit/NKError.swift +++ b/Sources/NextcloudKit/NKError.swift @@ -32,7 +32,7 @@ extension OCSPath { static var ocsXMLMsg: Self { ["d:error", "s:message"] } } -public class NKError: NSObject { +public struct NKError: Sendable, Equatable { static let internalError = -9999 // Chunk error public static let chunkNoEnoughMemory = -9998 @@ -159,7 +159,7 @@ public class NKError: NSObject { self.responseData = responseData } - convenience init(httpResponse: HTTPURLResponse) { + init(httpResponse: HTTPURLResponse) { self.init(statusCode: httpResponse.statusCode, fallbackDescription: httpResponse.description) } @@ -181,7 +181,7 @@ public class NKError: NSObject { self.error = NSError(domain: NSCocoaErrorDomain, code: self.errorCode, userInfo: [NSLocalizedDescriptionKey: self.errorDescription]) } - public convenience init(error: AFError?, afResponse: T, responseData: Data? = nil) { + public init(error: AFError?, afResponse: T, responseData: Data? = nil) { if let errorCode = afResponse.response?.statusCode { guard let dataResponse = afResponse as? Alamofire.DataResponse, let errorData = dataResponse.data @@ -216,9 +216,13 @@ public class NKError: NSObject { } } - public override func isEqual(_ object: Any?) -> Bool { - if let object = object as? NKError { - return self.errorCode == object.errorCode && self.errorDescription == object.errorDescription + public static func == (lhs: NKError, rhs: NKError) -> Bool { + return lhs.errorCode == rhs.errorCode && lhs.errorDescription == rhs.errorDescription + } + + public static func == (lhs: NKError, rhs: NKError?) -> Bool { + if let rhs { + return lhs == rhs; } return false } diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index ba019afc..89e6c340 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -3,9 +3,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later import Foundation -import Alamofire +@preconcurrency import Alamofire -public class NKSession { +public struct NKSession: Sendable { public var urlBase: String public var user: String public var userId: String @@ -26,7 +26,8 @@ public class NKSession { public let sessionUploadBackgroundWWan: URLSession public let sessionUploadBackgroundExt: URLSession - init(urlBase: String, + init(nkCommonInstance: NKCommon, + urlBase: String, user: String, userId: String, password: String, @@ -51,7 +52,7 @@ public class NKSession { self.httpMaximumConnectionsPerHostInUpload = httpMaximumConnectionsPerHostInUpload self.requestCachePolicy = requestCachePolicy - let backgroundSessionDelegate = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance) + let backgroundSessionDelegate = NKBackground(nkCommonInstance: nkCommonInstance) /// Strange but works ?!?! let sharedCookieStorage = user + "@" + urlBase @@ -66,11 +67,11 @@ public class NKSession { configuration.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage) sessionData = Alamofire.Session(configuration: configuration, - delegate: NextcloudKitSessionDelegate(nkCommonInstance: NextcloudKit.shared.nkCommonInstance), - rootQueue: NextcloudKit.shared.nkCommonInstance.rootQueue, - requestQueue: NextcloudKit.shared.nkCommonInstance.requestQueue, - serializationQueue: NextcloudKit.shared.nkCommonInstance.serializationQueue, - eventMonitors: [NKLogger(nkCommonInstance: NextcloudKit.shared.nkCommonInstance)]) + delegate: NextcloudKitSessionDelegate(nkCommonInstance: nkCommonInstance), + rootQueue: nkCommonInstance.rootQueue, + requestQueue: nkCommonInstance.requestQueue, + serializationQueue: nkCommonInstance.serializationQueue, + eventMonitors: [NKLogger(nkCommonInstance: nkCommonInstance)]) /// Session Download Background let configurationDownloadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionDownloadBackground) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index a9ad6158..ec8bc84f 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -379,7 +379,19 @@ public extension NextcloudKit { #else #if os(iOS) - let screenScale = UIScreen.main.scale + var screenScale = 1.0 + if #available(iOS 13.0, *) { + let semaphore = DispatchSemaphore(value: 0) + Task { + screenScale = await UIScreen.main.scale + semaphore.signal() + } + semaphore.wait() + } else { + #if swift(<6.0) + screenScale = UIScreen.main.scale + #endif + } #else let screenScale = 1.0 #endif diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index 6438f082..809d1f35 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -161,7 +161,7 @@ public extension NextcloudKit { if error == .success { completion(NKError()) } else if error.errorCode == 404 { - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, account: account, options: options) { _, _, _, _, error in + self.createFolder(serverUrlFileName: serverUrlChunkFolder, account: account, options: options) { _, _, _, _, error in completion(error) } } else { diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index ce7b8402..b4d55640 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -11,25 +11,35 @@ import Alamofire import SwiftyJSON open class NextcloudKit { +#if swift(<6.0) public static let shared: NextcloudKit = { let instance = NextcloudKit() return instance }() +#endif #if !os(watchOS) private let reachabilityManager = Alamofire.NetworkReachabilityManager() #endif - public let nkCommonInstance = NKCommon() + public var nkCommonInstance = NKCommon() internal lazy var internalSession: Alamofire.Session = { return Alamofire.Session(configuration: URLSessionConfiguration.af.default, delegate: NextcloudKitSessionDelegate(nkCommonInstance: nkCommonInstance), eventMonitors: [NKLogger(nkCommonInstance: self.nkCommonInstance)]) }() +#if swift(<6.0) init() { #if !os(watchOS) startNetworkReachabilityObserver() #endif } +#else + public init() { +#if !os(watchOS) + startNetworkReachabilityObserver() +#endif + } +#endif deinit { #if !os(watchOS) @@ -70,6 +80,7 @@ open class NextcloudKit { } let nkSession = NKSession( + nkCommonInstance: nkCommonInstance, urlBase: urlBase, user: user, userId: userId, @@ -94,7 +105,7 @@ open class NextcloudKit { userAgent: String? = nil, nextcloudVersion: Int? = nil, replaceWithAccount: String? = nil) { - guard let nkSession = nkCommonInstance.nksessions.filter({ $0.account == account }).first else { return } + guard var nkSession = nkCommonInstance.nksessions.filter({ $0.account == account }).first else { return } if let urlBase { nkSession.urlBase = urlBase } diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index 8b7da1ec..ad74e864 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -4,7 +4,7 @@ import Foundation -public class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, URLSessionDownloadDelegate { +public final class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDelegate, URLSessionDownloadDelegate { let nkCommonInstance: NKCommon public init(nkCommonInstance: NKCommon) { diff --git a/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift index c8b42707..9d9c65ce 100644 --- a/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift +++ b/Sources/NextcloudKit/NextcloudKitSessionDelegate.swift @@ -14,15 +14,11 @@ import Alamofire import SwiftyJSON final class NextcloudKitSessionDelegate: SessionDelegate { - public var nkCommonInstance: NKCommon? + public let nkCommonInstance: NKCommon? - override public init(fileManager: FileManager = .default) { - super.init(fileManager: fileManager) - } - - convenience init(nkCommonInstance: NKCommon?) { - self.init() + public init(fileManager: FileManager = .default, nkCommonInstance: NKCommon? = nil) { self.nkCommonInstance = nkCommonInstance + super.init(fileManager: fileManager) } public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { diff --git a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift index ca0d508a..b835f381 100644 --- a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift +++ b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift @@ -12,47 +12,43 @@ import Foundation // Copyright © 2024 Marino Faggiana. All rights reserved. // -public class FileAutoRenamer { +public final class FileAutoRenamer: Sendable { public static let shared: FileAutoRenamer = { let instance = FileAutoRenamer() return instance }() - private var forbiddenFileNameCharacters: [String] = [] - - private var forbiddenFileNameExtensions: [String] = [] { - didSet { - forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.lowercased()}) - } - } + private let forbiddenFileNameCharacters: [String] + private let forbiddenFileNameExtensions: [String] private let replacement = "_" - public func setup(forbiddenFileNameCharacters: [String], forbiddenFileNameExtensions: [String]) { + public init(forbiddenFileNameCharacters: [String] = [], forbiddenFileNameExtensions: [String] = []) { self.forbiddenFileNameCharacters = forbiddenFileNameCharacters - self.forbiddenFileNameExtensions = forbiddenFileNameExtensions + self.forbiddenFileNameExtensions = forbiddenFileNameExtensions.map { $0.lowercased() } } public func rename(filename: String, isFolderPath: Bool = false) -> String { var pathSegments = filename.split(separator: "/", omittingEmptySubsequences: false).map { String($0) } + var mutableForbiddenFileNameCharacters = self.forbiddenFileNameCharacters if isFolderPath { - forbiddenFileNameCharacters.removeAll { $0 == "/" } + mutableForbiddenFileNameCharacters.removeAll { $0 == "/" } } pathSegments = pathSegments.map { segment in var modifiedSegment = segment - forbiddenFileNameCharacters.forEach { forbiddenChar in + if mutableForbiddenFileNameCharacters.contains(" ") { + modifiedSegment = modifiedSegment.trimmingCharacters(in: .whitespaces) + } + + mutableForbiddenFileNameCharacters.forEach { forbiddenChar in if modifiedSegment.contains(forbiddenChar) { modifiedSegment = modifiedSegment.replacingOccurrences(of: forbiddenChar, with: replacement, options: .caseInsensitive) } } - if forbiddenFileNameExtensions.contains(" ") { - modifiedSegment = modifiedSegment.trimmingCharacters(in: .whitespaces) - } - // Replace forbidden extension, if any (ex. .part -> _part) forbiddenFileNameExtensions.forEach { forbiddenExtension in if modifiedSegment.lowercased().hasSuffix(forbiddenExtension) && isFullExtension(forbiddenExtension) { @@ -77,6 +73,11 @@ public class FileAutoRenamer { modifiedSegment.append(".\(fileExtension.lowercased())") } + if modifiedSegment.hasPrefix(".") { + modifiedSegment.remove(at: modifiedSegment.startIndex) + modifiedSegment = replacement + modifiedSegment + } + return modifiedSegment } diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index 52dc05c1..af509cab 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -4,61 +4,39 @@ import Foundation -public class FileNameValidator { - public static let shared: FileNameValidator = { - let instance = FileNameValidator() - return instance - }() - - private var forbiddenFileNames: [String] = [] { - didSet { - forbiddenFileNames = forbiddenFileNames.map({$0.uppercased()}) - } - } - - private var forbiddenFileNameBasenames: [String] = [] { - didSet { - forbiddenFileNameBasenames = forbiddenFileNameBasenames.map({$0.uppercased()}) - } - } - - private var forbiddenFileNameCharacters: [String] = [] - - private var forbiddenFileNameExtensions: [String] = [] { - didSet { - forbiddenFileNameExtensions = forbiddenFileNameExtensions.map({$0.uppercased()}) - } +public final class FileNameValidator: Sendable { + private let forbiddenFileNames: [String] + private let forbiddenFileNameBasenames: [String] + private let forbiddenFileNameCharacters: [String] + private let forbiddenFileNameExtensions: [String] + + public func fileWithSpaceError() -> NKError { + NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: NSLocalizedString("_file_name_validator_error_space_", value: "Name must not contain spaces at the beginning or end.", comment: "")) } - public let fileWithSpaceError = NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: NSLocalizedString("_file_name_validator_error_space_", value: "Name must not contain spaces at the beginning or end.", comment: "")) - - public var fileReservedNameError: NKError { + public func fileReservedNameError(templateString: String) -> NKError { let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_reserved_name_", value: "\"%@\" is a forbidden name.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } - public var fileForbiddenFileExtensionError: NKError { - let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: "\".%@\" is a forbidden file extension.", comment: "") + public func fileForbiddenFileExtensionError(templateString: String) -> NKError { + let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_forbidden_file_extension_", value: ".\"%@\" is a forbidden file extension.", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } - public var fileInvalidCharacterError: NKError { + public func fileInvalidCharacterError(templateString: String) -> NKError { let errorMessageTemplate = NSLocalizedString("_file_name_validator_error_invalid_character_", value: "Name contains an invalid character: \"%@\".", comment: "") let errorMessage = String(format: errorMessageTemplate, templateString) return NKError(errorCode: NSURLErrorCannotCreateFile, errorDescription: errorMessage) } - private var templateString = "" - - private init() {} - - public func setup(forbiddenFileNames: [String], forbiddenFileNameBasenames: [String], forbiddenFileNameCharacters: [String], forbiddenFileNameExtensions: [String]) { - self.forbiddenFileNames = forbiddenFileNames - self.forbiddenFileNameBasenames = forbiddenFileNameBasenames + public init(forbiddenFileNames: [String], forbiddenFileNameBasenames: [String], forbiddenFileNameCharacters: [String], forbiddenFileNameExtensions: [String]) { + self.forbiddenFileNames = forbiddenFileNames.map { $0.uppercased() } + self.forbiddenFileNameBasenames = forbiddenFileNameBasenames.map { $0.uppercased() } self.forbiddenFileNameCharacters = forbiddenFileNameCharacters - self.forbiddenFileNameExtensions = forbiddenFileNameExtensions + self.forbiddenFileNameExtensions = forbiddenFileNameExtensions.map { $0.uppercased() } } public func checkFileName(_ filename: String) -> NKError? { @@ -68,23 +46,20 @@ public class FileNameValidator { if forbiddenFileNames.contains(filename.uppercased()) || forbiddenFileNames.contains(filename.withRemovedFileExtension.uppercased()) || forbiddenFileNameBasenames.contains(filename.uppercased()) || forbiddenFileNameBasenames.contains(filename.withRemovedFileExtension.uppercased()) { - templateString = filename - return fileReservedNameError + return fileReservedNameError(templateString: filename) } for fileNameExtension in forbiddenFileNameExtensions { if fileNameExtension == " " { if filename.uppercased().hasSuffix(fileNameExtension) || filename.uppercased().hasPrefix(fileNameExtension) { - return fileWithSpaceError + return fileWithSpaceError() } } else if filename.uppercased().hasSuffix(fileNameExtension.uppercased()) { if fileNameExtension == " " { - return fileWithSpaceError + return fileWithSpaceError() } - templateString = filename.fileExtension - - return fileForbiddenFileExtensionError + return fileForbiddenFileExtensionError(templateString: filename.fileExtension) } } @@ -106,8 +81,7 @@ public class FileNameValidator { let range = NSRange(location: 0, length: charAsString.utf16.count) if regex.firstMatch(in: charAsString, options: [], range: range) != nil { - templateString = charAsString - return fileInvalidCharacterError + return fileInvalidCharacterError(templateString: charAsString) } } return nil diff --git a/Sources/NextcloudKit/Utils/ThreadSafeArray.swift b/Sources/NextcloudKit/Utils/ThreadSafeArray.swift index 1dec61d1..fae38414 100644 --- a/Sources/NextcloudKit/Utils/ThreadSafeArray.swift +++ b/Sources/NextcloudKit/Utils/ThreadSafeArray.swift @@ -7,14 +7,13 @@ import Foundation /// A thread-safe array. -public class ThreadSafeArray { +public struct ThreadSafeArray: Sendable { private var array = [Element]() - private let queue = DispatchQueue(label: "com.nextcloud.ThreadSafeArray", attributes: .concurrent) public init() { } - public convenience init(_ array: [Element]) { + public init(_ array: [Element]) { self.init() self.array = array } @@ -26,37 +25,27 @@ public extension ThreadSafeArray { /// The first element of the collection. var first: Element? { - var result: Element? - queue.sync { result = self.array.first } - return result + NSLock().perform { self.array.first } } /// The last element of the collection. var last: Element? { - var result: Element? - queue.sync { result = self.array.last } - return result + NSLock().perform { self.array.last } } /// The number of elements in the array. var count: Int { - var result = 0 - queue.sync { result = self.array.count } - return result + NSLock().perform { self.array.count } } /// A Boolean value indicating whether the collection is empty. var isEmpty: Bool { - var result = false - queue.sync { result = self.array.isEmpty } - return result + NSLock().perform { self.array.isEmpty } } /// A textual representation of the array and its elements. var description: String { - var result = "" - queue.sync { result = self.array.description } - return result + NSLock().perform { self.array.description } } } @@ -69,9 +58,7 @@ public extension ThreadSafeArray { /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. /// - Returns: The first element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. func first(where predicate: (Element) -> Bool) -> Element? { - var result: Element? - queue.sync { result = self.array.first(where: predicate) } - return result + NSLock().perform { self.array.first(where: predicate) } } /// Returns the last element of the sequence that satisfies the given predicate. @@ -79,9 +66,7 @@ public extension ThreadSafeArray { /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. /// - Returns: The last element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. func last(where predicate: (Element) -> Bool) -> Element? { - var result: Element? - queue.sync { result = self.array.last(where: predicate) } - return result + NSLock().perform { self.array.last(where: predicate) } } /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate. @@ -89,9 +74,7 @@ public extension ThreadSafeArray { /// - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array. /// - Returns: An array of the elements that includeElement allowed. func filter(_ isIncluded: @escaping (Element) -> Bool) -> ThreadSafeArray { - var result: ThreadSafeArray? - queue.sync { result = ThreadSafeArray(self.array.filter(isIncluded)) } - return result! + NSLock().perform { ThreadSafeArray(self.array.filter(isIncluded)) } } /// Returns the first index in which an element of the collection satisfies the given predicate. @@ -99,9 +82,7 @@ public extension ThreadSafeArray { /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match. /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil. func index(where predicate: (Element) -> Bool) -> Int? { - var result: Int? - queue.sync { result = self.array.firstIndex(where: predicate) } - return result + NSLock().perform { self.array.firstIndex(where: predicate) } } /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements. @@ -109,9 +90,7 @@ public extension ThreadSafeArray { /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false. /// - Returns: A sorted array of the collection’s elements. func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> ThreadSafeArray { - var result: ThreadSafeArray? - queue.sync { result = ThreadSafeArray(self.array.sorted(by: areInIncreasingOrder)) } - return result! + NSLock().perform { ThreadSafeArray(self.array.sorted(by: areInIncreasingOrder)) } } /// Returns an array containing the results of mapping the given closure over the sequence’s elements. @@ -119,9 +98,7 @@ public extension ThreadSafeArray { /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. func map(_ transform: @escaping (Element) -> ElementOfResult) -> [ElementOfResult] { - var result = [ElementOfResult]() - queue.sync { result = self.array.map(transform) } - return result + NSLock().perform { self.array.map(transform) } } /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. @@ -129,9 +106,7 @@ public extension ThreadSafeArray { /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. func compactMap(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] { - var result = [ElementOfResult]() - queue.sync { result = self.array.compactMap(transform) } - return result + NSLock().perform { self.array.compactMap(transform) } } /// Returns the result of combining the elements of the sequence using the given closure. @@ -141,9 +116,7 @@ public extension ThreadSafeArray { /// - nextPartialResult: A closure that combines an accumulating value and an element of the sequence into a new accumulating value, to be used in the next call of the nextPartialResult closure or returned to the caller. /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. func reduce(_ initialResult: ElementOfResult, _ nextPartialResult: @escaping (ElementOfResult, Element) -> ElementOfResult) -> ElementOfResult { - var result: ElementOfResult? - queue.sync { result = self.array.reduce(initialResult, nextPartialResult) } - return result ?? initialResult + NSLock().perform { self.array.reduce(initialResult, nextPartialResult) } } /// Returns the result of combining the elements of the sequence using the given closure. @@ -153,16 +126,14 @@ public extension ThreadSafeArray { /// - updateAccumulatingResult: A closure that updates the accumulating value with an element of the sequence. /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. func reduce(into initialResult: ElementOfResult, _ updateAccumulatingResult: @escaping (inout ElementOfResult, Element) -> Void) -> ElementOfResult { - var result: ElementOfResult? - queue.sync { result = self.array.reduce(into: initialResult, updateAccumulatingResult) } - return result ?? initialResult + NSLock().perform { self.array.reduce(into: initialResult, updateAccumulatingResult) } } /// Calls the given closure on each element in the sequence in the same order as a for-in loop. /// /// - Parameter body: A closure that takes an element of the sequence as a parameter. func forEach(_ body: (Element) -> Void) { - queue.sync { self.array.forEach(body) } + NSLock().perform { self.array.forEach(body) } } /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate. @@ -170,9 +141,7 @@ public extension ThreadSafeArray { /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match. /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false. func contains(where predicate: (Element) -> Bool) -> Bool { - var result = false - queue.sync { result = self.array.contains(where: predicate) } - return result + NSLock().perform { self.array.contains(where: predicate) } } /// Returns a Boolean value indicating whether every element of a sequence satisfies a given predicate. @@ -180,18 +149,14 @@ public extension ThreadSafeArray { /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element satisfies a condition. /// - Returns: true if the sequence contains only elements that satisfy predicate; otherwise, false. func allSatisfy(_ predicate: (Element) -> Bool) -> Bool { - var result = false - queue.sync { result = self.array.allSatisfy(predicate) } - return result + NSLock().perform { self.array.allSatisfy(predicate) } } /// Returns the array /// /// - Returns: the array part. func getArray() -> [Element]? { - var results: [Element]? - queue.sync { results = self.array } - return results + NSLock().perform { self.array } } } @@ -202,19 +167,15 @@ public extension ThreadSafeArray { /// Adds a new element at the end of the array. /// /// - Parameter element: The element to append to the array. - func append(_ element: Element) { - queue.async(flags: .barrier) { - self.array.append(element) - } + mutating func append(_ element: Element) { + NSLock().perform { self.array.append(element) } } /// Adds new elements at the end of the array. /// /// - Parameter element: The elements to append to the array. - func append(_ elements: [Element]) { - queue.async(flags: .barrier) { - self.array += elements - } + mutating func append(_ elements: [Element]) { + NSLock().perform { self.array += elements } } /// Inserts a new element at the specified position. @@ -222,10 +183,8 @@ public extension ThreadSafeArray { /// - Parameters: /// - element: The new element to insert into the array. /// - index: The position at which to insert the new element. - func insert(_ element: Element, at index: Int) { - queue.async(flags: .barrier) { - self.array.insert(element, at: index) - } + mutating func insert(_ element: Element, at index: Int) { + NSLock().perform { self.array.insert(element, at: index) } } /// Removes and returns the element at the specified position. @@ -233,11 +192,8 @@ public extension ThreadSafeArray { /// - Parameters: /// - index: The position of the element to remove. /// - completion: The handler with the removed element. - func remove(at index: Int, completion: ((Element) -> Void)? = nil) { - queue.async(flags: .barrier) { - let element = self.array.remove(at: index) - DispatchQueue.main.async { completion?(element) } - } + mutating func remove(at index: Int, completion: ((Element) -> Void)? = nil) { + NSLock().perform { self.array.remove(at: index) } } /// Removes and returns the elements that meet the criteria. @@ -245,27 +201,21 @@ public extension ThreadSafeArray { /// - Parameters: /// - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. /// - completion: The handler with the removed elements. - func remove(where predicate: @escaping (Element) -> Bool, completion: (([Element]) -> Void)? = nil) { - queue.async(flags: .barrier) { + mutating func remove(where predicate: @escaping (Element) -> Bool) -> [Element] { + return NSLock().perform { var elements = [Element]() - while let index = self.array.firstIndex(where: predicate) { elements.append(self.array.remove(at: index)) } - - DispatchQueue.main.async { completion?(elements) } + return elements } } /// Removes all elements from the array. /// - /// - Parameter completion: The handler with the removed elements. - func removeAll(completion: (([Element]) -> Void)? = nil) { - queue.async(flags: .barrier) { - let elements = self.array - self.array.removeAll() - DispatchQueue.main.async { completion?(elements) } - } + /// - Parameter keepingCapacity: Pass true to keep the existing capacity of the array after removing its elements. The default value is false. + mutating func removeAll(keepingCapacity: Bool = false) { + NSLock().perform { self.array.removeAll(keepingCapacity: keepingCapacity) } } } @@ -277,21 +227,14 @@ public extension ThreadSafeArray { /// - Returns: optional element if it exists. subscript(index: Int) -> Element? { get { - var result: Element? - - queue.sync { - guard self.array.startIndex.. Bool { - var result = false - queue.sync { result = self.array.contains(element) } - return result + NSLock().perform { self.array.contains(element) } } } diff --git a/Tests/NextcloudKitIntegrationTests/Common/BaseXCTestCase.swift b/Tests/NextcloudKitIntegrationTests/Common/BaseXCTestCase.swift index 393d2e78..158b3720 100644 --- a/Tests/NextcloudKitIntegrationTests/Common/BaseXCTestCase.swift +++ b/Tests/NextcloudKitIntegrationTests/Common/BaseXCTestCase.swift @@ -10,11 +10,17 @@ import NextcloudKit class BaseXCTestCase: XCTestCase { var appToken = "" + var ncKit: NextcloudKit! - func setupAppToken() { + func setupAppToken() async { let expectation = expectation(description: "Should get app token") +#if swift(<6.0) + ncKit = NextcloudKit.shared +#else + ncKit = NextcloudKit() +#endif - NextcloudKit.shared.getAppPassword(url: TestConstants.server, user: TestConstants.username, password: TestConstants.password) { token, _, error in + ncKit.getAppPassword(url: TestConstants.server, user: TestConstants.username, password: TestConstants.password) { token, _, error in XCTAssertEqual(error.errorCode, 0) XCTAssertNotNil(token) @@ -23,11 +29,11 @@ class BaseXCTestCase: XCTestCase { self.appToken = token expectation.fulfill() } - - waitForExpectations(timeout: TestConstants.timeoutLong) + + await fulfillment(of: [expectation], timeout: TestConstants.timeoutLong) } - override func setUpWithError() throws { - setupAppToken() + override func setUp() async throws { + await setupAppToken() } } diff --git a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift index d82ae672..f2191821 100644 --- a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift @@ -12,11 +12,11 @@ import Testing let forbiddenFilenameExtension = "." let characterArrays = [ - ["\\\\", "*", ">", "&", "/", "|", ":", "<", "?"], - [">", ":", "?", "&", "*", "\\\\", "|", "<", "/"], - ["<", "|", "?", ":", "&", "*", "\\\\", "/", ">"], - ["?", "/", ":", "&", "<", "|", ">", "\\\\", "*"], - ["&", "<", "|", "*", "/", "?", ">", ":", "\\\\"] + ["\\\\", "*", ">", "&", "/", "|", ":", "<", "?", " "], + [">", ":", "?", " ", "&", "*", "\\\\", "|", "<", "/"], + ["<", "|", "?", ":", "&", "*", "\\\\", " ", "/", ">"], + ["?", "/", " ", ":", "&", "<", "|", ">", "\\\\", "*"], + ["&", "<", "|", "*", "/", "?", ">", " ", ":", "\\\\"] ] let extensionArrays = [ @@ -35,7 +35,7 @@ import Testing @Test func testInvalidChar() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -49,7 +49,7 @@ import Testing @Test func testInvalidExtension() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -63,7 +63,7 @@ import Testing @Test func testMultipleInvalidChars() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -77,7 +77,7 @@ import Testing @Test func testStartEndInvalidExtensions() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -91,7 +91,7 @@ import Testing @Test func testStartInvalidExtension() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -105,7 +105,7 @@ import Testing @Test func testEndInvalidExtension() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -119,7 +119,7 @@ import Testing @Test func testHiddenFile() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -133,7 +133,7 @@ import Testing @Test func testUppercaseExtension() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -147,7 +147,7 @@ import Testing @Test func testMiddleNonPrintableChar() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -161,7 +161,7 @@ import Testing @Test func testStartNonPrintableChar() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -175,7 +175,7 @@ import Testing @Test func testEndNonPrintableChar() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -189,7 +189,7 @@ import Testing @Test func testExtensionNonPrintableChar() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -203,7 +203,7 @@ import Testing @Test func testMiddleInvalidFolderChar() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -217,7 +217,7 @@ import Testing @Test func testEndInvalidFolderChar() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -231,7 +231,7 @@ import Testing @Test func testStartInvalidFolderChar() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -245,7 +245,7 @@ import Testing @Test func testMixedInvalidChar() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -259,7 +259,7 @@ import Testing @Test func testStartsWithPathSeparator() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) @@ -273,7 +273,7 @@ import Testing @Test func testStartsWithPathSeparatorAndValidFilepath() { for (characterArray, extensionArray) in combinedTuples { - fileAutoRenamer.setup( + let fileAutoRenamer = FileAutoRenamer( forbiddenFileNameCharacters: characterArray, forbiddenFileNameExtensions: extensionArray ) diff --git a/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift b/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift index fab4d2ed..7828c27a 100644 --- a/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/FileNameValidatorUnitTests.swift @@ -6,10 +6,10 @@ import XCTest @testable import NextcloudKit class FileNameValidatorUnitTests: XCTestCase { - let fileNameValidator = FileNameValidator.shared + var fileNameValidator: FileNameValidator! override func setUp() { - fileNameValidator.setup( + fileNameValidator = FileNameValidator( forbiddenFileNames: [".htaccess",".htaccess"], forbiddenFileNameBasenames: ["con", "prn", "aux", "nul", "com0", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", "com¹", "com²", "com³", @@ -23,31 +23,31 @@ class FileNameValidatorUnitTests: XCTestCase { func testInvalidCharacter() { let result = fileNameValidator.checkFileName("file Date: Fri, 31 Jan 2025 14:35:40 +0100 Subject: [PATCH 097/135] Add static/refactor Signed-off-by: Milen Pivchev --- Sources/NextcloudKit/Utils/FileAutoRenamer.swift | 5 ----- Sources/NextcloudKit/Utils/FileNameValidator.swift | 2 +- Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift | 2 -- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift index b835f381..0ca8298c 100644 --- a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift +++ b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift @@ -13,11 +13,6 @@ import Foundation // public final class FileAutoRenamer: Sendable { - public static let shared: FileAutoRenamer = { - let instance = FileAutoRenamer() - return instance - }() - private let forbiddenFileNameCharacters: [String] private let forbiddenFileNameExtensions: [String] diff --git a/Sources/NextcloudKit/Utils/FileNameValidator.swift b/Sources/NextcloudKit/Utils/FileNameValidator.swift index af509cab..12320712 100644 --- a/Sources/NextcloudKit/Utils/FileNameValidator.swift +++ b/Sources/NextcloudKit/Utils/FileNameValidator.swift @@ -71,7 +71,7 @@ public final class FileNameValidator: Sendable { .allSatisfy { checkFileName(String($0)) == nil } } - public func isFileHidden(_ name: String) -> Bool { + public static func isFileHidden(_ name: String) -> Bool { return !name.isEmpty && name.first == "." } diff --git a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift index f2191821..814dc262 100644 --- a/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift +++ b/Tests/NextcloudKitUnitTests/FileAutoRenamerUnitTests.swift @@ -6,8 +6,6 @@ import Testing @testable import NextcloudKit @Suite(.serialized) struct FileAutoRenamerUnitTests { - let fileAutoRenamer = FileAutoRenamer.shared - let forbiddenFilenameCharacter = ">" let forbiddenFilenameExtension = "." From 31e5f494f575174bf852a4ddce24ec151c0a41d1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 31 Jan 2025 16:15:32 +0100 Subject: [PATCH 098/135] NextcloudKitDelegate Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index ab7e2590..d1072f0a 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -12,7 +12,7 @@ import MobileCoreServices import CoreServices #endif -public protocol NextcloudKitDelegate: Sendable { +public protocol NextcloudKitDelegate: AnyObject, Sendable { func authenticationChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) From 3ab5b95ff1280fbf78749f136e9f1f8ce701aac1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 13:25:09 +0100 Subject: [PATCH 099/135] change Name to NKMonitor Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/{NKLogger.swift => NKMonitor.swift} | 2 +- Sources/NextcloudKit/NKSession.swift | 2 +- Sources/NextcloudKit/NextcloudKit.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename Sources/NextcloudKit/{NKLogger.swift => NKMonitor.swift} (98%) diff --git a/Sources/NextcloudKit/NKLogger.swift b/Sources/NextcloudKit/NKMonitor.swift similarity index 98% rename from Sources/NextcloudKit/NKLogger.swift rename to Sources/NextcloudKit/NKMonitor.swift index 5b85215b..65ec23d0 100644 --- a/Sources/NextcloudKit/NKLogger.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -5,7 +5,7 @@ import Foundation import Alamofire -final class NKLogger: EventMonitor { +final class NKMonitor: EventMonitor { let nkCommonInstance: NKCommon init(nkCommonInstance: NKCommon) { diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index 89e6c340..018ecbd1 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -71,7 +71,7 @@ public struct NKSession: Sendable { rootQueue: nkCommonInstance.rootQueue, requestQueue: nkCommonInstance.requestQueue, serializationQueue: nkCommonInstance.serializationQueue, - eventMonitors: [NKLogger(nkCommonInstance: nkCommonInstance)]) + eventMonitors: [NKMonitor(nkCommonInstance: nkCommonInstance)]) /// Session Download Background let configurationDownloadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionDownloadBackground) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index b4d55640..e97e5628 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -24,7 +24,7 @@ open class NextcloudKit { internal lazy var internalSession: Alamofire.Session = { return Alamofire.Session(configuration: URLSessionConfiguration.af.default, delegate: NextcloudKitSessionDelegate(nkCommonInstance: nkCommonInstance), - eventMonitors: [NKLogger(nkCommonInstance: self.nkCommonInstance)]) + eventMonitors: [NKMonitor(nkCommonInstance: self.nkCommonInstance)]) }() #if swift(<6.0) From b93eca46bddd343ab55ccaf311551418e0deef24 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 15:02:34 +0100 Subject: [PATCH 100/135] Added NKInterceptor Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 9 ++++++-- Sources/NextcloudKit/NKInterceptor.swift | 26 ++++++++++++++++++++++++ Sources/NextcloudKit/NKMonitor.swift | 22 +++++++++++++++++++- Sources/NextcloudKit/NextcloudKit.swift | 3 ++- 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 Sources/NextcloudKit/NKInterceptor.swift diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index d1072f0a..b20237b0 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -32,6 +32,7 @@ public protocol NextcloudKitDelegate: AnyObject, Sendable { public struct NKCommon: Sendable { public var nksessions = ThreadSafeArray() public var delegate: NextcloudKitDelegate? + public var groupIdentifier: String = "" public let identifierSessionDownload: String = "com.nextcloud.nextcloudkit.session.download" public let identifierSessionUpload: String = "com.nextcloud.nextcloudkit.session.upload" @@ -248,7 +249,7 @@ public struct NKCommon: Sendable { } public func getFileProperties(inUTI: CFString) -> NKFileProperty { - var fileProperty = NKFileProperty() + let fileProperty = NKFileProperty() let typeIdentifier: String = inUTI as String if let fileExtension = UTTypeCopyPreferredTagWithClass(inUTI as CFString, kUTTagClassFilenameExtension) { @@ -426,7 +427,7 @@ public struct NKCommon: Sendable { return session } - public func getStandardHeaders(account: String, options: NKRequestOptions? = nil) -> HTTPHeaders? { + public func getStandardHeaders(account: String, checkUnauthorized: Bool = false, options: NKRequestOptions? = nil) -> HTTPHeaders? { guard let session = nksessions.filter({ $0.account == account }).first else { return nil} var headers: HTTPHeaders = [] @@ -447,6 +448,10 @@ public struct NKCommon: Sendable { for (key, value) in options?.customHeader ?? [:] { headers.update(name: key, value: value) } + headers.update(name: "X-NC-Account", value: account) + if checkUnauthorized { + headers.update(name: "X-NC-CheckUnauthorized", value: "true") + } // Paginate if let options { if options.paginate { diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift new file mode 100644 index 00000000..80bc9d16 --- /dev/null +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Alamofire + +final class NKInterceptor: RequestInterceptor, Sendable { + static let shared = Interceptor() + + func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) + + // + // Detect if exists in the groupDefaults Unauthorized array the account + // + if let account = urlRequest.value(forHTTPHeaderField: "X-NC-Account"), + let unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String], + unauthorizedArray.contains(account) { + let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 401)) + return completion(.failure(error)) + } + + completion(.success(urlRequest)) + } +} diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 65ec23d0..2e6febcf 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -5,7 +5,7 @@ import Foundation import Alamofire -final class NKMonitor: EventMonitor { +final class NKMonitor: EventMonitor, Sendable { let nkCommonInstance: NKCommon init(nkCommonInstance: NKCommon) { @@ -27,7 +27,27 @@ final class NKMonitor: EventMonitor { func request(_ request: DataRequest, didParseResponse response: AFDataResponse) { self.nkCommonInstance.delegate?.request(request, didParseResponse: response) + let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) + // + // Error 401, append the account in groupDefaults Unauthorized array + // + if let statusCode = response.response?.statusCode, + statusCode == 401, + let isCheckUnauthorized = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"] as? Bool, + isCheckUnauthorized, + let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { + + var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] + if !unauthorizedArray.contains(account) { + unauthorizedArray.append(account) + groupDefaults?.set(unauthorizedArray, forKey: "Unauthorized") + } + } + + // + // LOG + // guard let date = self.nkCommonInstance.convertDate(Date(), format: "yyyy-MM-dd' 'HH:mm:ss") else { return } let responseResultString = String("\(response.result)") let responseDebugDescription = String("\(response.debugDescription)") diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index e97e5628..7eab9425 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -49,8 +49,9 @@ open class NextcloudKit { // MARK: - Session setup - public func setup(delegate: NextcloudKitDelegate?, memoryCapacity: Int = 30, diskCapacity: Int = 500, removeAllCachedResponses: Bool = false) { + public func setup(groupIdentifier: String, delegate: NextcloudKitDelegate?, memoryCapacity: Int = 30, diskCapacity: Int = 500, removeAllCachedResponses: Bool = false) { self.nkCommonInstance.delegate = delegate + self.nkCommonInstance.groupIdentifier = groupIdentifier /// Cache URLSession /// From dce0f8c25858a7f39e270948b29d32f03a6afcc1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 15:12:14 +0100 Subject: [PATCH 101/135] Interceptor Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKDataFileXML.swift | 2 +- Sources/NextcloudKit/NextcloudKit+API.swift | 32 ++++++++++----------- Sources/NextcloudKit/NextcloudKit.swift | 1 + 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Sources/NextcloudKit/NKDataFileXML.swift b/Sources/NextcloudKit/NKDataFileXML.swift index d9c567bd..6cd1310a 100644 --- a/Sources/NextcloudKit/NKDataFileXML.swift +++ b/Sources/NextcloudKit/NKDataFileXML.swift @@ -577,7 +577,7 @@ class NKDataFileXML: NSObject { file.downloadLimits.append(NKDownloadLimit(count: count, limit: limit, token: token)) } - var results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) + let results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) file.contentType = results.mimeType file.iconName = results.iconName diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index ec8bc84f..efb0af7b 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -39,7 +39,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, .urlError) } } - internalSession.request(url, method: .head, parameters: nil, encoding: URLEncoding.default, headers: nil, interceptor: nil).onURLSessionTaskCreation { task in + internalSession.request(url, method: .head, encoding: URLEncoding.default).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -71,7 +71,7 @@ public extension NextcloudKit { } let method = HTTPMethod(rawValue: method.uppercased()) - nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -102,7 +102,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, externalSites, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -164,7 +164,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - internalSession.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + internalSession.request(url, method: .get, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -222,7 +222,7 @@ public extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -264,7 +264,7 @@ public extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -301,7 +301,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, width, height, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -339,7 +339,7 @@ public extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -433,7 +433,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -463,7 +463,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -532,7 +532,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -660,7 +660,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -736,7 +736,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -819,7 +819,7 @@ public extension NextcloudKit { } let method = HTTPMethod(rawValue: method) - nkSession.sessionData.request(urlRequest, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, method: method, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -854,7 +854,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -898,7 +898,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 7eab9425..415635db 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -21,6 +21,7 @@ open class NextcloudKit { private let reachabilityManager = Alamofire.NetworkReachabilityManager() #endif public var nkCommonInstance = NKCommon() + public let nkInterceptor = NKInterceptor.shared internal lazy var internalSession: Alamofire.Session = { return Alamofire.Session(configuration: URLSessionConfiguration.af.default, delegate: NextcloudKitSessionDelegate(nkCommonInstance: nkCommonInstance), From ff51b7752790b65a74cbc68666a52f93ecdf5c3a Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 15:36:59 +0100 Subject: [PATCH 102/135] Interceptor Signed-off-by: Marino Faggiana --- .../NextcloudKit/NextcloudKit+Assistant.swift | 10 ++++----- .../NextcloudKit/NextcloudKit+Comments.swift | 10 ++++----- .../NextcloudKit/NextcloudKit+Dashboard.swift | 4 ++-- .../NextcloudKit/NextcloudKit+Download.swift | 2 +- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 22 +++++++++---------- .../NextcloudKit/NextcloudKit+FilesLock.swift | 2 +- .../NextcloudKit+Groupfolders.swift | 2 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 2 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 2 +- Sources/NextcloudKit/NextcloudKit+Login.swift | 4 ++-- .../NextcloudKit/NextcloudKit+NCText.swift | 8 +++---- .../NextcloudKit+PushNotification.swift | 8 +++---- .../NextcloudKit+RecommendedFiles.swift | 2 +- .../NextcloudKit+Richdocuments.swift | 8 +++---- .../NextcloudKit/NextcloudKit+Search.swift | 4 ++-- Sources/NextcloudKit/NextcloudKit+Share.swift | 10 ++++----- .../NextcloudKit+ShareDownloadLimit.swift | 6 ++--- .../NextcloudKit+TermsOfService.swift | 4 ++-- .../NextcloudKit/NextcloudKit+Upload.swift | 2 +- .../NextcloudKit+UserStatus.swift | 14 ++++++------ .../NextcloudKit/NextcloudKit+WebDAV.swift | 18 +++++++-------- 21 files changed, 72 insertions(+), 72 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index 1b558de2..bdb4d8e6 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -18,7 +18,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -59,7 +59,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -96,7 +96,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -133,7 +133,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -170,7 +170,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 818bd325..0a8a0bea 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -32,7 +32,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -82,7 +82,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -129,7 +129,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -162,7 +162,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -207,7 +207,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 160b0122..4676de86 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -58,7 +58,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Download.swift b/Sources/NextcloudKit/NextcloudKit+Download.swift index 16081acb..bf4e55df 100644 --- a/Sources/NextcloudKit/NextcloudKit+Download.swift +++ b/Sources/NextcloudKit/NextcloudKit+Download.swift @@ -33,7 +33,7 @@ public extension NextcloudKit { } destination = destinationFile - let request = nkSession.sessionData.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let request = nkSession.sessionData.download(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } } .downloadProgress { progress in diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index 38f6af7d..9e2175e3 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -25,7 +25,7 @@ public extension NextcloudKit { } let method: HTTPMethod = delete ? .delete : .put - nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -77,7 +77,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-COUNTER", value: e2eCounter) } - nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -122,7 +122,7 @@ public extension NextcloudKit { parameters["e2e-token"] = e2eToken } - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -177,7 +177,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-SIGNATURE", value: signature) } - nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -228,7 +228,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -272,7 +272,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -311,7 +311,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -352,7 +352,7 @@ public extension NextcloudKit { } let parameters = ["csr": certificate] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -394,7 +394,7 @@ public extension NextcloudKit { } let parameters = ["privateKey": privateKey] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -432,7 +432,7 @@ public extension NextcloudKit { let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -464,7 +464,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index c9d5678e..87be6bce 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -24,7 +24,7 @@ public extension NextcloudKit { } headers.update(name: "X-User-Lock", value: "1") - nkSession.sessionData.request(url, method: method, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 80413c3d..db5b7421 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index b5ba2dd6..c775c4bc 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 5c49ad86..21fb08a4 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -32,7 +32,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index df5ce0d7..2f4ae8da 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -112,7 +112,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - internalSession.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + internalSession.request(url, method: .post, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -149,7 +149,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - internalSession.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + internalSession.request(url, method: .post, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index 9d2fb097..568c526e 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -20,7 +20,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, editors, creators, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -90,7 +90,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -121,7 +121,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -174,7 +174,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index 847093d9..efdf9464 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -27,7 +27,7 @@ public extension NextcloudKit { "proxyServer": proxyServerUrl ] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -65,7 +65,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -105,7 +105,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -143,7 +143,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - nkSession.sessionData.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift index 172fb58c..8a5a10e8 100644 --- a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift +++ b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift @@ -22,7 +22,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index e6e25894..12248684 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -20,7 +20,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["fileId": fileID] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -55,7 +55,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -104,7 +104,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["path": path, "template": templateId] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -140,7 +140,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["path": path] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index 82fecb02..f3d7b636 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -40,7 +40,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - let requestUnifiedSearch = nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestUnifiedSearch = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -131,7 +131,7 @@ public extension NextcloudKit { return nil } - let requestSearchProvider = nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestSearchProvider = nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 1b64ddbf..80cc2c32 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -67,7 +67,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -130,7 +130,7 @@ public extension NextcloudKit { "lookup": lookupString ] - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -300,7 +300,7 @@ public extension NextcloudKit { parameters["attributes"] = attributes } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -385,7 +385,7 @@ public extension NextcloudKit { parameters["attributes"] = "[]" } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -423,7 +423,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift index 316c540b..50df97f7 100644 --- a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift +++ b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift @@ -25,7 +25,7 @@ public extension NextcloudKit { nkSession .sessionData - .request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil) + .request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor) .validate(statusCode: 200..<300) .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -94,7 +94,7 @@ public extension NextcloudKit { nkSession .sessionData - .request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil) + .request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor) .validate(statusCode: 200..<300) .response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -136,7 +136,7 @@ public extension NextcloudKit { nkSession .sessionData - .request(urlRequest) + .request(urlRequest, interceptor: nkInterceptor) .validate(statusCode: 200..<300) .response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift b/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift index 1ea602e3..9207c45d 100644 --- a/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift +++ b/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -70,7 +70,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index 809d1f35..5c5e4e74 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -43,7 +43,7 @@ public extension NextcloudKit { headers.update(name: "Overwrite", value: "true") } - let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nil, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in + let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nkInterceptor, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } }) .uploadProgress { progress in diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index 72260b4f..8e61b9ce 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -22,7 +22,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -73,7 +73,7 @@ public extension NextcloudKit { "statusType": String(status) ] - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -115,7 +115,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -162,7 +162,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -197,7 +197,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -232,7 +232,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -289,7 +289,7 @@ public extension NextcloudKit { "offset": String(offset) ] - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index b29c461a..af194856 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -26,7 +26,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -70,7 +70,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -114,7 +114,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -159,7 +159,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -216,7 +216,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -386,7 +386,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -437,7 +437,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -483,7 +483,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -538,7 +538,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, items, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in From 4184d62b498a2198d6e1173bc08cf9c3170e3e02 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 15:40:02 +0100 Subject: [PATCH 103/135] interceptor Signed-off-by: Marino Faggiana --- .../NextcloudKit/NextcloudKitBackground.swift | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index ad74e864..a7682024 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -19,18 +19,29 @@ public final class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDel taskDescription: String? = nil, account: String) -> URLSessionDownloadTask? { var url: URL? + let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) + if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { url = (serverUrlFileName as? String)?.encodedToUrl as? URL } - guard let nkSession = nkCommonInstance.getSession(account: account) else { + + if let unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String], + unauthorizedArray.contains(account) { + return nil + } + + guard let nkSession = nkCommonInstance.getSession(account: account), + let urlForRequest = url + else { return nil } - guard let urlForRequest = url else { return nil } var request = URLRequest(url: urlForRequest) let loginString = "\(nkSession.user):\(nkSession.password)" - guard let loginData = loginString.data(using: String.Encoding.utf8) else { + + guard let loginData = loginString.data(using: String.Encoding.utf8) + else { return nil } let base64LoginString = loginData.base64EncodedString() @@ -58,15 +69,25 @@ public final class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDel sessionIdentifier: String) -> URLSessionUploadTask? { var url: URL? var uploadSession: URLSession? - guard let nkSession = nkCommonInstance.getSession(account: account) else { return nil } + let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) + if serverUrlFileName is URL { url = serverUrlFileName as? URL } else if serverUrlFileName is String || serverUrlFileName is NSString { url = (serverUrlFileName as? String)?.encodedToUrl as? URL } - guard let urlForRequest = url else { + + if let unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String], + unauthorizedArray.contains(account) { + return nil + } + + guard let nkSession = nkCommonInstance.getSession(account: account), + let urlForRequest = url + else { return nil } + var request = URLRequest(url: urlForRequest) let loginString = "\(nkSession.user):\(nkSession.password)" guard let loginData = loginString.data(using: String.Encoding.utf8) else { @@ -139,13 +160,16 @@ public final class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDel } var nkError: NKError = .success - if let httpResponse = (task.response as? HTTPURLResponse) { - if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 { + if let response = (task.response as? HTTPURLResponse) { + if response.statusCode == 401 { + + } + if response.statusCode >= 200 && response.statusCode < 300 { if let error = error { nkError = NKError(error: error) } } else { - nkError = NKError(httpResponse: httpResponse) + nkError = NKError(httpResponse: response) } } else { if let error = error { From d4940fba6cb6f42ced084cdd53d29c509e53847f Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 15:48:11 +0100 Subject: [PATCH 104/135] cleaning Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKitBackground.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKitBackground.swift b/Sources/NextcloudKit/NextcloudKitBackground.swift index a7682024..2a80b817 100644 --- a/Sources/NextcloudKit/NextcloudKitBackground.swift +++ b/Sources/NextcloudKit/NextcloudKitBackground.swift @@ -161,9 +161,6 @@ public final class NKBackground: NSObject, URLSessionTaskDelegate, URLSessionDel var nkError: NKError = .success if let response = (task.response as? HTTPURLResponse) { - if response.statusCode == 401 { - - } if response.statusCode >= 200 && response.statusCode < 300 { if let error = error { nkError = NKError(error: error) From 5539a14007e7b6a83559a25d8ceb7ece6edbdd62 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 18:08:49 +0100 Subject: [PATCH 105/135] Interceptor Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index 018ecbd1..e3d3041f 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -71,6 +71,7 @@ public struct NKSession: Sendable { rootQueue: nkCommonInstance.rootQueue, requestQueue: nkCommonInstance.requestQueue, serializationQueue: nkCommonInstance.serializationQueue, + interceptor: NKInterceptor.shared, eventMonitors: [NKMonitor(nkCommonInstance: nkCommonInstance)]) /// Session Download Background From 860c10408d9bd3e8fda79f0007ad1da0537d7fe4 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 18:16:35 +0100 Subject: [PATCH 106/135] INterceptor Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKInterceptor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index 80bc9d16..29e660c4 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -5,7 +5,7 @@ import Foundation import Alamofire -final class NKInterceptor: RequestInterceptor, Sendable { +class NKInterceptor: RequestInterceptor, @unchecked Sendable { static let shared = Interceptor() func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { From e19c0747788841ad1a7b5e23befa825d07171493 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 18:24:36 +0100 Subject: [PATCH 107/135] sendable Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKInterceptor.swift | 3 +- Sources/NextcloudKit/NKSession.swift | 2 +- Sources/NextcloudKit/NextcloudKit+API.swift | 28 +++++++++---------- .../NextcloudKit/NextcloudKit+Assistant.swift | 10 +++---- .../NextcloudKit/NextcloudKit+Comments.swift | 10 +++---- .../NextcloudKit/NextcloudKit+Dashboard.swift | 4 +-- .../NextcloudKit/NextcloudKit+Download.swift | 2 +- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 22 +++++++-------- .../NextcloudKit/NextcloudKit+FilesLock.swift | 2 +- .../NextcloudKit+Groupfolders.swift | 2 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 2 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 2 +- .../NextcloudKit/NextcloudKit+NCText.swift | 8 +++--- .../NextcloudKit+PushNotification.swift | 8 +++--- .../NextcloudKit+RecommendedFiles.swift | 2 +- .../NextcloudKit+Richdocuments.swift | 8 +++--- .../NextcloudKit/NextcloudKit+Search.swift | 4 +-- Sources/NextcloudKit/NextcloudKit+Share.swift | 10 +++---- .../NextcloudKit+ShareDownloadLimit.swift | 6 ++-- .../NextcloudKit+TermsOfService.swift | 4 +-- .../NextcloudKit/NextcloudKit+Upload.swift | 2 +- .../NextcloudKit+UserStatus.swift | 14 +++++----- .../NextcloudKit/NextcloudKit+WebDAV.swift | 18 ++++++------ Sources/NextcloudKit/NextcloudKit.swift | 1 - 24 files changed, 86 insertions(+), 88 deletions(-) diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index 29e660c4..1ac6c9eb 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -5,8 +5,7 @@ import Foundation import Alamofire -class NKInterceptor: RequestInterceptor, @unchecked Sendable { - static let shared = Interceptor() +final class NKInterceptor: RequestInterceptor, Sendable { func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index e3d3041f..9c719370 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -71,7 +71,7 @@ public struct NKSession: Sendable { rootQueue: nkCommonInstance.rootQueue, requestQueue: nkCommonInstance.requestQueue, serializationQueue: nkCommonInstance.serializationQueue, - interceptor: NKInterceptor.shared, + interceptor: NKInterceptor(), eventMonitors: [NKMonitor(nkCommonInstance: nkCommonInstance)]) /// Session Download Background diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index efb0af7b..48b13f1a 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -71,7 +71,7 @@ public extension NextcloudKit { } let method = HTTPMethod(rawValue: method.uppercased()) - nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -102,7 +102,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, externalSites, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -222,7 +222,7 @@ public extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -264,7 +264,7 @@ public extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -301,7 +301,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, width, height, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -339,7 +339,7 @@ public extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -433,7 +433,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -463,7 +463,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -532,7 +532,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -660,7 +660,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -736,7 +736,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -819,7 +819,7 @@ public extension NextcloudKit { } let method = HTTPMethod(rawValue: method) - nkSession.sessionData.request(urlRequest, method: method, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -854,7 +854,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -898,7 +898,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index bdb4d8e6..ef3bc5a3 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -18,7 +18,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -59,7 +59,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -96,7 +96,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -133,7 +133,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -170,7 +170,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index 0a8a0bea..f4b8a277 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -32,7 +32,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -82,7 +82,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -129,7 +129,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -162,7 +162,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -207,7 +207,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 4676de86..8c83f768 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -58,7 +58,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Download.swift b/Sources/NextcloudKit/NextcloudKit+Download.swift index bf4e55df..d06cf410 100644 --- a/Sources/NextcloudKit/NextcloudKit+Download.swift +++ b/Sources/NextcloudKit/NextcloudKit+Download.swift @@ -33,7 +33,7 @@ public extension NextcloudKit { } destination = destinationFile - let request = nkSession.sessionData.download(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor, to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let request = nkSession.sessionData.download(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(), to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } } .downloadProgress { progress in diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index 9e2175e3..a6839f9a 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -25,7 +25,7 @@ public extension NextcloudKit { } let method: HTTPMethod = delete ? .delete : .put - nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -77,7 +77,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-COUNTER", value: e2eCounter) } - nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -122,7 +122,7 @@ public extension NextcloudKit { parameters["e2e-token"] = e2eToken } - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -177,7 +177,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-SIGNATURE", value: signature) } - nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -228,7 +228,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -272,7 +272,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -311,7 +311,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -352,7 +352,7 @@ public extension NextcloudKit { } let parameters = ["csr": certificate] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -394,7 +394,7 @@ public extension NextcloudKit { } let parameters = ["privateKey": privateKey] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -432,7 +432,7 @@ public extension NextcloudKit { let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -464,7 +464,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index 87be6bce..2082e324 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -24,7 +24,7 @@ public extension NextcloudKit { } headers.update(name: "X-User-Lock", value: "1") - nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index db5b7421..7e3babaa 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index c775c4bc..c3e42737 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 21fb08a4..3b994f96 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -32,7 +32,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index 568c526e..380fa3f9 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -20,7 +20,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, editors, creators, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -90,7 +90,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -121,7 +121,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -174,7 +174,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index efdf9464..d369bdcc 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -27,7 +27,7 @@ public extension NextcloudKit { "proxyServer": proxyServerUrl ] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -65,7 +65,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -105,7 +105,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -143,7 +143,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - nkSession.sessionData.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift index 8a5a10e8..68be9600 100644 --- a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift +++ b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift @@ -22,7 +22,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index 12248684..88d741eb 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -20,7 +20,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["fileId": fileID] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -55,7 +55,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -104,7 +104,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["path": path, "template": templateId] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -140,7 +140,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["path": path] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index f3d7b636..423d44fb 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -40,7 +40,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - let requestUnifiedSearch = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestUnifiedSearch = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -131,7 +131,7 @@ public extension NextcloudKit { return nil } - let requestSearchProvider = nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let requestSearchProvider = nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 80cc2c32..635725ae 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -67,7 +67,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -130,7 +130,7 @@ public extension NextcloudKit { "lookup": lookupString ] - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -300,7 +300,7 @@ public extension NextcloudKit { parameters["attributes"] = attributes } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -385,7 +385,7 @@ public extension NextcloudKit { parameters["attributes"] = "[]" } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -423,7 +423,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift index 50df97f7..02d2a84e 100644 --- a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift +++ b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift @@ -25,7 +25,7 @@ public extension NextcloudKit { nkSession .sessionData - .request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor) + .request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()) .validate(statusCode: 200..<300) .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -94,7 +94,7 @@ public extension NextcloudKit { nkSession .sessionData - .request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor) + .request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()) .validate(statusCode: 200..<300) .response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -136,7 +136,7 @@ public extension NextcloudKit { nkSession .sessionData - .request(urlRequest, interceptor: nkInterceptor) + .request(urlRequest, interceptor: NKInterceptor()) .validate(statusCode: 200..<300) .response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift b/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift index 9207c45d..22aaa277 100644 --- a/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift +++ b/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -70,7 +70,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index 5c5e4e74..6d78b5f4 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -43,7 +43,7 @@ public extension NextcloudKit { headers.update(name: "Overwrite", value: "true") } - let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: nkInterceptor, fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in + let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: NKInterceptor(), fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } }) .uploadProgress { progress in diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index 8e61b9ce..732ae404 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -22,7 +22,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -73,7 +73,7 @@ public extension NextcloudKit { "statusType": String(status) ] - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -115,7 +115,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -162,7 +162,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -197,7 +197,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -232,7 +232,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -289,7 +289,7 @@ public extension NextcloudKit { "offset": String(offset) ] - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index af194856..c27be842 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -26,7 +26,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -70,7 +70,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -114,7 +114,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -159,7 +159,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -216,7 +216,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -386,7 +386,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -437,7 +437,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -483,7 +483,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -538,7 +538,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, items, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: nkInterceptor).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 415635db..7eab9425 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -21,7 +21,6 @@ open class NextcloudKit { private let reachabilityManager = Alamofire.NetworkReachabilityManager() #endif public var nkCommonInstance = NKCommon() - public let nkInterceptor = NKInterceptor.shared internal lazy var internalSession: Alamofire.Session = { return Alamofire.Session(configuration: URLSessionConfiguration.af.default, delegate: NextcloudKitSessionDelegate(nkCommonInstance: nkCommonInstance), From f858b07dc1301394d4137f28ed866dc73cec893b Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 18:30:56 +0100 Subject: [PATCH 108/135] test Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index 9c719370..012a26ad 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -71,7 +71,7 @@ public struct NKSession: Sendable { rootQueue: nkCommonInstance.rootQueue, requestQueue: nkCommonInstance.requestQueue, serializationQueue: nkCommonInstance.serializationQueue, - interceptor: NKInterceptor(), + // interceptor: NKInterceptor(), eventMonitors: [NKMonitor(nkCommonInstance: nkCommonInstance)]) /// Session Download Background From 2d6709af5f7edfd693587be24caff6e0810f027f Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 18:34:43 +0100 Subject: [PATCH 109/135] cleaning Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKSession.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/NextcloudKit/NKSession.swift b/Sources/NextcloudKit/NKSession.swift index 012a26ad..018ecbd1 100644 --- a/Sources/NextcloudKit/NKSession.swift +++ b/Sources/NextcloudKit/NKSession.swift @@ -71,7 +71,6 @@ public struct NKSession: Sendable { rootQueue: nkCommonInstance.rootQueue, requestQueue: nkCommonInstance.requestQueue, serializationQueue: nkCommonInstance.serializationQueue, - // interceptor: NKInterceptor(), eventMonitors: [NKMonitor(nkCommonInstance: nkCommonInstance)]) /// Session Download Background From c764644551a12c22d785692fb62c0d3cb89b3005 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 1 Feb 2025 18:51:30 +0100 Subject: [PATCH 110/135] checkUnauthorized Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+API.swift | 28 +++++++++---------- .../NextcloudKit+PushNotification.swift | 4 +-- .../NextcloudKit+RecommendedFiles.swift | 2 +- .../NextcloudKit/NextcloudKit+WebDAV.swift | 18 ++++++------ 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 48b13f1a..485a51f4 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -66,7 +66,7 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: method.uppercased()) @@ -98,7 +98,7 @@ public extension NextcloudKit { let endpoint = "ocs/v2.php/apps/external/api/v1" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, externalSites, nil, .urlError) } } @@ -214,7 +214,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } if var etag = etag { @@ -255,7 +255,7 @@ public extension NextcloudKit { let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, width, height, nil, nil, .urlError) } } @@ -297,7 +297,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, width, height, nil, .urlError) } } @@ -330,7 +330,7 @@ public extension NextcloudKit { let endpoint = "index.php/avatar/\(user)/\(sizeImage)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } @@ -429,7 +429,7 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrl.asUrl, let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } @@ -459,7 +459,7 @@ public extension NextcloudKit { let endpoint = "ocs/v2.php/cloud/user" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -528,7 +528,7 @@ public extension NextcloudKit { let endpoint = "ocs/v1.php/cloud/capabilities" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } @@ -656,7 +656,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, nil, .urlError) } } @@ -732,7 +732,7 @@ public extension NextcloudKit { let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -804,7 +804,7 @@ public extension NextcloudKit { taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } var url: URLConvertible? @@ -850,7 +850,7 @@ public extension NextcloudKit { ] guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -887,7 +887,7 @@ public extension NextcloudKit { /// guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } var urlRequest: URLRequest diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index d369bdcc..f12067b0 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -18,7 +18,7 @@ public extension NextcloudKit { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } let parameters = [ @@ -61,7 +61,7 @@ public extension NextcloudKit { let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } diff --git a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift index 68be9600..c4419147 100644 --- a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift +++ b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift @@ -18,7 +18,7 @@ public extension NextcloudKit { /// guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index c27be842..15b2863f 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -14,7 +14,7 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlFileName.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: "MKCOL") @@ -59,7 +59,7 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlFileName.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } var urlRequest: URLRequest @@ -96,7 +96,7 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlFileNameSource.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "MOVE") @@ -140,7 +140,7 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { guard let url = serverUrlFileNameSource.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "COPY") @@ -192,7 +192,7 @@ public extension NextcloudKit { /// guard let nkSession = nkCommonInstance.getSession(account: account), let url = serverUrlFileName.encodedToUrl, - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } if depth == "0", serverUrlFileName.last == "/" { @@ -369,7 +369,7 @@ public extension NextcloudKit { options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } var files: [NKFile] = [] @@ -418,7 +418,7 @@ public extension NextcloudKit { options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + "/" + fileName @@ -464,7 +464,7 @@ public extension NextcloudKit { options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId @@ -515,7 +515,7 @@ public extension NextcloudKit { options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } var serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/trashbin/" + nkSession.userId + "/trash/" From 97dedf74925f7bdc85b7a8b5a762d208cf0e953a Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 2 Feb 2025 12:40:52 +0100 Subject: [PATCH 111/135] debug Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKInterceptor.swift | 2 +- Sources/NextcloudKit/NKMonitor.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index 1ac6c9eb..8dc1f2ca 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -6,7 +6,6 @@ import Foundation import Alamofire final class NKInterceptor: RequestInterceptor, Sendable { - func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) @@ -16,6 +15,7 @@ final class NKInterceptor: RequestInterceptor, Sendable { if let account = urlRequest.value(forHTTPHeaderField: "X-NC-Account"), let unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String], unauthorizedArray.contains(account) { + print("Unauthorized for account: \(account)") let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 401)) return completion(.failure(error)) } diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 2e6febcf..7e322ad8 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -42,6 +42,8 @@ final class NKMonitor: EventMonitor, Sendable { if !unauthorizedArray.contains(account) { unauthorizedArray.append(account) groupDefaults?.set(unauthorizedArray, forKey: "Unauthorized") + + self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") } } From 788aadf6c53edf52e056ec433dce84a49864d5bb Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 2 Feb 2025 12:56:42 +0100 Subject: [PATCH 112/135] X-NC-CheckUnauthorized Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 7e322ad8..2897ffb4 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -34,8 +34,8 @@ final class NKMonitor: EventMonitor, Sendable { // if let statusCode = response.response?.statusCode, statusCode == 401, - let isCheckUnauthorized = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"] as? Bool, - isCheckUnauthorized, + let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], + headerValue.lowercased() == "true", let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] From 8fe27d52c74c1405c1fbed3c8ab204c94ea0e505 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 2 Feb 2025 17:48:21 +0100 Subject: [PATCH 113/135] test Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 2897ffb4..64f622d0 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -32,19 +32,22 @@ final class NKMonitor: EventMonitor, Sendable { // // Error 401, append the account in groupDefaults Unauthorized array // - if let statusCode = response.response?.statusCode, - statusCode == 401, - let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], - headerValue.lowercased() == "true", - let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { + if let statusCode = response.response?.statusCode { + if statusCode == 401, + let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], + headerValue.lowercased() == "true", + let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { - var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] - if !unauthorizedArray.contains(account) { - unauthorizedArray.append(account) - groupDefaults?.set(unauthorizedArray, forKey: "Unauthorized") + var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] + if !unauthorizedArray.contains(account) { + unauthorizedArray.append(account) + groupDefaults?.set(unauthorizedArray, forKey: "Unauthorized") - self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") - } + self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") + } + } else if statusCode == 503 { + print("503 Service Unavailable") + } } // From b5c71ed32a96ea6c3210b7c42fa86aca8a77d68b Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 2 Feb 2025 18:08:16 +0100 Subject: [PATCH 114/135] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 58 +++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 64f622d0..b422e850 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -37,13 +37,16 @@ final class NKMonitor: EventMonitor, Sendable { let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], headerValue.lowercased() == "true", let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { + self.readFile(serverUrlFileName: "", account: account) { account, error in + /* + var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] + if !unauthorizedArray.contains(account) { + unauthorizedArray.append(account) + groupDefaults?.set(unauthorizedArray, forKey: "Unauthorized") - var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] - if !unauthorizedArray.contains(account) { - unauthorizedArray.append(account) - groupDefaults?.set(unauthorizedArray, forKey: "Unauthorized") - - self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") + self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") + } + */ } } else if statusCode == 503 { print("503 Service Unavailable") @@ -72,4 +75,47 @@ final class NKMonitor: EventMonitor, Sendable { } } } + + func readFile(serverUrlFileName: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ error: NKError) -> Void) { + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = serverUrlFileName.encodedToUrl, + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + return options.queue.async { completion(account, .urlError) } + } + + let method = HTTPMethod(rawValue: "PROPFIND") + headers.update(name: "Depth", value: "0") + var urlRequest: URLRequest + + do { + try urlRequest = URLRequest(url: url, method: method, headers: headers) + urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFile(createProperties: options.createProperties, removeProperties: options.removeProperties).data(using: .utf8) + + } catch { + return options.queue.async { completion(account, NKError(error: error)) } + } + + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, error) } + case .success: + options.queue.async { completion(account, .success) } + } + } + } } From 8b9be417c1130f35de15f148e17d34f9b139a747 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 2 Feb 2025 18:19:20 +0100 Subject: [PATCH 115/135] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index b422e850..0f6475f7 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -36,8 +36,10 @@ final class NKMonitor: EventMonitor, Sendable { if statusCode == 401, let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], headerValue.lowercased() == "true", - let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { - self.readFile(serverUrlFileName: "", account: account) { account, error in + let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String, + let session = nkCommonInstance.nksessions.filter({ $0.account == account }).first { + let serverUrlFileName = session.urlBase + "/remote.php/dav/files/" + session.userId + self.readFile(serverUrlFileName: serverUrlFileName, account: account) { account, error in /* var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] if !unauthorizedArray.contains(account) { From 29ba81a58be1f5716892e033aa106840f9471493 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 2 Feb 2025 18:26:49 +0100 Subject: [PATCH 116/135] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 0f6475f7..b9edea5b 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -37,7 +37,7 @@ final class NKMonitor: EventMonitor, Sendable { let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], headerValue.lowercased() == "true", let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String, - let session = nkCommonInstance.nksessions.filter({ $0.account == account }).first { + let session = nkCommonInstance.getSession(account: account) { let serverUrlFileName = session.urlBase + "/remote.php/dav/files/" + session.userId self.readFile(serverUrlFileName: serverUrlFileName, account: account) { account, error in /* From ace908baacf8cbc83e96238ccd74e4d7e47e88ce Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 3 Feb 2025 06:58:28 +0100 Subject: [PATCH 117/135] common (#121) * fix Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 2 - Sources/NextcloudKit/NKMonitor.swift | 100 +++++++++------------------ 2 files changed, 33 insertions(+), 69 deletions(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index b20237b0..d87296af 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -25,8 +25,6 @@ public protocol NextcloudKitDelegate: AnyObject, Sendable { func downloadComplete(fileName: String, serverUrl: String, etag: String?, date: Date?, dateLastModified: Date?, length: Int64, task: URLSessionTask, error: NKError) func uploadComplete(fileName: String, serverUrl: String, ocId: String?, etag: String?, date: Date?, size: Int64, task: URLSessionTask, error: NKError) - - func request(_ request: DataRequest, didParseResponse response: AFDataResponse) } public struct NKCommon: Sendable { diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index b9edea5b..3369eacc 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -26,33 +26,42 @@ final class NKMonitor: EventMonitor, Sendable { } func request(_ request: DataRequest, didParseResponse response: AFDataResponse) { - self.nkCommonInstance.delegate?.request(request, didParseResponse: response) let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) - // - // Error 401, append the account in groupDefaults Unauthorized array - // if let statusCode = response.response?.statusCode { - if statusCode == 401, - let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], - headerValue.lowercased() == "true", - let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String, - let session = nkCommonInstance.getSession(account: account) { - let serverUrlFileName = session.urlBase + "/remote.php/dav/files/" + session.userId - self.readFile(serverUrlFileName: serverUrlFileName, account: account) { account, error in - /* - var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] - if !unauthorizedArray.contains(account) { - unauthorizedArray.append(account) - groupDefaults?.set(unauthorizedArray, forKey: "Unauthorized") - - self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") - } - */ - } - } else if statusCode == 503 { - print("503 Service Unavailable") - } + + // + // Error 401, append the account in groupDefaults Unauthorized array + // + + if statusCode == 401, + let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], + headerValue.lowercased() == "true", + let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { + + var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] + if !unauthorizedArray.contains(account) { + unauthorizedArray.append(account) + groupDefaults?.set(unauthorizedArray, forKey: "Unauthorized") + + self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") + } + + + // + // Error 503, append the account in groupDefaults Unavailable array + // + } else if statusCode == 503, + let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { + + var unauthorizedArray = groupDefaults?.array(forKey: "Unavailable") as? [String] ?? [] + if !unauthorizedArray.contains(account) { + unauthorizedArray.append(account) + groupDefaults?.set(unauthorizedArray, forKey: "Unavailable") + + self.nkCommonInstance.writeLog("Unavailable set for account: \(account)") + } + } } // @@ -77,47 +86,4 @@ final class NKMonitor: EventMonitor, Sendable { } } } - - func readFile(serverUrlFileName: String, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - /// - options.contentType = "application/xml" - /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = serverUrlFileName.encodedToUrl, - var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { - return options.queue.async { completion(account, .urlError) } - } - - let method = HTTPMethod(rawValue: "PROPFIND") - headers.update(name: "Depth", value: "0") - var urlRequest: URLRequest - - do { - try urlRequest = URLRequest(url: url, method: method, headers: headers) - urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodyFile(createProperties: options.createProperties, removeProperties: options.removeProperties).data(using: .utf8) - - } catch { - return options.queue.async { completion(account, NKError(error: error)) } - } - - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - if self.nkCommonInstance.levelLog > 0 { - debugPrint(response) - } - switch response.result { - case .failure(let error): - let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, error) } - case .success: - options.queue.async { completion(account, .success) } - } - } - } } From f8f9a764427f5e7d4b65e8ceb97d5039ef68f41d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 3 Feb 2025 07:03:05 +0100 Subject: [PATCH 118/135] cleaning Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 2 +- Sources/NextcloudKit/NKMonitor.swift | 1 - Sources/NextcloudKit/NextcloudKit.swift | 6 +++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index d87296af..9fb0c219 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -30,7 +30,7 @@ public protocol NextcloudKitDelegate: AnyObject, Sendable { public struct NKCommon: Sendable { public var nksessions = ThreadSafeArray() public var delegate: NextcloudKitDelegate? - public var groupIdentifier: String = "" + public var groupIdentifier: String? public let identifierSessionDownload: String = "com.nextcloud.nextcloudkit.session.download" public let identifierSessionUpload: String = "com.nextcloud.nextcloudkit.session.upload" diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 3369eacc..a481898d 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -47,7 +47,6 @@ final class NKMonitor: EventMonitor, Sendable { self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") } - // // Error 503, append the account in groupDefaults Unavailable array // diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 7eab9425..50ca63c3 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -49,7 +49,11 @@ open class NextcloudKit { // MARK: - Session setup - public func setup(groupIdentifier: String, delegate: NextcloudKitDelegate?, memoryCapacity: Int = 30, diskCapacity: Int = 500, removeAllCachedResponses: Bool = false) { + public func setup(groupIdentifier: String? = nil, + delegate: NextcloudKitDelegate? = nil, + memoryCapacity: Int = 30, + diskCapacity: Int = 500, + removeAllCachedResponses: Bool = false) { self.nkCommonInstance.delegate = delegate self.nkCommonInstance.groupIdentifier = groupIdentifier From 893762a51dd8994b71ab4bdf5dedcbddddc77c8a Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 3 Feb 2025 07:20:45 +0100 Subject: [PATCH 119/135] cleaning Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index a481898d..2b69857e 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -53,10 +53,10 @@ final class NKMonitor: EventMonitor, Sendable { } else if statusCode == 503, let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { - var unauthorizedArray = groupDefaults?.array(forKey: "Unavailable") as? [String] ?? [] - if !unauthorizedArray.contains(account) { - unauthorizedArray.append(account) - groupDefaults?.set(unauthorizedArray, forKey: "Unavailable") + var unavailableArray = groupDefaults?.array(forKey: "Unavailable") as? [String] ?? [] + if !unavailableArray.contains(account) { + unavailableArray.append(account) + groupDefaults?.set(unavailableArray, forKey: "Unavailable") self.nkCommonInstance.writeLog("Unavailable set for account: \(account)") } From 1c2ceeeeec7eca20d4abf39b616c0134dbfa2b00 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 3 Feb 2025 08:16:51 +0100 Subject: [PATCH 120/135] added checkUnauthorized options Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKRequestOptions.swift | 3 + Sources/NextcloudKit/NextcloudKit+API.swift | 84 +++++++++++++++---- .../NextcloudKit+PushNotification.swift | 12 ++- .../NextcloudKit+RecommendedFiles.swift | 6 +- .../NextcloudKit/NextcloudKit+WebDAV.swift | 54 ++++++++++-- 5 files changed, 133 insertions(+), 26 deletions(-) diff --git a/Sources/NextcloudKit/NKRequestOptions.swift b/Sources/NextcloudKit/NKRequestOptions.swift index 4b586a86..2c2baa68 100644 --- a/Sources/NextcloudKit/NKRequestOptions.swift +++ b/Sources/NextcloudKit/NKRequestOptions.swift @@ -16,6 +16,7 @@ public class NKRequestOptions: NSObject { var taskDescription: String? var createProperties: [NKProperties]? var removeProperties: [NKProperties] + var checkUnauthorized: Bool? var paginate: Bool var paginateToken: String? var paginateOffset: Int? @@ -32,6 +33,7 @@ public class NKRequestOptions: NSObject { taskDescription: String? = nil, createProperties: [NKProperties]? = nil, removeProperties: [NKProperties] = [], + checkUnauthorized: Bool? = nil, paginate: Bool = false, paginateToken: String? = nil, paginateOffset: Int? = nil, @@ -48,6 +50,7 @@ public class NKRequestOptions: NSObject { self.taskDescription = taskDescription self.createProperties = createProperties self.removeProperties = removeProperties + self.checkUnauthorized = checkUnauthorized self.paginate = paginate self.paginateToken = paginateToken self.paginateOffset = paginateOffset diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 485a51f4..d234ddc0 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -64,9 +64,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: method.uppercased()) @@ -96,9 +100,13 @@ public extension NextcloudKit { completion: @escaping (_ account: String, _ externalFiles: [NKExternalSite], _ responseData: AFDataResponse?, _ error: NKError) -> Void) { var externalSites: [NKExternalSite] = [] let endpoint = "ocs/v2.php/apps/external/api/v1" + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, externalSites, nil, .urlError) } } @@ -213,8 +221,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } guard let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } if var etag = etag { @@ -252,10 +264,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ width: Int, _ height: Int, _ etag: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "index.php/core/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, width, height, nil, nil, .urlError) } } @@ -293,11 +309,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ width: Int, _ height: Int, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "index.php/apps/files_trashbin/preview?fileId=\(fileId)&x=\(width)&y=\(height)&a=\(crop)&mode=\(cropMode)&forceIcon=\(forceIcon)&mimeFallback=\(mimeFallback)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, width, height, nil, .urlError) } } @@ -327,10 +347,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ imageAvatar: UIImage?, _ imageOriginal: UIImage?, _ etag: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "index.php/avatar/\(user)/\(sizeImage)" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } @@ -427,9 +451,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } guard let url = serverUrl.asUrl, let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } @@ -456,10 +484,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ userProfile: NKUserProfile?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "ocs/v2.php/cloud/user" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -525,10 +557,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "ocs/v1.php/cloud/capabilities" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } @@ -632,6 +668,10 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ activities: [NKActivity], _ activityFirstKnown: Int, _ activityLastGiven: Int, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } var activities: [NKActivity] = [] var activityFirstKnown = 0 var activityLastGiven = 0 @@ -656,7 +696,7 @@ public extension NextcloudKit { guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, nil, .urlError) } } @@ -729,10 +769,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ notifications: [NKNotifications]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "ocs/v2.php/apps/notifications/api/v2/notifications" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -803,8 +847,12 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } var url: URLConvertible? @@ -843,6 +891,10 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ url: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "ocs/v2.php/apps/dav/api/v1/direct" let parameters: [String: Any] = [ "fileId": fileId, @@ -850,7 +902,7 @@ public extension NextcloudKit { ] guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } @@ -881,13 +933,17 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "ocs/v2.php/apps/security_guard/diagnostics" /// options.contentType = "application/json" /// guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } var urlRequest: URLRequest diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index f12067b0..e97ef4be 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -15,10 +15,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ deviceIdentifier: String?, _ signature: String?, _ publicKey: String?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, nil, nil, .urlError) } } let parameters = [ @@ -58,10 +62,14 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "ocs/v2.php/apps/notifications/api/v2/push" guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } diff --git a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift index c4419147..891cbcbc 100644 --- a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift +++ b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift @@ -12,13 +12,17 @@ public extension NextcloudKit { request: @escaping (DataRequest?) -> Void = { _ in }, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ recommendations: [NKRecommendation]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } let endpoint = "ocs/v2.php/apps/recommendations/api/v1/recommendations" /// options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 15b2863f..c1ad7704 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -12,9 +12,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ ocId: String?, _ date: Date?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } guard let url = serverUrlFileName.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } let method = HTTPMethod(rawValue: "MKCOL") @@ -57,9 +61,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } guard let url = serverUrlFileName.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } var urlRequest: URLRequest @@ -94,9 +102,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } guard let url = serverUrlFileNameSource.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "MOVE") @@ -138,9 +150,13 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } guard let url = serverUrlFileNameSource.encodedToUrl, let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let method = HTTPMethod(rawValue: "COPY") @@ -185,6 +201,10 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } var files: [NKFile] = [] var serverUrlFileName = serverUrlFileName /// @@ -192,7 +212,7 @@ public extension NextcloudKit { /// guard let nkSession = nkCommonInstance.getSession(account: account), let url = serverUrlFileName.encodedToUrl, - var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } if depth == "0", serverUrlFileName.last == "/" { @@ -365,11 +385,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } /// options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } var files: [NKFile] = [] @@ -414,11 +438,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } /// options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + "/" + fileName @@ -460,11 +488,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ files: [NKFile]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } /// options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), - let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + let headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId @@ -511,11 +543,15 @@ public extension NextcloudKit { options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, completion: @escaping (_ account: String, _ items: [NKTrash]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + var checkUnauthorized = true + if let optionsCheckUnauthorized = options.checkUnauthorized { + checkUnauthorized = optionsCheckUnauthorized + } /// options.contentType = "application/xml" /// guard let nkSession = nkCommonInstance.getSession(account: account), - var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: true, options: options) else { + var headers = nkCommonInstance.getStandardHeaders(account: account, checkUnauthorized: checkUnauthorized, options: options) else { return options.queue.async { completion(account, nil, nil, .urlError) } } var serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/trashbin/" + nkSession.userId + "/trash/" From 0abd7f4f1b27abfa49947c60b1e8b4b38a4a4778 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 3 Feb 2025 08:56:00 +0100 Subject: [PATCH 121/135] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKInterceptor.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index 8dc1f2ca..dce21cd1 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -12,6 +12,11 @@ final class NKInterceptor: RequestInterceptor, Sendable { // // Detect if exists in the groupDefaults Unauthorized array the account // + if let checkUnauthorized = urlRequest.value(forHTTPHeaderField: "X-NC-CheckUnauthorized"), + checkUnauthorized == "false" { + completion(.success(urlRequest)) + } + if let account = urlRequest.value(forHTTPHeaderField: "X-NC-Account"), let unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String], unauthorizedArray.contains(account) { From 2b034e19bf6a8bb9778a0177b5c5cb3f8c3d1580 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 3 Feb 2025 09:01:42 +0100 Subject: [PATCH 122/135] fix Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKCommon.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index 9fb0c219..e5a13ca8 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -425,7 +425,7 @@ public struct NKCommon: Sendable { return session } - public func getStandardHeaders(account: String, checkUnauthorized: Bool = false, options: NKRequestOptions? = nil) -> HTTPHeaders? { + public func getStandardHeaders(account: String, checkUnauthorized: Bool? = nil, options: NKRequestOptions? = nil) -> HTTPHeaders? { guard let session = nksessions.filter({ $0.account == account }).first else { return nil} var headers: HTTPHeaders = [] @@ -447,8 +447,8 @@ public struct NKCommon: Sendable { headers.update(name: key, value: value) } headers.update(name: "X-NC-Account", value: account) - if checkUnauthorized { - headers.update(name: "X-NC-CheckUnauthorized", value: "true") + if let checkUnauthorized { + headers.update(name: "X-NC-CheckUnauthorized", value: checkUnauthorized.description) } // Paginate if let options { From 9a53ffb5e24fbcfc3316cbdf8d50e37f65d68e20 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 3 Feb 2025 09:06:51 +0100 Subject: [PATCH 123/135] cleaning Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKInterceptor.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index dce21cd1..d7844530 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -14,10 +14,8 @@ final class NKInterceptor: RequestInterceptor, Sendable { // if let checkUnauthorized = urlRequest.value(forHTTPHeaderField: "X-NC-CheckUnauthorized"), checkUnauthorized == "false" { - completion(.success(urlRequest)) - } - - if let account = urlRequest.value(forHTTPHeaderField: "X-NC-Account"), + return completion(.success(urlRequest)) + } else if let account = urlRequest.value(forHTTPHeaderField: "X-NC-Account"), let unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String], unauthorizedArray.contains(account) { print("Unauthorized for account: \(account)") From a6002109e37ff86e82529b3fd68e0e20c5726b28 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 07:54:10 +0100 Subject: [PATCH 124/135] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKDataFileXML.swift | 58 ------------------- .../NextcloudKit/NextcloudKit+WebDAV.swift | 25 +++----- 2 files changed, 8 insertions(+), 75 deletions(-) diff --git a/Sources/NextcloudKit/NKDataFileXML.swift b/Sources/NextcloudKit/NKDataFileXML.swift index 6cd1310a..268b3d20 100644 --- a/Sources/NextcloudKit/NKDataFileXML.swift +++ b/Sources/NextcloudKit/NKDataFileXML.swift @@ -147,64 +147,6 @@ class NKDataFileXML: NSObject { } func getRequestBodySearchMedia(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { - let request = """ - - - - - - """ + NKProperties.properties(createProperties: createProperties, removeProperties: removeProperties) + """ - - - - - %@ - infinity - - - - - <%@> - - - - - - - - - - - - - image/%% - - - - video/%% - - - - - - <%@> - %@ - - - <%@> - %@ - - - - - - - - """ - return request - } - - func getRequestBodySearchMediaWithLimit(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { let request = """ diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index c1ad7704..4c95bcec 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -333,8 +333,6 @@ public extension NextcloudKit { greaterDate: Any, elementDate: String, limit: Int, - showHiddenFiles: Bool, - includeHiddenFiles: [String] = [], account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, @@ -343,6 +341,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } let files: [NKFile] = [] + let elementDate = elementDate + "/" var greaterDateString: String?, lessDateString: String? let href = "/files/" + nkSession.userId + path if let lessDate = lessDate as? Date { @@ -355,25 +354,17 @@ public extension NextcloudKit { } else if let greaterDate = greaterDate as? Int { greaterDateString = String(greaterDate) } - if lessDateString == nil || greaterDateString == nil { + guard let lessDateString, let greaterDateString else { return options.queue.async { completion(account, files, nil, .invalidDate) } } - var requestBody = "" - - if let lessDateString, let greaterDateString { - if limit > 0 { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMediaWithLimit(createProperties: options.createProperties, removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString, String(limit)) - } else { - requestBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMedia(createProperties: options.createProperties, removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString) - } + guard let httpBody = String(format: NKDataFileXML(nkCommonInstance: self.nkCommonInstance).getRequestBodySearchMedia(createProperties: options.createProperties, removeProperties: options.removeProperties), href, elementDate, elementDate, lessDateString, elementDate, greaterDateString, String(limit)).data(using: .utf8) else { + return options.queue.async { completion(account, files, nil, .invalidData) } } - if let httpBody = requestBody.data(using: .utf8) { - search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: showHiddenFiles, includeHiddenFiles: includeHiddenFiles, account: account, options: options) { task in - taskHandler(task) - } completion: { account, files, responseData, error in - options.queue.async { completion(account, files, responseData, error) } - } + search(serverUrl: nkSession.urlBase, httpBody: httpBody, showHiddenFiles: false, includeHiddenFiles: [], account: account, options: options) { task in + taskHandler(task) + } completion: { account, files, responseData, error in + options.queue.async { completion(account, files, responseData, error) } } } From e5a99699902850e737f758d936b7b04d284b2bdc Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 09:24:05 +0100 Subject: [PATCH 125/135] setupLog Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index 50ca63c3..d34165df 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -69,6 +69,14 @@ open class NextcloudKit { } } + public func setupLog(pathLog: String, + levelLog: Int, + copyLogToDocumentDirectory: Bool) { + self.nkCommonInstance.pathLog = pathLog + self.nkCommonInstance.levelLog = levelLog + self.nkCommonInstance.copyLogToDocumentDirectory = copyLogToDocumentDirectory + } + public func appendSession(account: String, urlBase: String, user: String, From 34ee929c088bac8318fe4efe32edf40e2fbe5f15 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 09:38:38 +0100 Subject: [PATCH 126/135] dependencies version updated Signed-off-by: Marino Faggiana --- Package.resolved | 8 ++++---- Package.swift | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Package.resolved b/Package.resolved index bff4ffdc..cfe19b9c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire", "state" : { - "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", - "version" : "5.9.1" + "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", + "version" : "5.10.2" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/WeTransfer/Mocker.git", "state" : { - "revision" : "5d86f27a8f80d4ba388bc1a379a3c2289a1f3d18", - "version" : "2.6.0" + "revision" : "95fa785c751f6bc40c49e112d433c3acf8417a97", + "version" : "3.0.2" } }, { diff --git a/Package.swift b/Package.swift index 05616674..f38c574d 100644 --- a/Package.swift +++ b/Package.swift @@ -22,10 +22,10 @@ let package = Package( targets: ["NextcloudKit"]), ], dependencies: [ - .package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "2.3.0")), - .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.9.1")), - .package(url: "https://github.com/SwiftyJSON/SwiftyJSON", .upToNextMajor(from: "5.0.0")), - .package(url: "https://github.com/yahoojapan/SwiftyXMLParser", .upToNextMajor(from: "5.3.0")), + .package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "3.0.2")), + .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.10.2")), + .package(url: "https://github.com/SwiftyJSON/SwiftyJSON", .upToNextMajor(from: "5.0.2")), + .package(url: "https://github.com/yahoojapan/SwiftyXMLParser", .upToNextMajor(from: "5.6.0")), ], targets: [ .target( From 2ece4c73f8bb6a3e2b766aa5eaa3d1f5ed085a19 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 11:13:55 +0100 Subject: [PATCH 127/135] improved code Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+API.swift | 24 +++++++++++-------- Sources/NextcloudKit/NextcloudKit+Login.swift | 6 ++--- Sources/NextcloudKit/NextcloudKit.swift | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index d234ddc0..87aee675 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -39,7 +39,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, .urlError) } } - internalSession.request(url, method: .head, encoding: URLEncoding.default).onURLSessionTaskCreation { task in + unauthorizedSession.request(url, method: .head, encoding: URLEncoding.default).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -172,7 +172,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - internalSession.request(url, method: .get, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + unauthorizedSession.request(url, method: .get, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -598,13 +598,15 @@ public extension NextcloudKit { /// options.contentType = "application/json" /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { return options.queue.async { completion(account, false, nil, .urlError) } } + var headers: HTTPHeaders? + if let userAgent = options.customUserAgent { + headers = [HTTPHeader.userAgent(userAgent)] + } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + unauthorizedSession.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -634,13 +636,15 @@ public extension NextcloudKit { /// options.contentType = "application/json" /// - guard let nkSession = nkCommonInstance.getSession(account: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + guard let url = nkCommonInstance.createStandardUrl(serverUrl: serverUrl, endpoint: endpoint, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } + var headers: HTTPHeaders? + if let userAgent = options.customUserAgent { + headers = [HTTPHeader.userAgent(userAgent)] + } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + unauthorizedSession.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Login.swift b/Sources/NextcloudKit/NextcloudKit+Login.swift index 2f4ae8da..251a0a56 100644 --- a/Sources/NextcloudKit/NextcloudKit+Login.swift +++ b/Sources/NextcloudKit/NextcloudKit+Login.swift @@ -32,7 +32,7 @@ public extension NextcloudKit { return options.queue.async { completion(nil, nil, NKError(error: error)) } } - internalSession.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + unauthorizedSession.request(urlRequest).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -112,7 +112,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - internalSession.request(url, method: .post, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + unauthorizedSession.request(url, method: .post, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -149,7 +149,7 @@ public extension NextcloudKit { headers = [HTTPHeader.userAgent(userAgent)] } - internalSession.request(url, method: .post, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + unauthorizedSession.request(url, method: .post, encoding: URLEncoding.default, headers: headers).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit.swift b/Sources/NextcloudKit/NextcloudKit.swift index d34165df..3c73daf6 100644 --- a/Sources/NextcloudKit/NextcloudKit.swift +++ b/Sources/NextcloudKit/NextcloudKit.swift @@ -21,7 +21,7 @@ open class NextcloudKit { private let reachabilityManager = Alamofire.NetworkReachabilityManager() #endif public var nkCommonInstance = NKCommon() - internal lazy var internalSession: Alamofire.Session = { + internal lazy var unauthorizedSession: Alamofire.Session = { return Alamofire.Session(configuration: URLSessionConfiguration.af.default, delegate: NextcloudKitSessionDelegate(nkCommonInstance: nkCommonInstance), eventMonitors: [NKMonitor(nkCommonInstance: self.nkCommonInstance)]) From 3ab309df96f4fbd25c003ff93e79dd579d3c38df Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 11:44:38 +0100 Subject: [PATCH 128/135] improved Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 2b69857e..9e7a3c77 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -37,12 +37,14 @@ final class NKMonitor: EventMonitor, Sendable { if statusCode == 401, let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], headerValue.lowercased() == "true", - let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { + let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String, + let groupDefaults { - var unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String] ?? [] + var unauthorizedArray = groupDefaults.array(forKey: "Unauthorized") as? [String] ?? [] if !unauthorizedArray.contains(account) { unauthorizedArray.append(account) - groupDefaults?.set(unauthorizedArray, forKey: "Unauthorized") + groupDefaults.set(unauthorizedArray, forKey: "Unauthorized") + groupDefaults.synchronize() self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") } @@ -51,12 +53,14 @@ final class NKMonitor: EventMonitor, Sendable { // Error 503, append the account in groupDefaults Unavailable array // } else if statusCode == 503, - let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { + let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String, + let groupDefaults { - var unavailableArray = groupDefaults?.array(forKey: "Unavailable") as? [String] ?? [] + var unavailableArray = groupDefaults.array(forKey: "Unavailable") as? [String] ?? [] if !unavailableArray.contains(account) { unavailableArray.append(account) - groupDefaults?.set(unavailableArray, forKey: "Unavailable") + groupDefaults.set(unavailableArray, forKey: "Unavailable") + groupDefaults.synchronize() self.nkCommonInstance.writeLog("Unavailable set for account: \(account)") } From a4ea592a7a0c5a7e1231fb373dab715ef2155702 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 12:04:08 +0100 Subject: [PATCH 129/135] cleaning Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKInterceptor.swift | 7 +++---- Sources/NextcloudKit/NKMonitor.swift | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index d7844530..b4c6dbb3 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -7,8 +7,6 @@ import Alamofire final class NKInterceptor: RequestInterceptor, Sendable { func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { - let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) - // // Detect if exists in the groupDefaults Unauthorized array the account // @@ -16,8 +14,9 @@ final class NKInterceptor: RequestInterceptor, Sendable { checkUnauthorized == "false" { return completion(.success(urlRequest)) } else if let account = urlRequest.value(forHTTPHeaderField: "X-NC-Account"), - let unauthorizedArray = groupDefaults?.array(forKey: "Unauthorized") as? [String], - unauthorizedArray.contains(account) { + let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier), + let unauthorizedArray = groupDefaults.array(forKey: "Unauthorized") as? [String], + unauthorizedArray.contains(account) { print("Unauthorized for account: \(account)") let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 401)) return completion(.failure(error)) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 9e7a3c77..1f16c601 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -26,8 +26,6 @@ final class NKMonitor: EventMonitor, Sendable { } func request(_ request: DataRequest, didParseResponse response: AFDataResponse) { - let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) - if let statusCode = response.response?.statusCode { // @@ -38,7 +36,7 @@ final class NKMonitor: EventMonitor, Sendable { let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], headerValue.lowercased() == "true", let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String, - let groupDefaults { + let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) { var unauthorizedArray = groupDefaults.array(forKey: "Unauthorized") as? [String] ?? [] if !unauthorizedArray.contains(account) { @@ -54,7 +52,7 @@ final class NKMonitor: EventMonitor, Sendable { // } else if statusCode == 503, let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String, - let groupDefaults { + let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier) { var unavailableArray = groupDefaults.array(forKey: "Unavailable") as? [String] ?? [] if !unavailableArray.contains(account) { From 0107c4ed4753097c1da03d5b1cb955e45ed3938d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 12:21:02 +0100 Subject: [PATCH 130/135] debugPrint Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/Models/NKTermsOfService.swift | 2 +- Sources/NextcloudKit/NKCommon.swift | 4 ++-- Sources/NextcloudKit/NKInterceptor.swift | 3 ++- Sources/NextcloudKit/Utils/FileAutoRenamer.swift | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/NextcloudKit/Models/NKTermsOfService.swift b/Sources/NextcloudKit/Models/NKTermsOfService.swift index 5d8b41ff..6c8bce43 100644 --- a/Sources/NextcloudKit/Models/NKTermsOfService.swift +++ b/Sources/NextcloudKit/Models/NKTermsOfService.swift @@ -19,7 +19,7 @@ public class NKTermsOfService: NSObject { self.data = decodedResponse.ocs.data return true } catch { - print("decode error:", error) + debugPrint("decode error:", error) return false } } diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index e5a13ca8..e84f9af4 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -370,7 +370,7 @@ public struct NKCommon: Sendable { writer = nil chunk = 0 counterChunk(counter) - print("Counter: \(counter)") + debugPrint("Counter: \(counter)") counter += 1 } @@ -532,7 +532,7 @@ public struct NKCommon: Sendable { let attributes = try FileManager.default.attributesOfItem(atPath: filePath) return attributes[FileAttributeKey.size] as? Int64 ?? 0 } catch { - print(error) + debugPrint(error) } return 0 } diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index b4c6dbb3..2e1b63fa 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -10,6 +10,7 @@ final class NKInterceptor: RequestInterceptor, Sendable { // // Detect if exists in the groupDefaults Unauthorized array the account // + debugPrint("[DEBUG] Interceptor request url: \(String(describing: urlRequest.url))") if let checkUnauthorized = urlRequest.value(forHTTPHeaderField: "X-NC-CheckUnauthorized"), checkUnauthorized == "false" { return completion(.success(urlRequest)) @@ -17,7 +18,7 @@ final class NKInterceptor: RequestInterceptor, Sendable { let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier), let unauthorizedArray = groupDefaults.array(forKey: "Unauthorized") as? [String], unauthorizedArray.contains(account) { - print("Unauthorized for account: \(account)") + debugPrint("[DEBUG] Unauthorized for account: \(account)") let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 401)) return completion(.failure(error)) } diff --git a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift index 0ca8298c..450047d8 100644 --- a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift +++ b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift @@ -97,7 +97,7 @@ public final class FileAutoRenamer: Sendable { let range = NSRange(location: 0, length: filename.utf16.count) return regex.stringByReplacingMatches(in: filename, options: [], range: range, withTemplate: "") } catch { - print("Could not remove printable unicode characters.") + debugPrint("Could not remove printable unicode characters.") return filename } } From f83bc6816c64ae7933a69bf7a37aaaf50ef2dd0e Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 12:23:35 +0100 Subject: [PATCH 131/135] debugprint Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/Models/NKTermsOfService.swift | 2 +- Sources/NextcloudKit/NKCommon.swift | 2 +- Sources/NextcloudKit/NKMonitor.swift | 4 ++-- Sources/NextcloudKit/Utils/FileAutoRenamer.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/NextcloudKit/Models/NKTermsOfService.swift b/Sources/NextcloudKit/Models/NKTermsOfService.swift index 6c8bce43..61c17d77 100644 --- a/Sources/NextcloudKit/Models/NKTermsOfService.swift +++ b/Sources/NextcloudKit/Models/NKTermsOfService.swift @@ -19,7 +19,7 @@ public class NKTermsOfService: NSObject { self.data = decodedResponse.ocs.data return true } catch { - debugPrint("decode error:", error) + debugPrint("[DEBUG] decode error:", error) return false } } diff --git a/Sources/NextcloudKit/NKCommon.swift b/Sources/NextcloudKit/NKCommon.swift index e84f9af4..0b51b1b8 100644 --- a/Sources/NextcloudKit/NKCommon.swift +++ b/Sources/NextcloudKit/NKCommon.swift @@ -370,7 +370,7 @@ public struct NKCommon: Sendable { writer = nil chunk = 0 counterChunk(counter) - debugPrint("Counter: \(counter)") + debugPrint("[DEBUG] Counter: \(counter)") counter += 1 } diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 1f16c601..3f42ab51 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -44,7 +44,7 @@ final class NKMonitor: EventMonitor, Sendable { groupDefaults.set(unauthorizedArray, forKey: "Unauthorized") groupDefaults.synchronize() - self.nkCommonInstance.writeLog("Unauthorized set for account: \(account)") + self.nkCommonInstance.writeLog("[DEBUG] Unauthorized set for account: \(account)") } // @@ -60,7 +60,7 @@ final class NKMonitor: EventMonitor, Sendable { groupDefaults.set(unavailableArray, forKey: "Unavailable") groupDefaults.synchronize() - self.nkCommonInstance.writeLog("Unavailable set for account: \(account)") + self.nkCommonInstance.writeLog("[DEBUG] Unavailable set for account: \(account)") } } } diff --git a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift index 450047d8..632475ca 100644 --- a/Sources/NextcloudKit/Utils/FileAutoRenamer.swift +++ b/Sources/NextcloudKit/Utils/FileAutoRenamer.swift @@ -97,7 +97,7 @@ public final class FileAutoRenamer: Sendable { let range = NSRange(location: 0, length: filename.utf16.count) return regex.stringByReplacingMatches(in: filename, options: [], range: range, withTemplate: "") } catch { - debugPrint("Could not remove printable unicode characters.") + debugPrint("[DEBUG] Could not remove printable unicode characters.") return filename } } From 1c32a7578ff94bd458cf901a1e2f08d6733e9813 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 12:28:57 +0100 Subject: [PATCH 132/135] cod Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKInterceptor.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index 2e1b63fa..c0ecc791 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -10,7 +10,10 @@ final class NKInterceptor: RequestInterceptor, Sendable { // // Detect if exists in the groupDefaults Unauthorized array the account // - debugPrint("[DEBUG] Interceptor request url: \(String(describing: urlRequest.url))") + if let url: String = urlRequest.url?.absoluteString { + debugPrint("[DEBUG] Interceptor request url: " + url) + } + if let checkUnauthorized = urlRequest.value(forHTTPHeaderField: "X-NC-CheckUnauthorized"), checkUnauthorized == "false" { return completion(.success(urlRequest)) From 069c7746df6da2d93d6edd461645cc2c0dabec93 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 16:55:03 +0100 Subject: [PATCH 133/135] log Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 3f42ab51..0ffcaa62 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -29,9 +29,8 @@ final class NKMonitor: EventMonitor, Sendable { if let statusCode = response.response?.statusCode { // - // Error 401, append the account in groupDefaults Unauthorized array + // Unauthorized, append the account in groupDefaults unauthorized array // - if statusCode == 401, let headerValue = request.request?.allHTTPHeaderFields?["X-NC-CheckUnauthorized"], headerValue.lowercased() == "true", @@ -43,12 +42,10 @@ final class NKMonitor: EventMonitor, Sendable { unauthorizedArray.append(account) groupDefaults.set(unauthorizedArray, forKey: "Unauthorized") groupDefaults.synchronize() - - self.nkCommonInstance.writeLog("[DEBUG] Unauthorized set for account: \(account)") } // - // Error 503, append the account in groupDefaults Unavailable array + // Unavailable, append the account in groupDefaults unavailable array // } else if statusCode == 503, let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String, @@ -59,10 +56,13 @@ final class NKMonitor: EventMonitor, Sendable { unavailableArray.append(account) groupDefaults.set(unavailableArray, forKey: "Unavailable") groupDefaults.synchronize() - - self.nkCommonInstance.writeLog("[DEBUG] Unavailable set for account: \(account)") } } + + if let url = request.request?.url?.absoluteString, + let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { + self.nkCommonInstance.writeLog("[DEBUG] Interceptor request url: \(url), status code \(statusCode), account: \(account)") + } } // From 5d967319c5b008f11a98c15d52a0ddde36779658 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 7 Feb 2025 16:58:37 +0100 Subject: [PATCH 134/135] cleaning Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 0ffcaa62..03bd1f01 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -61,7 +61,7 @@ final class NKMonitor: EventMonitor, Sendable { if let url = request.request?.url?.absoluteString, let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { - self.nkCommonInstance.writeLog("[DEBUG] Interceptor request url: \(url), status code \(statusCode), account: \(account)") + self.nkCommonInstance.writeLog("[DEBUG] Monitor request url: \(url), status code \(statusCode), account: \(account)") } } From 1d122f9844ca38272f7040920892f7150d95c483 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 8 Feb 2025 10:37:46 +0100 Subject: [PATCH 135/135] LOG (#123) * cod Signed-off-by: Marino Faggiana * cod Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKInterceptor.swift | 11 ++++++-- Sources/NextcloudKit/NKMonitor.swift | 5 ++-- Sources/NextcloudKit/NextcloudKit+API.swift | 28 +++++++++---------- .../NextcloudKit/NextcloudKit+Assistant.swift | 10 +++---- .../NextcloudKit/NextcloudKit+Comments.swift | 10 +++---- .../NextcloudKit/NextcloudKit+Dashboard.swift | 4 +-- .../NextcloudKit/NextcloudKit+Download.swift | 2 +- Sources/NextcloudKit/NextcloudKit+E2EE.swift | 22 +++++++-------- .../NextcloudKit/NextcloudKit+FilesLock.swift | 2 +- .../NextcloudKit+Groupfolders.swift | 2 +- .../NextcloudKit/NextcloudKit+Hovercard.swift | 2 +- .../NextcloudKit/NextcloudKit+Livephoto.swift | 2 +- .../NextcloudKit/NextcloudKit+NCText.swift | 8 +++--- .../NextcloudKit+PushNotification.swift | 8 +++--- .../NextcloudKit+RecommendedFiles.swift | 2 +- .../NextcloudKit+Richdocuments.swift | 8 +++--- .../NextcloudKit/NextcloudKit+Search.swift | 4 +-- Sources/NextcloudKit/NextcloudKit+Share.swift | 10 +++---- .../NextcloudKit+ShareDownloadLimit.swift | 6 ++-- .../NextcloudKit+TermsOfService.swift | 4 +-- .../NextcloudKit/NextcloudKit+Upload.swift | 2 +- .../NextcloudKit+UserStatus.swift | 14 +++++----- .../NextcloudKit/NextcloudKit+WebDAV.swift | 18 ++++++------ 23 files changed, 96 insertions(+), 88 deletions(-) diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index c0ecc791..795c23cb 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -6,11 +6,18 @@ import Foundation import Alamofire final class NKInterceptor: RequestInterceptor, Sendable { + let nkCommonInstance: NKCommon + + init(nkCommonInstance: NKCommon) { + self.nkCommonInstance = nkCommonInstance + } + func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { // // Detect if exists in the groupDefaults Unauthorized array the account // - if let url: String = urlRequest.url?.absoluteString { + if let url: String = urlRequest.url?.absoluteString, + self.nkCommonInstance.levelLog > 0 { debugPrint("[DEBUG] Interceptor request url: " + url) } @@ -21,7 +28,7 @@ final class NKInterceptor: RequestInterceptor, Sendable { let groupDefaults = UserDefaults(suiteName: NextcloudKit.shared.nkCommonInstance.groupIdentifier), let unauthorizedArray = groupDefaults.array(forKey: "Unauthorized") as? [String], unauthorizedArray.contains(account) { - debugPrint("[DEBUG] Unauthorized for account: \(account)") + self.nkCommonInstance.writeLog("[DEBUG] Unauthorized for account: \(account)") let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 401)) return completion(.failure(error)) } diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 03bd1f01..8e83e13c 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -60,8 +60,9 @@ final class NKMonitor: EventMonitor, Sendable { } if let url = request.request?.url?.absoluteString, - let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String { - self.nkCommonInstance.writeLog("[DEBUG] Monitor request url: \(url), status code \(statusCode), account: \(account)") + let account = request.request?.allHTTPHeaderFields?["X-NC-Account"] as? String, + self.nkCommonInstance.levelLog > 0 { + debugPrint("[DEBUG] Monitor request url: \(url), status code \(statusCode), account: \(account)") } } diff --git a/Sources/NextcloudKit/NextcloudKit+API.swift b/Sources/NextcloudKit/NextcloudKit+API.swift index 87aee675..42b209d0 100644 --- a/Sources/NextcloudKit/NextcloudKit+API.swift +++ b/Sources/NextcloudKit/NextcloudKit+API.swift @@ -75,7 +75,7 @@ public extension NextcloudKit { } let method = HTTPMethod(rawValue: method.uppercased()) - nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, 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 @@ -110,7 +110,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, externalSites, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -234,7 +234,7 @@ public extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -280,7 +280,7 @@ public extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -321,7 +321,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, width, height, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -363,7 +363,7 @@ public extension NextcloudKit { headers.update(name: "If-None-Match", value: etag) } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -461,7 +461,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -495,7 +495,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -568,7 +568,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -704,7 +704,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, activities, activityFirstKnown, activityLastGiven, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, 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 @@ -784,7 +784,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -871,7 +871,7 @@ public extension NextcloudKit { } let method = HTTPMethod(rawValue: method) - nkSession.sessionData.request(urlRequest, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -910,7 +910,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, 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 @@ -958,7 +958,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Assistant.swift b/Sources/NextcloudKit/NextcloudKit+Assistant.swift index ef3bc5a3..992e870d 100644 --- a/Sources/NextcloudKit/NextcloudKit+Assistant.swift +++ b/Sources/NextcloudKit/NextcloudKit+Assistant.swift @@ -18,7 +18,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -59,7 +59,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["input": input, "type": typeId, "appId": appId, "identifier": identifier] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, 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 @@ -96,7 +96,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -133,7 +133,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, 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 @@ -170,7 +170,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 diff --git a/Sources/NextcloudKit/NextcloudKit+Comments.swift b/Sources/NextcloudKit/NextcloudKit+Comments.swift index f4b8a277..65149708 100644 --- a/Sources/NextcloudKit/NextcloudKit+Comments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Comments.swift @@ -32,7 +32,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task 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 @@ -82,7 +82,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -129,7 +129,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -162,7 +162,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -207,7 +207,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift index 8c83f768..516a6bd5 100644 --- a/Sources/NextcloudKit/NextcloudKit+Dashboard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Dashboard.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = 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 @@ -58,7 +58,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let dashboardRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let dashboardRequest = 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 diff --git a/Sources/NextcloudKit/NextcloudKit+Download.swift b/Sources/NextcloudKit/NextcloudKit+Download.swift index d06cf410..ae08bc5a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Download.swift +++ b/Sources/NextcloudKit/NextcloudKit+Download.swift @@ -33,7 +33,7 @@ public extension NextcloudKit { } destination = destinationFile - let request = nkSession.sessionData.download(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(), to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let request = nkSession.sessionData.download(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance), to: destination).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } } .downloadProgress { progress in diff --git a/Sources/NextcloudKit/NextcloudKit+E2EE.swift b/Sources/NextcloudKit/NextcloudKit+E2EE.swift index a6839f9a..7644a8db 100644 --- a/Sources/NextcloudKit/NextcloudKit+E2EE.swift +++ b/Sources/NextcloudKit/NextcloudKit+E2EE.swift @@ -25,7 +25,7 @@ public extension NextcloudKit { } let method: HTTPMethod = delete ? .delete : .put - nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, 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 @@ -77,7 +77,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-COUNTER", value: e2eCounter) } - nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, 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 @@ -122,7 +122,7 @@ public extension NextcloudKit { parameters["e2e-token"] = e2eToken } - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, 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 @@ -177,7 +177,7 @@ public extension NextcloudKit { headers.update(name: "X-NC-E2EE-SIGNATURE", value: signature) } - nkSession.sessionData.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, parameters: parameters, 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 @@ -228,7 +228,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -272,7 +272,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -311,7 +311,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -352,7 +352,7 @@ public extension NextcloudKit { } let parameters = ["csr": certificate] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, 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 @@ -394,7 +394,7 @@ public extension NextcloudKit { } let parameters = ["privateKey": privateKey] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, 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 @@ -432,7 +432,7 @@ public extension NextcloudKit { let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -464,7 +464,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift index 2082e324..54d2d493 100644 --- a/Sources/NextcloudKit/NextcloudKit+FilesLock.swift +++ b/Sources/NextcloudKit/NextcloudKit+FilesLock.swift @@ -24,7 +24,7 @@ public extension NextcloudKit { } headers.update(name: "X-User-Lock", value: "1") - nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift index 7e3babaa..8641ad10 100644 --- a/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift +++ b/Sources/NextcloudKit/NextcloudKit+Groupfolders.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 diff --git a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift index c3e42737..2e192c91 100644 --- a/Sources/NextcloudKit/NextcloudKit+Hovercard.swift +++ b/Sources/NextcloudKit/NextcloudKit+Hovercard.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 diff --git a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift index 3b994f96..1e84cd70 100644 --- a/Sources/NextcloudKit/NextcloudKit+Livephoto.swift +++ b/Sources/NextcloudKit/NextcloudKit+Livephoto.swift @@ -32,7 +32,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+NCText.swift b/Sources/NextcloudKit/NextcloudKit+NCText.swift index 380fa3f9..c5f1661e 100644 --- a/Sources/NextcloudKit/NextcloudKit+NCText.swift +++ b/Sources/NextcloudKit/NextcloudKit+NCText.swift @@ -20,7 +20,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, editors, creators, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -90,7 +90,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, 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 @@ -121,7 +121,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -174,7 +174,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .post, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, 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 diff --git a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift index e97ef4be..18d29faa 100644 --- a/Sources/NextcloudKit/NextcloudKit+PushNotification.swift +++ b/Sources/NextcloudKit/NextcloudKit+PushNotification.swift @@ -31,7 +31,7 @@ public extension NextcloudKit { "proxyServer": proxyServerUrl ] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, 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 @@ -73,7 +73,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -113,7 +113,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -151,7 +151,7 @@ public extension NextcloudKit { ] let headers = HTTPHeaders(arrayLiteral: .userAgent(userAgent)) - nkSession.sessionData.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift index 891cbcbc..55d5e587 100644 --- a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift +++ b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift @@ -26,7 +26,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let tosRequest = 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 diff --git a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift index 88d741eb..eb73e694 100644 --- a/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift +++ b/Sources/NextcloudKit/NextcloudKit+Richdocuments.swift @@ -20,7 +20,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["fileId": fileID] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, 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 @@ -55,7 +55,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -104,7 +104,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["path": path, "template": templateId] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, 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 @@ -140,7 +140,7 @@ public extension NextcloudKit { } let parameters: [String: Any] = ["path": path] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, 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 diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index 423d44fb..64b8e8df 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -40,7 +40,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - let requestUnifiedSearch = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -131,7 +131,7 @@ public extension NextcloudKit { return nil } - let requestSearchProvider = nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 diff --git a/Sources/NextcloudKit/NextcloudKit+Share.swift b/Sources/NextcloudKit/NextcloudKit+Share.swift index 635725ae..3ca60e91 100644 --- a/Sources/NextcloudKit/NextcloudKit+Share.swift +++ b/Sources/NextcloudKit/NextcloudKit+Share.swift @@ -67,7 +67,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, parameters: parameters.queryParameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters.queryParameters, 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 @@ -130,7 +130,7 @@ public extension NextcloudKit { "lookup": lookupString ] - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, 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 @@ -300,7 +300,7 @@ public extension NextcloudKit { parameters["attributes"] = attributes } - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .post, parameters: parameters, 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 @@ -385,7 +385,7 @@ public extension NextcloudKit { parameters["attributes"] = "[]" } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, 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 @@ -423,7 +423,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in diff --git a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift index 02d2a84e..1aaa19ab 100644 --- a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift +++ b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift @@ -25,7 +25,7 @@ public extension NextcloudKit { nkSession .sessionData - .request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()) + .request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) .validate(statusCode: 200..<300) .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -94,7 +94,7 @@ public extension NextcloudKit { nkSession .sessionData - .request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()) + .request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) .validate(statusCode: 200..<300) .response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { @@ -136,7 +136,7 @@ public extension NextcloudKit { nkSession .sessionData - .request(urlRequest, interceptor: NKInterceptor()) + .request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) .validate(statusCode: 200..<300) .response(queue: self.nkCommonInstance.backgroundQueue) { response in if self.nkCommonInstance.levelLog > 0 { diff --git a/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift b/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift index 22aaa277..862e5c00 100644 --- a/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift +++ b/Sources/NextcloudKit/NextcloudKit+TermsOfService.swift @@ -19,7 +19,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + let tosRequest = 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 @@ -70,7 +70,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task 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 diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index 6d78b5f4..3d3e2f69 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -43,7 +43,7 @@ public extension NextcloudKit { headers.update(name: "Overwrite", value: "true") } - let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: NKInterceptor(), fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in + let request = nkSession.sessionData.upload(fileNameLocalPathUrl, to: url, method: .put, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance), fileManager: .default).validate(statusCode: 200..<300).onURLSessionTaskCreation(perform: { task in task.taskDescription = options.taskDescription options.queue.async { taskHandler(task) } }) .uploadProgress { progress in diff --git a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift index 732ae404..13ac6e39 100644 --- a/Sources/NextcloudKit/NextcloudKit+UserStatus.swift +++ b/Sources/NextcloudKit/NextcloudKit+UserStatus.swift @@ -22,7 +22,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, nil, nil, false, nil, false, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -73,7 +73,7 @@ public extension NextcloudKit { "statusType": String(status) ] - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, 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 @@ -115,7 +115,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, 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 @@ -162,7 +162,7 @@ public extension NextcloudKit { parameters["clearAt"] = String(clearAt) } - nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .put, parameters: parameters, 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 @@ -197,7 +197,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, .urlError) } } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .delete, 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 @@ -232,7 +232,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, .urlError) } } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + 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 @@ -289,7 +289,7 @@ public extension NextcloudKit { "offset": String(offset) ] - nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(url, method: .get, parameters: parameters, 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 diff --git a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift index 4c95bcec..2948973a 100644 --- a/Sources/NextcloudKit/NextcloudKit+WebDAV.swift +++ b/Sources/NextcloudKit/NextcloudKit+WebDAV.swift @@ -30,7 +30,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, nil, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -78,7 +78,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -126,7 +126,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -175,7 +175,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -236,7 +236,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task 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 @@ -401,7 +401,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task 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 @@ -456,7 +456,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in task.taskDescription = options.taskDescription taskHandler(task) }.response(queue: self.nkCommonInstance.backgroundQueue) { response in @@ -506,7 +506,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, files, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task 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 @@ -565,7 +565,7 @@ public extension NextcloudKit { return options.queue.async { completion(account, items, nil, NKError(error: error)) } } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor()).validate(statusCode: 200..<300).onURLSessionTaskCreation { task 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

    #W$a%;_Mq1 zA4-MiL3y5s#6RA&3=`v4`M38#!T}xkg8`T68xKLG+h>2#jM5;);(f}>^=77+jZ?bZ7ZuI!KM>d4v*90tRf@QMe zysZ`w0M_k4Z>A*H*+saGStcLbKXoi0){-S5wvxpi5u0%3oh&MkD< z@onX`f%N>_?ObL-casD8xaD_q#z;68A~<`Z!xWgRS1mGie?2B_kb=65# zhdzRsuebWrWJ&BlAr6BL{CMovM6>(|@@ghl2M69p7vY!hyOXH?l39q^+IT!KXF-{x zEwQF%21VK_hwSyZNbpBo#kk`mWWGSln|BDy@Ayas9$US9KNNM~`D;_%d^gtR-JO`* zKP?^muNOWx0z9-&CV2LY@(xSp=Y%qJ#eD^cV^#NV$wZlC=k>inn+CdyM;&1>*_hlMgp(Mimk*%=2zXw6yaUxeAVFB_oFbN3Cb9A+afRovpIML5c#lz zKF5J{pcc^^@pAPnS-td!wB;cA4Q+61ahoc~14@OIfU4bXp{gIVock!_wAtCDxETqi365XoV ziZ|Rc83-V>rH?8A>z5Vorn>$cGCwHMM)P{DkFg^8m#l5tQ?bf zF`CiFO5VN^OY@O8Mh_E8&u|nfsmnffIB~QXZHaEdlvQLDq^jqBr5a3tbkO|H7Y5em zhz05M7b1@SgXA&&|hM+1?nma~UnUtU694@*pcAs7^KjG={VyH8gMo{( z6+pkAp|?EOAKC-<*%}@1C^p=;)q&8JX{IX#1m{cBnugd$^K_3oIkvE)qH#EiVa3b% zbsK1_TB?wllr7Ed!c0e01pgHX)vrI?i?`(+>Z@sJgFtXkmJnz%OH`6((moT+AXq6s z-|t8herHcsm!tSVR#sUPK=uMG**FuJqkI~}LoF^&Yaq3OY6Q3#?IDoL-8DBCST$&jZiRK=j3^`bBuq=UmYT3!br{0Pe)PhACho)@2Qw@3MKzaz%il03 z7PEN!FRYs~4fuayp6y*b-$bSzguJcE|LRE z>RuL`=QI)sO<`cUOR}jWYoied zNRf>Xc(Q}C<%O4ekd`Sm!}`7#*mh=IMl@meVUNi@>?x0HWmdYhUG63!o`AH1u| z!+cV)q%B*JCUAoW{R@bd79_|T}A%MiC?V+@S)x)^RKd%xxiAe(LPRbgsYlL7=bu1Ji(WgGRL7<|Q)XX( z0W18foN48^F)}S@svbx7590Ie>7pVaJSs7=Xo-zlqE46Rw@w*)*CIJa$ea$md|aS+ zK*^gvwoVx@EfTd~XW)xYL2EsMW0&T+&mEUEqr|63GpPG}id{lV=IFmw&XwGKHXDP~ zZBLtWhsNv29y-5>JVw>U`R-fnYJK^TXt%m?D6mToX@s_kR{DGGV&tBwgtm+u(-?W>92mYkqx-+9%zm zL|byWYdJ0#!8V0j;=(&xgq}_im%)?X7WQeJrvj?3rJtF%wGvuf&w4EF!0-n;;G8Z* z-b2rz2jyHxScXo}1GV}OQ+FV)z_2P5nhmh6EM1j#O^TK}Yer2}p%b2d9ha2UnxTaz zEjTyJWU3#1{sq5*M?#tzzsR&B@rik|f3~R|ZN_Ho>#@~Wb=#h0;w(OWSAgTJ)+Eiw zp8DKg3Gv)oqAFUlWr@9zOPA0xb~7oO`>WO!3kE0lCsAjgcW61&aVI;;oD9)`7@CBE zK^Jw6%>I1RSSFXpiGpkTlh3r91?Fscc``MnolisNLaMVxp}+ZwI>B-F5`W)Nhqs03 zgYu~{fsRw8k^Xi= z|1>;peuc|r)U3hm@{qf`8CyKBM6pcu3NFaz&zJq`u67kM6yq}M^R4W@tC6PMJWkBj zdq(_iAQVV6)F6Y)-ks<7;R{}0TN=IXEz{RO_6(r71Z@&yQb^+Tgy?LMCDnxk3%X5TlCVajQgiNAl*8vi-Gx$-;jd8NlUn|FKXK`|Xmp zk0fnO;%?7FB#6Uhcr>Ssp8T1$VwtP~cK5H>90_ujFm{xYT>7!60_Ok0ZQn`6$4^I4 z8i;jzvs7zFxm||O){E>y2Kqfe-^6BJ(~H}v6*FhNpH3&`qNypIoZ@E^j$!(cCgN@9 z57y_Od)p=eLnCYgeTTIq%v;nMGzWif+KG>mbW~Gc-ahdX@;jYHD~~ zxUt2K*9u$>&V~$HNNPnGI2Vn3+Nbla%^p+o{yr$CEFt zR*UO+2T)4CHcECG(v7>}f0IT+cN~jKuE(*A5T)NVo{2#Y2U7X-!am2c^XtbNG3e@)aXt$i>1oiI>)5?jXF7Wc>Zyj zKOgA^S;v9&8JkFX;5y!y;oX6G)QwpZCB`_kZk2GU6?Q9xCl4~ zmptA8BjvsClxFbWp=YJ)`~dJZp5M-cm~Z+&B+Fe(L>lE2e>NWLog_ylB7g)8UR>G_ zBDtbH(=-rQXT#j?eoeErcB6k(DESAWg-&cTM_YyFM7Q+ofqx&yPx0@$qxX9de_kB* zZ_38s4wPoekmN0@$}35K;M4a^t9z>i>0MG}E?hNy!2H}4!s-|){^CV%deN1kr>lGp z<-E3c#(b7PYK_uI>d$Wwih1qO|5URJJI>(jjntvr`69Y;Ov3K|J5rhHQT;BB6`o(R zs^Ya~VM_}AUtKy3k2HQijN0My_>vJr*nJ`PvC|izK`lWi1{fo!mU;ST>>EQ7OvYN> zXS3F2qPFVdy0k~2=J4dQMe~+*pykx8-%bi%Q)ArTt7ZCVaM3nuJNekg;Bxa&?2_sM z-4?K&CGCey4Y>9-{yGQ(*xW4*V&*Bt#_gvK>f$)XZ>wqB9Zs9{ld)f~6{WP7KiqI@ z^Zu~OX`8T&J9zLL!4ZJNJo4DNz^UnFTV}p#d*;SAL2ti9IVO)B=xF#uV{?^Xey-Xc z`VCRnHMPttU>SRR!eP_30H>g=<2?5oNQ2H8#X-r4ZrYSKj|ni31GH|hPw6=k zJY%lw-c;zT3>^@Z6TVj!8`$?f_wVFY^R7)b8un%D*}Yuf)e?8!f=-z{{(hW<=*3E% zVi?s;&$hWq@^N|>pYsOzexly1|)tIuYK4l`I_UH7$sjf)GnhIS%)1Q0b#A}-HW?)`fjh@ z?Zse7xzlTr`jc?OD&8I`bdX$?ch6IRoiV-zC zYn}}DjIz8{K=YjK1mvXg99*ifl8D@0h*H}SF`)=dXEv|h`b^iLYq3@nPkw94AQ|yE z{Ca=UJFgA46*c_=zgy(w7n5)JA1~KF?d>pdF+U`3t5HEI8;^vl`F{HPh6^olk`>^9 zYF+mkJc$R5rmuCJ#s?fUH%EKRRn2d zkoatnD*d_2SA*&A?mfx(VdSU$GmXq2&tvj)m1;|@)nUBozZdF($j}H5DjACDu{(xX z?!8DwP08`nA6wN#GaioHdOx|_LtY-f$hZ}23A^pGT2^RqgF2TXKa>reyh9+*?l)GP9wxESR{|*52@Wt6 z9m9B8g!?053^<>g=KL%~v6!6`iA(Hsr_l^L`F^(bR%0q_BSQ z#hxv6I{$t*{;O}CcYL}7<0O?tU){cgkHj!H|J+@lvKpg2$KI&TdX|3tI(GiiQp>WA zQNzU}0rA_zr~JiQ_x!a43*z!E)&d!k^px1?%5y#S#!>K~Xn)GnY{3fAgTJObS7U}R zpAy9^-1v9y+fC*lXYRiLt(pSw)0=2>0LlVSf2;qUPhVBX44sjh;=W_P zhME>3jtwja=&e{2)k+izcI=Za;cglJF%o9vtJ+l-=*3&=Hcy@>dyo%eXk zFR}B!#c&v-z-O%tE&5eavrwL|MEhHbToQw!;8JkMKyU+au`HlA;iO->T6N9xY!tkX zS~wwnz}ycvqzI-P7)YKeC<5{7m=sg5;T(t<09jnqPv#8!9MD+^7g{S zl_xv+bjIv6S47&#AL3+bKlsIN-`X8H?R7av8drYl#}{tu`RpilepK&e!f1$V-T`x! z9oR#3U*6Fz;LrBc;YI1}oOj_m4BY$(DeqV;(EE+O|wL0wI9ITYFDoQ_#{Po)X*C9+hdVQKj=jKH9 zzmY8W$1+$*j#sM6B>xLu?>3@D-&jOtf#M6nOFW}j;yRw|RHN*>3R0^P{>9f^JXI*n zbVps8=N-%*4a042@(&OdrAohy?fp8M+zX>Ud!WY>A3L^c9wGiF_6z2Q#_fkhRDZgv zW$_bmA#*ksRrA_u;C;nbHaSYxItQJqvFt={m`+DixNa2+4f+OQS?WV#jpUi6MJ%$@l~SjCkhl1keAMDa9)AZK%_3Pw5Z zqB<`T-?m(7i7yf+U|bI64xPP9#^Q)HY~TEGn0Kb_SHblVs&B8!JjG=0zAJUJ_z(bH z41hJ<>~$)2pq3%G9F6GB){cuSJai*-B}SEFH5c#GKEFa>&|PB)5j;N3cD>ZKV-p$4 zE^d05q8e`q&sglvC5;*hi@xG+jvupG`Dk41zxry24V-w%s>Q80t=B$+#=whBA=$d; z%UUboDf!D)QrJ$6%QoW}?|?}6 zTgg&|vz6sGpA6Jc_X@_uCZBN0trJ;6`FaItL0=sT5KhaTQ2%I`EP7eBT_&wAd8w@C zVnY+6NaJ{s?9B<)JyTfu*Oe$TsrJWPGF>V6QYciX9TJQa@Ue?w0HOS;&{%%|tvEV+ zWlm_K{b(394gYK>X6TZwbJpPP|IuavH-raOCsQ-!Gke!f>u;ACN)6BQw|-ky%>ni- z!sa-97k^J;Mqqvv0FNueC~hyYSozv^V1;&>%I#e-9YPXAWpQZ+tM`u31x{mftHXWUP(29sZ8clh@CoXsibPf~u-eihjulNnYg zrw?4CJ=#nfVj~qOW1vs$qdo7YiEE9!AI4fU(j^6$G#5U_45^ui-&4%>P7I3@*FC^d z@G%RT!{$6$Wr+cW%zp|W`vf$o=BnI%-#0FT&hD#8jRah~Xby~ETHU@9M7`iN%Mf)3 zmSdPZZ;xg;;V4&A!PKiILXB6lt^R&F&o;kq+|Lq@lK|vPHPD}aon~n=PJI~gvwhKj z=6#fd3Ci*@x1*w{uSg%HmVN(hc?N8Q9Rd)F08gZO!0k48h5zb$l3aU{{yS`le$-s_ zrT_Id*L|Qn@V&vEkc^X`@rTJM!UX8-=D>&Sk@rGC&fbs@Yu>|pxMnb!4iibgO#{oj1@zD3txt|ij<42Yma z5^}l;p?b&dL}c6bL*B|&$4}1?WYY;=DwG@(5Sw@+@GWj;F0-R^jk~jRhaQy;?SbJ` z`ZJ@n13|{SsO_$hf_G$T?~)?$&E-ubVWnIWBDu;Qp2O`iLQ83kHM|9yUXR{d=+pkE z&b*h!_8Gkezn?cXg2M(Hq-?1fv`rH_Yc8UX_RN-I6?0R71P9TVqO4Ef0OZSdQ5>#2 z``ebU)ZbAy(W#9->W~t~XsxLBJef$@G(8rU3)>w@3Tvo4BlayrlL>S7FoqU4P`ZvQly8A#>?KnfB#Ch zK=)5l&0ncls$58eGf$rOd$TA4-$}uT;_;1WM$U!T^YmNeh&t&=D~w5pi9`TD?Sgm%*KGf z$yR%p+yB|I6-KcA>gRu6so~?1z`)Mvr@i!vt*PaBM!ds+cE{P6br*hDIn@@$`wicR zDuTLput?dc#YMNLGIp}F=*R7kLU%O9W6CaZIl}HADX$)FdREtlhv$Tgys@l!#zG0H z(KcY3xG3@5N%W2N*;ZaA(fea8k@}@Lo3K|v!|ZTT+&&&?SpGQThq39`Iws?f27amm zj{=!V|H%g5;fFv}c~{-aMbh=HXF+?np}lWFj}z6!v!zlqKe5FAb>1Yiyk#(zMQ95= zVzGSi+b80&$@HY!EQ&Zj|D+~4+dH8srSyr`EtniP$0TF!O5Lr8R9Bl1_2K!a-b(w8P~g`D}tsJs5BO=}37%55eNtTZky2Z^T>bq@Xo2$syVj zXY`Lg#rz`mJoQDu^148mXD50=06la9xz9ZF_eTbg1l%Fld(cF%>6Jw^*M>Xtrs$lx zVS`b!ZxT@Dw?0Tf2}UC`iX_OQbSBGIJx~;(p#+6-Ig0#k5CWlbbut$LL9RHk)jOZ+ zhw#&2)|5R0NflB;Z|ReWCxU-AW|lOYrfGjk#cHH_6!0uTbgbX+lvw`zBvln_b>b=R zzYmzZ310-upsL{DY64DeV&Omci-H`Hji<~r|@#z=8pVb%I@&I zwNUG^(*f6JPGWsefSbR2YZQN*O}532d09f>%T?!ouiBTPLq|7rdUWIpus0D~*ya>c zze9ZR_9RE2!|y`@@NR@OoJqWHxP49AH(3=BpQKH=E_f-&ghxN1+K2fD_P8FF<6&Py z@o08EYyjNFkK}T_Du0w~C!Jx7;5k3%B}IY0N}?wIgaKce8B0~wo_W&}o*{q-?u;eS zXZ8ti%$Y!OOYzz0Rc6dM!%#buI4j8`m@c$5I*O!?M)=tMB(2;XigrXl>8dY1$o%x| zN$t~PujUD23$b(*xANSR$(7)8YOsK}ZF$HC7QCsWLRJn)9#w_J=&jSQ3$Ymgi|*#k zyK&yvJx_b_WGp|4W)INp4t(261oYUIoW26wm4&$M4Z83A{Y_ioAM_W%Ui!wS)%}L! zJwX>tsTcb*$pQyg5%DyZF_PRHF(fkf&~NG;)j90SD%qvq@dyl|-y=(R;Z-5t1PdMT z68FYBMfWvJds##82YoPga!wfb2|Err&0;fFYw+I$M8K?koBtK~Q#^~VFY)-3*qh6~ zL*-?J@qG?@7 zx7kI0KxB2J?)iH(ugVJM+2NuiuDoqExjK6#9s^f~J&JyRq%vDgQEekf4m0@7qV4&I z9Yq`7{5b_b3&_Wo9AyMQnG?1vsV-7VC)n!wyN@B`kIiq4+=i$@@}0N7AmC4FN2E!V zJ&(6*qvgtkzi%tpV!==4&;RITy``3}4}>m^CHB%lu9vFUxHX3p%^<>}mR9A=w+o=Q zBw4!IxB|vZ>3@GwOJO2rJwhJ;5}995^7zgjJ!XI*Nj_;%`wm@y**S8}tjR`;E^dZt zO?pY6xTM>cxaP61mMJpbzO0WK2+0`ECkF?E+j3NrpC=LZ2)u6mGFN7MDebll{&h`O zwvqGM%UexK%WGY|MJ>fc6uOTc@TGEzzN#e-sY|e=Qo)O7{LCZsR}-&HC@32(sAkGLGX!A@Efu|1z)8G>z2J&~c-hp{O53rv?TL_aC$5m@r3 zl!2QW!{`%(7X-*SFZ#Awq|RGd+E|D8(d)Xru3vK|MI3en5aS7!-7eTmeS#Gp26XHA zC;UTU?7LzkCLIh#~$sGCFz2 zbvZ_0N>=9kFx3xVw*q2%4mz|*uFbow7nC=5t8WwM`BUhvwqnIM>1;M1dE)Ge3eC84>|msrOwcEw4^+R zB6l}39GXm2AmCadm#mMSpW0g<}y$EimY~2eT3KMI271hJnf9> zHeVHW@l?u>Frfnj-B!=IIUlLotm`aFb0wL~&cr;)W1yP7`N(O-xo>LaJt`T`Mve1J zPPO*HgRE(=39~CHZ~P}FDEu$rbjTJ1Y5!t6!r02&0NY}BsnB%VFjp(mdDKiEj}(>V z9zH#mS5?M6(@zd~{(l!7xXyRe?4{m&KTx-z&s4&JUlldjITKu;vm=kBUu#Drt`ZJZsWT;#f`?d@Pr%? ze|nhJjij19N(yo@otak2=bu)OL9v{@5GvpXPXE+0@{uoXo8hBIsHHo*I`;8VE67G5 z1%4TpO;K5k9;lmLO9tEDAEMXZOR-ml;G`046tQEM)2p~R|3CanlbAnBqNB>?I>HT7 zkrRon6++Q(*gXoY7iXSOrP^JF+~r?A8IbX+E^dla>^0)~^g^!M2gyqF$LC$uQj6o{ z-;j(9mi!?i7VF(G18uwyL%Z_3l?$)rOezaH*#$g4m>l)Bc-3QVeDeD5;?^kYqSl`! zQ>OOt^igDVXO_CgA%gLg;+|zrILjGOZB&)@c~rc%V4~ryFT`~~jexpV?kgA<=s-D< zcznhj-Oh2cr6{1IW3{9o>XbtBpKJNspLP5~hwkd{_u+$fHVONcW9%f;rWMcNu>m$s zw1(!Xf3Umr)h<~6HD-2M$CR?ZOf)-q*ZKFB@}UyEoayu274+y*eHD%S(@O!=DUh$P ziWC3x*?j3_nKO4G_=n^=NtBh2{8lm5xl=&+ulTsfvf-sF@Y`uz;A_U4K<(KeM|an` z*25rwMAiZNxOutBcbQGf8^vJyfSd=NL6)wX>NgzG4ewf8l zz8m^|@IO)Pe7M&ZqXYeDjXyJ@-CFi8+oEz{g_&iVHj*(eWxABJPW5@eUPXU%o?3%4 z+&`lY$~??#A#CA4wXlBk)3h)$q~$S%HL;IWvzloq9n#!|X-N+iM6c=(7-&@_6IR?273K;6%*gQ)oxYFK&8emPB^_QlJ^zzjH1~Eg(+BFE?7N7x#%8E2kA1BL5LAI-3$v z^Xn`&M3Y6C{ceG?9=zTCkQ_FB;b3#5E0~$o&B3`opv8f z-%V^N^6}Qwood|YT?Z_g^^ZwN2R>fqd{KL`$x6ctpr)xwEZ!8$?4+oleA@E;1M}az z#pjEv@jgfvym|qj9#)_4O4$){iaF7#LxOfYJg+OgM=UzVXc>z2Fz|^Xw$N zF4r4NC4qXLN`kk3y49BLRcaqh3xs?>?*5_bhi~?GK<}~3m2YCFO46C>jsKM)rq>sm zg>Ic+4sYAK-}9fs99K*SSGY?TBlGnSd8RI?pAXmKj!d1Vc*nM-DA}UHXlfL4cYuX@ zv!~ydkY@;rK+|&vV`rEx;)YLq(jz!{NLoc@qUf)PbmW6u_4%RC*YeKmx3o>7v`nu# zIWo2E16B}F z@3~aIPn+jwEifb9%Y+36@$Ivw`pnTwt01`YQr`%GX1q62ulNbJ_)CrU(myq_+WrM7YW z3|p#df1K=kfn42JJb9reMRfq9+Uz~LlG=w8$|q(LaaXy_+t|Xy8yQS;9=o`ca5)K4 zdEJq|113rMWJC6nm1aKryIzqk-;$`v@1ymEg`>!%!PiSz8z}Z1unUams(aAl;b&*c zTA+es!A;XE*LsRt<6)A>_gF(Y>vzuY3~OBaOG0@{Op%zPLDBT`E-1q}zg_zM-?qnp z?^z}ozY`P%*}52IJIZr=FN$n^Tu#b!M-Dql(}Wicr3AC|=4#FDdG^H?X-ZDX;zc`! z-aLQ$pA0r@2I=)`U{#*k3o5GYf`@mW@UCsDonmCLuTsf&8JvKs&>37t9RsRE2@JZQ zKHFK{NM1fY@?Jn#^i+g7L<>{S7-@=bq(JVZGJbSiC{6YH`lv_<8muuGz$2S!8O1X9 z*@Xa82WTSoL{eCO4TI|byg$N(%=Qu<72R%TDTiTHBW;Z)=rM{8zv1ojb>dMg`i{qc z`e1PB45tdSYu=>)=%xdsQd~G)j~8u&{7oOz5j50o@FOH#lsh(U+V6do*B7XRsl+dv zD6W0EOA*-&ETBRrkw3(;NP=^=(*FHJ33U||El;Sx_j(L{k+)RI+MXQlAm7@g{0t1j zp5^ctu!J_$xJRl~ANeKE+aKN+9Z5q2^5Dx}I54$$LI44zcqOp&xTiGPI&_UMDYvw6 zD>2&A9hk8ezGE3|%KRKWYF6BOBmusLy9WI^#7fAY)}#7_O!hZ_0c<1ZT#KbOUq`Qz zRS2&mLmm-tj_^4Ty)nI+`Iy>XJtgv|-;9H?ebSZ9tV8P)N+#IJ_Z@4atW`6knQ$@U z;bo2_(!wup-;O|`2il$>v5;6@=4<*%CP;i~l{ojfYvjtigl!r3>BZxBQYUfbZ&8KW z&94(jCXNNKDVYQTnQOjkVoRw#VHp6%l_0COw4k`9$&8hx*vah)T9Z`cLE5~I61v;m zzT$2Q#>RxA@Mu|wSSqZ&zo3wb`YrixldFIwZxC;PWNyN|vxjR(5xi;_7@bb&!=>AE z+GErQ)v|ki2*_If^|Q7nXek=^;zYjyK*Lyw{+0vC-A6z?+q??!i%tg#dLOH-^nCVR zQ@}=jhwsw#w&It|oW=V~<|D-=nD9jnE_YH)aJ(y!({~X((270(=I#dzQ}nC)Xy3cq zdjETbf7>!PXl>g$(?tfVXvpXnc&ctiY>{G>FgG@?$3r*;Ygu`N`2Se|zCd`#LXy7v zLSMXYj1yzIeJ>K5;jHgK8o?nt3i&WOxz}$)lRuQ{2I7k%s?}KM*#B$z@s_^o=_he- zQ198ZFTAvX8|JBwvN*O)mhxU)*BL7%2lBs#MtNs#o0OV^w`>2bm(3!YJ+!2j&Ugr zr+~f-%lC!@o3QPDf`V`1Uf7DCiMGWHw_uher7D=Z;y5(e)coxyStAC3gT!h2y!%$& z71ShC{kZ0Mo#7^Zs~f&>Wq19~dJ0|SWOJ5w2Pv;KH^%p{+^ds191hbr5%Zk>5NWNI z@QZ7Qn&}zCTfH%}-|0GxXw40})krqb?#+WtUg*s6aa$78 z4NKCl9xFWK`KTePj$5=dpVM^}358GSdZsW^p8Eu**1cK?-Sr1$-LWDK0fRi3 z*oD6w%YA1WOy1DcWm=S2zg-df>@d~cu#SaB7ZY${jqNV@M0NJ{)GK%Cs7*c7!wA&1j?Cf3dW z*(OS&jZw0yToqmSW{k=y{6mVGBy1$M@}}X3NtXW-E|gnZ8Y9gMhc}W*#lNKJ_xr