Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/NextcloudKit/Extensions/String+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extension String {
return String(NSString(string: self).pathExtension)
}

func parsedDate(using format: String) -> Date? {
public func parsedDate(using format: String) -> Date? {
NKLogFileManager.shared.convertDate(self, format: format)
}
}
13 changes: 12 additions & 1 deletion Sources/NextcloudKit/NKCommon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ public struct NKCommon: Sendable {
return serverUrl.asUrl
}

func findHeader(_ header: String, allHeaderFields: [AnyHashable: Any]?) -> String? {
public 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)) }

Expand All @@ -377,6 +377,17 @@ public struct NKCommon: Sendable {
return nil
}

/// Normalizes an HTTP ETag value by removing wrapping quotes when present.
/// - Parameter value: The raw ETag header value returned by the HTTP response.
/// - Returns: The normalized ETag value without surrounding double quotes.
public func normalizedETag(_ value: String?) -> String? {
guard let value else {
return nil
}

return value.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
}

func getHostName(urlString: String) -> String? {
if let url = URL(string: urlString) {
guard let hostName = url.host else { return nil }
Expand Down
44 changes: 7 additions & 37 deletions Sources/NextcloudKit/NextcloudKit+Download.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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, _ headers: [AnyHashable: any Sendable]?, _ afError: AFError?, _ nKError: NKError) -> Void) {
completionHandler: @escaping (_ account: String, _ response: AFDownloadResponse<URL?>?, _ nKError: NKError) -> Void) {
var convertible: URLConvertible?
if serverUrlFileName is URL {
convertible = serverUrlFileName as? URLConvertible
Expand All @@ -36,7 +36,7 @@ public extension NextcloudKit {
guard let url = convertible,
let nkSession = nkCommonInstance.nksessions.session(forAccount: account),
let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else {
return options.queue.async { completionHandler(account, nil, nil, 0, nil, nil, .urlError) }
return options.queue.async { completionHandler(account, nil, .urlError) }
}
var destination: Alamofire.DownloadRequest.Destination?
let fileNamePathLocalDestinationURL = NSURL.fileURL(withPath: fileNameLocalPath)
Expand All @@ -51,30 +51,8 @@ public extension NextcloudKit {
} .downloadProgress { progress in
options.queue.async { progressHandler(progress) }
} .response(queue: self.nkCommonInstance.backgroundQueue) { response in
if let error = response.error {
let resultError = NKError(error: error, afResponse: response, responseData: nil)
options.queue.async { completionHandler(account, nil, nil, 0, response.response?.allHeaderFields, error, resultError) }
} else {
var date: Date?
var etag: String?
var length: Int64 = 0

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 dateRaw = self.nkCommonInstance.findHeader("Date", allHeaderFields: response.response?.allHeaderFields) {
date = dateRaw.parsedDate(using: "yyyy-MM-dd HH:mm:ss")
}

options.queue.async { completionHandler(account, etag, date, length, response.response?.allHeaderFields, nil, .success) }
options.queue.async {
completionHandler(account, response, self.evaluateDownloadResponse(response))
}
}

Expand All @@ -100,11 +78,7 @@ public extension NextcloudKit {
progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }
) async -> (
account: String,
etag: String?,
date: Date?,
length: Int64,
headers: [AnyHashable: any Sendable]?,
afError: AFError?,
response: AFDownloadResponse<URL?>?,
nkError: NKError
) {
await withCheckedContinuation { continuation in
Expand All @@ -114,14 +88,10 @@ public extension NextcloudKit {
options: options,
requestHandler: requestHandler,
taskHandler: taskHandler,
progressHandler: progressHandler) { account, etag, date, length, headers, afError, nkError in
progressHandler: progressHandler) { account, response, nkError in
continuation.resume(returning: (
account: account,
etag: etag,
date: date,
length: length,
headers: headers,
afError: afError,
response: response,
nkError: nkError
))
}
Expand Down
44 changes: 4 additions & 40 deletions Sources/NextcloudKit/NextcloudKit+Upload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ 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, _ ownerId: String?, _ permissions: String?, _ response: AFDataResponse<Data>?, _ nkError: NKError) -> Void) {
completionHandler: @escaping (_ account: String, _ response: AFDataResponse<Data>?, _ nkError: NKError) -> Void) {
var convertible: URLConvertible?
var uploadedSize: Int64 = 0

if serverUrlFileName is URL {
convertible = serverUrlFileName as? URLConvertible
Expand All @@ -54,7 +53,7 @@ public extension NextcloudKit {
guard let url = convertible,
let nkSession = nkCommonInstance.nksessions.session(forAccount: account),
var headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else {
return options.queue.async { completionHandler(account, nil, nil, nil, 0, nil, nil, nil, .urlError) }
return options.queue.async { completionHandler(account, nil, .urlError) }
}
let fileNameLocalPathUrl = URL(fileURLWithPath: fileNameLocalPath)
// Epoch of linux do not permitted negativ value
Expand All @@ -79,33 +78,10 @@ public extension NextcloudKit {
task.taskDescription = options.taskDescription
options.queue.async { taskHandler(task) }
}) .uploadProgress { progress in
uploadedSize = progress.totalUnitCount
options.queue.async { progressHandler(progress) }
} .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in
var ocId: String?, etag: String?, date: Date?, ownerId: String?, permissions: String?
let allHeaderFields = response.response?.allHeaderFields

ownerId = self.nkCommonInstance.findHeader("x-nc-ownerid", allHeaderFields: allHeaderFields)
permissions = self.nkCommonInstance.findHeader("x-nc-permissions", allHeaderFields: allHeaderFields)
if self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: allHeaderFields) != nil {
ocId = self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: allHeaderFields)
} else if self.nkCommonInstance.findHeader("fileid", allHeaderFields: allHeaderFields) != nil {
ocId = self.nkCommonInstance.findHeader("fileid", allHeaderFields: allHeaderFields)
}
if self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: allHeaderFields) != nil {
etag = self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: allHeaderFields)
} else if self.nkCommonInstance.findHeader("etag", allHeaderFields: allHeaderFields) != nil {
etag = self.nkCommonInstance.findHeader("etag", allHeaderFields: allHeaderFields)
}
if etag != nil {
etag = etag?.replacingOccurrences(of: "\"", with: "")
}
if let dateRaw = self.nkCommonInstance.findHeader("date", allHeaderFields: allHeaderFields) {
date = dateRaw.parsedDate(using: "EEE, dd MMM y HH:mm:ss zzz")
}

options.queue.async {
completionHandler(account, ocId, etag, date, uploadedSize, ownerId, permissions, response, self.evaluateResponse(response))
completionHandler(account, response, self.evaluateResponse(response))
}
}

Expand Down Expand Up @@ -141,12 +117,6 @@ public extension NextcloudKit {
progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }
) async -> (
account: String,
ocId: String?,
etag: String?,
date: Date?,
size: Int64,
ownerId: String?,
permissions: String?,
response: AFDataResponse<Data>?,
error: NKError
) {
Expand All @@ -161,15 +131,9 @@ public extension NextcloudKit {
options: options,
requestHandler: requestHandler,
taskHandler: taskHandler,
progressHandler: progressHandler) { account, ocId, etag, date, size, ownerId, permissions, response, error in
progressHandler: progressHandler) { account, response, error in
continuation.resume(returning: (
account: account,
ocId: ocId,
etag: etag,
date: date,
size: size,
ownerId: ownerId,
permissions: permissions,
response: response,
error: error
))
Expand Down
36 changes: 36 additions & 0 deletions Sources/NextcloudKit/NextcloudKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,40 @@ open class NextcloudKit: @unchecked Sendable {
return NKError(error: error, afResponse: response, responseData: response.data)
}
}

/// Evaluates a generic Alamofire download response into NKError with simple HTTP-aware rules.
/// - Note:
/// - Explicit cancellations return `.cancelled`.
/// - Any HTTP 2xx is considered success, regardless of downloaded file presence.
/// - If no HTTP status is available, fall back to Alamofire's `Result`.
func evaluateDownloadResponse<Data>(_ response: AFDownloadResponse<Data>) -> NKError {
// 1) Cancellations take precedence
if let afError = response.error?.asAFError,
afError.isExplicitlyCancelledError {
return .cancelled
}

// 2) Prefer HTTP status code when available
if let code = response.response?.statusCode {
if (200...299).contains(code) {
return .success
}
// Non-2xx: let the error flow below, even if serializer said "success".
}

// 3) Fall back to Alamofire's result.
// This covers transport errors, missing status code, file move errors,
// serializer errors, and other download-specific failures.
switch response.result {
case .success:
return .success

case .failure(let error):
return NKError(
error: error,
afResponse: response,
responseData: nil
)
}
}
}
Loading