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
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,15 @@ struct APIClient {
return min(baseDelay * adaptiveMultiplier, rateLimitConfiguration.maxDelay)
}


private func safeEndpointDescription(_ endpoint: Endpoint) -> String {
let method = endpoint.method.rawValue
guard let url = URL(string: endpoint.path) else {
return "[\(method) unknown]"
}
return "[\(method) \(url.path)]"
}

private func buildURLRequest(endpoint: Endpoint) throws -> URLRequest {
guard let url = URL(string: endpoint.path) else {
throw APIClientError(statusCode: -1, message: "Unable to build URL from \(endpoint.path)")
Expand Down Expand Up @@ -428,15 +437,17 @@ extension APIClient {
continuation.resume(with: .failure(CancellationError()))
return
}
let endpointDesc = self.safeEndpointDescription(endpoint)
if debugResponse == true {
print("API CLIENT ERROR", error)
print("API CLIENT ERROR \(endpointDesc)", error)
}
continuation.resume(with: .failure(APIClientError(statusCode: -1, message: error.localizedDescription)))
continuation.resume(with: .failure(APIClientError(statusCode: -1, message: "\(endpointDesc) \(error.localizedDescription)")))
return
}

guard let httpResponse = response as? HTTPURLResponse else {
continuation.resume(with: .failure(APIClientError(statusCode: -1, message: "No HTTP response")))
let endpointDesc = self.safeEndpointDescription(endpoint)
continuation.resume(with: .failure(APIClientError(statusCode: -1, message: "\(endpointDesc) No HTTP response")))
return
}

Expand All @@ -448,20 +459,22 @@ extension APIClient {

do {
guard let data = data else {
let endpointDesc = self.safeEndpointDescription(endpoint)
throw APIClientError(
statusCode: httpResponse.statusCode,
message: "Response is empty",
message: "\(endpointDesc) Response is empty",
headers: headers
)
}

if !(200...399).contains(httpResponse.statusCode) {
let endpointDesc = self.safeEndpointDescription(endpoint)
let errorMessage: String

if let decodedError = try? JSONDecoder().decode(APIErrorResponse.self, from: data) {
errorMessage = decodedError.message
errorMessage = "\(endpointDesc) \(decodedError.message)"
} else {
errorMessage = HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)
errorMessage = "\(endpointDesc) \(HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))"
}

let apiError = APIClientError(
Expand All @@ -483,15 +496,17 @@ extension APIClient {
continuation.resume(with: .success(json))

} catch let decodingError as DecodingError {
let message = decodingError.localizedDescription
let endpointDesc = self.safeEndpointDescription(endpoint)
let message = "\(endpointDesc) \(decodingError.localizedDescription)"
continuation.resume(with: .failure(APIClientError(
statusCode: httpResponse.statusCode,
message: message,
responseBody: data ?? Data(),
headers: headers
)))
} catch {
let message = error.localizedDescription
let endpointDesc = self.safeEndpointDescription(endpoint)
let message = "\(endpointDesc) \(error.localizedDescription)"
continuation.resume(with: .failure(APIClientError(
statusCode: httpResponse.statusCode,
message: message,
Expand Down
203 changes: 189 additions & 14 deletions Sources/InternxtSwiftCore/Services/Network/Download.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,98 @@ public class Download: NSObject {

func start(bucketId: String, fileId: String, destination: URL, progressHandler: ProgressHandler? = nil, debug: Bool = false) async throws -> DownloadResult {
if bucketId.isValidHex == false {
throw DownloadError.InvalidBucketId
throw EnrichedError(
code: .downloadInvalidBucket,
step: .downloadGetInfo,
context: ["bucket_id": bucketId],
cause: DownloadError.InvalidBucketId
)
}

let info: GetFileInfoResponse
do {
info = try await networkAPI.getFileInfo(bucketId: bucketId, fileId: fileId, debug: debug)
} catch let enrichedError as EnrichedError {
throw enrichedError
} catch {
let errorCode: ErrorCode
var context: [String: String] = [
"bucket_id": bucketId,
"file_id": fileId
]

if let apiError = error as? APIClientError {
context["status_code"] = String(apiError.statusCode)
context["api_message"] = apiError.message

switch apiError.statusCode {
case 401: errorCode = .apiUnauthorized
case 404: errorCode = .apiNotFound
case 500...599: errorCode = .apiServerError
default: errorCode = .downloadInfoFailed
}
} else if let urlError = error as? URLError {
context["url_error_code"] = String(urlError.code.rawValue)
switch urlError.code {
case .timedOut: errorCode = .networkTimeout
case .notConnectedToInternet: errorCode = .networkNoConnection
case .networkConnectionLost: errorCode = .networkConnectionLost
case .cannotConnectToHost: errorCode = .networkCannotConnect
default: errorCode = .downloadInfoFailed
}
} else {
errorCode = .downloadInfoFailed
}

throw EnrichedError(
code: errorCode,
step: .downloadGetInfo,
context: context,
cause: error
)
}
let info = try await networkAPI.getFileInfo(bucketId: bucketId, fileId: fileId, debug: debug)

let isV1Download = info.version == 1

if isV1Download {
let mirrors = try await networkAPI.getFileMirrors(bucketId: bucketId, fileId: fileId, debug: debug)
let mirrors: [FileMirrorShard]
do {
mirrors = try await networkAPI.getFileMirrors(bucketId: bucketId, fileId: fileId, debug: debug)
} catch let enrichedError as EnrichedError {
throw enrichedError
} catch {
var context: [String: String] = [
"bucket_id": bucketId,
"file_id": fileId
]
if let apiError = error as? APIClientError {
context["status_code"] = String(apiError.statusCode)
context["api_message"] = apiError.message
}
throw EnrichedError(
code: .downloadMirrorsFailed,
step: .downloadGetUrl,
context: context,
cause: error
)
}

guard let mirror = mirrors.first else {
throw DownloadError.NoMirrorsFound
guard mirrors.first != nil else {
throw EnrichedError(
code: .downloadNoMirrors,
step: .downloadGetUrl,
context: [
"bucket_id": bucketId,
"file_id": fileId
],
cause: DownloadError.NoMirrorsFound
)
}

try await mirrors.asyncForEach{ mirror in
let _ = try await downloadEncryptedFile(
let _ = try await self.downloadEncryptedFile(
bucketId: bucketId,
fileId: fileId,
downloadUrl: mirror.url,
destinationURL: destination,
overwriteFile: false
Expand All @@ -88,29 +165,110 @@ public class Download: NSObject {
}

guard let shards = info.shards else {
throw DownloadError.MissingShards
throw EnrichedError(
code: .downloadMissingShards,
step: .downloadGetUrl,
context: [
"bucket_id": bucketId,
"file_id": fileId
],
cause: DownloadError.MissingShards
)
}



if shards.count > 1 {
throw DownloadError.MultipartDownloadNotSupported
throw EnrichedError(
code: .downloadFailed,
step: .downloadGetUrl,
context: [
"bucket_id": bucketId,
"file_id": fileId,
"shard_count": String(shards.count)
],
cause: DownloadError.MultipartDownloadNotSupported
)
}

guard let shard = shards.first else {
throw DownloadError.MissingShards
throw EnrichedError(
code: .downloadMissingShards,
step: .downloadGetUrl,
context: [
"bucket_id": bucketId,
"file_id": fileId
],
cause: DownloadError.MissingShards
)
}

let url = try await downloadEncryptedFile(downloadUrl: shard.url, destinationURL: destination, progressHandler: progressHandler)
let url = try await downloadEncryptedFile(bucketId: bucketId, fileId: fileId, downloadUrl: shard.url, destinationURL: destination, progressHandler: progressHandler)

return DownloadResult(url: url, expectedContentHash: shard.hash, index: info.index)
}



private func downloadEncryptedFile(downloadUrl: String, destinationURL:URL, progressHandler: ProgressHandler? = nil, overwriteFile: Bool = true) async throws -> URL {
private func downloadEncryptedFile(bucketId: String, fileId: String, downloadUrl: String, destinationURL:URL, progressHandler: ProgressHandler? = nil, overwriteFile: Bool = true) async throws -> URL {
return try await withCheckedThrowingContinuation{continuation in
let task = urlSession.downloadTask(with: URL(string: downloadUrl)!, completionHandler: {localURL,_,_ in
guard let requestURL = URL(string: downloadUrl) else {
continuation.resume(throwing: EnrichedError(
code: .downloadS3Failed,
step: .downloadFromS3,
context: [
"bucket_id": bucketId,
"file_id": fileId
],
cause: DownloadError.MissingDownloadURL
))
return
}

let task = self.urlSession.downloadTask(with: requestURL, completionHandler: {localURL, response, error in
if let error = error {
let errorCode: ErrorCode
var context: [String: String] = [
"bucket_id": bucketId,
"file_id": fileId
]

if let urlError = error as? URLError {
context["url_error_code"] = String(urlError.code.rawValue)
switch urlError.code {
case .timedOut: errorCode = .networkTimeout
case .notConnectedToInternet: errorCode = .networkNoConnection
case .networkConnectionLost: errorCode = .networkConnectionLost
case .cannotConnectToHost: errorCode = .networkCannotConnect
default: errorCode = .downloadS3Failed
}
} else {
errorCode = .downloadS3Failed
}

continuation.resume(throwing: EnrichedError(
code: errorCode,
step: .downloadFromS3,
context: context,
cause: error
))
return
}

if let httpResponse = response as? HTTPURLResponse, !(200...399).contains(httpResponse.statusCode) {
continuation.resume(throwing: EnrichedError(
code: .downloadS3Failed,
step: .downloadFromS3,
context: [
"bucket_id": bucketId,
"file_id": fileId,
"status_code": String(httpResponse.statusCode)
],
cause: DownloadError.DownloadNotSuccessful
))
return
}

if let localURL = localURL {
defer {
try? FileManager.default.removeItem(at: localURL)
Expand All @@ -132,11 +290,28 @@ public class Download: NSObject {

continuation.resume(returning: destinationURL)
} catch {
continuation.resume(throwing: DownloadError.FailedToCopyDownloadedURL)
continuation.resume(throwing: EnrichedError(
code: .downloadCopyFailed,
step: .downloadFromS3,
context: [
"bucket_id": bucketId,
"file_id": fileId,
"destination_path": destinationURL.path
],
cause: error
))
}

} else {
continuation.resume(throwing: DownloadError.MissingDownloadURL)
continuation.resume(throwing: EnrichedError(
code: .downloadS3Failed,
step: .downloadFromS3,
context: [
"bucket_id": bucketId,
"file_id": fileId
],
cause: DownloadError.MissingDownloadURL
))
}
})

Expand Down
Loading
Loading