-
Notifications
You must be signed in to change notification settings - Fork 10
speeding up the gallery, a bit #720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ddd56b8
9de30de
8f928bb
12b2f52
0200596
c65b2b2
9518791
1f63b9c
e3e8cf6
0c85bc1
33f301f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // Part of BeeSwift. Copyright Beeminder | ||
|
|
||
| import AlamofireImage | ||
|
|
||
| final public class ImageDownloadService { | ||
| static public let shared = ImageDownloadService() | ||
| public let downloader: ImageDownloader | ||
|
|
||
| private init() { downloader = ImageDownloader.default } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,36 +11,12 @@ import Foundation | |
| import OSLog | ||
| import SwiftyJSON | ||
|
|
||
| public enum ServerError: LocalizedError { | ||
| case notFound | ||
| case unauthorized | ||
| case forbidden | ||
| case serverError(Int) | ||
| case custom(String, requestError: Error?) | ||
| public var errorDescription: String? { | ||
| switch self { | ||
| case .notFound: return "Not found" | ||
| case .unauthorized: return "Unauthorized" | ||
| case .forbidden: return "Permission denied" | ||
| case .serverError(let code): return "Server error (\(code)). Please try again later" | ||
| case .custom(let message, _): return message | ||
| } | ||
| } | ||
| var requestError: Error? { | ||
| switch self { | ||
| case .custom(_, let error): return error | ||
| default: return nil | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public class RequestManager { | ||
| public actor RequestManager { | ||
| public let baseURLString = Config().baseURLString | ||
| private let logger = Logger(subsystem: "com.beeminder.beeminder", category: "RequestManager") | ||
| func rawRequest(url: String, method: HTTPMethod, parameters: [String: Any]? = nil, headers: HTTPHeaders) async throws | ||
| -> Any? | ||
| { | ||
|
|
||
| var urlWithSubstitutions = url | ||
| if url.contains("{username}") { | ||
| guard let username = await ServiceLocator.currentUserManager.username else { | ||
|
|
@@ -51,36 +27,29 @@ public class RequestManager { | |
| } | ||
| urlWithSubstitutions = urlWithSubstitutions.replacingOccurrences(of: "{username}", with: username) | ||
| } | ||
|
|
||
| let encoding: ParameterEncoding = if method == .get { URLEncoding.default } else { JSONEncoding.default } // TODO | ||
| let encoding: ParameterEncoding = method == .get ? URLEncoding.default : JSONEncoding.default | ||
| let response = await AF.request( | ||
| "\(baseURLString)/\(urlWithSubstitutions)", | ||
| method: method, | ||
| parameters: parameters, | ||
| encoding: encoding, | ||
| headers: HTTPHeaders.default + headers | ||
| ).validate().serializingData(emptyRequestMethods: [HTTPMethod.post]).response | ||
|
|
||
| switch response.result { | ||
| case .success(let data): | ||
| let asJSON = try? JSONSerialization.jsonObject(with: data) | ||
| return asJSON | ||
|
|
||
| return try await Task.detached(priority: .low) { try JSONSerialization.jsonObject(with: data) }.value | ||
| case .failure(let error): | ||
| logger.error("Error issuing request \(url): \(error, privacy: .public)") | ||
|
|
||
| // Log out the user on an unauthorized response | ||
| if case .responseValidationFailed(let reason) = error { | ||
| if case .unacceptableStatusCode(let code) = reason { | ||
| if code == 401 { try? await ServiceLocator.currentUserManager.signOut() } | ||
| } | ||
| } | ||
|
|
||
| // If we receive an error message from the server use it as our user-visible error | ||
| if let data = response.data, let errorMessage = try JSON(data: data)["error_message"].string { | ||
| throw ServerError.custom(errorMessage, requestError: error) | ||
| } | ||
|
|
||
| // Handle common HTTP errors with specific error types | ||
| if case .responseValidationFailed(let reason) = error { | ||
| if case .unacceptableStatusCode(let code) = reason { | ||
|
|
@@ -93,10 +62,16 @@ public class RequestManager { | |
| } | ||
| } | ||
| } | ||
|
|
||
| throw error | ||
| } | ||
| } | ||
| func authenticationHeaders() -> HTTPHeaders { | ||
| guard let accessToken = ServiceLocator.currentUserManager.accessToken else { return HTTPHeaders() } | ||
| return HTTPHeaders([HTTPHeader(name: "Authorization", value: "Bearer " + accessToken)]) | ||
| } | ||
| } | ||
|
|
||
| extension RequestManager: RequestManaging { | ||
| public func get(url: String, parameters: [String: Any]? = nil) async throws -> Any? { | ||
| return try await rawRequest(url: url, method: .get, parameters: parameters, headers: authenticationHeaders()) | ||
| } | ||
|
|
@@ -109,11 +84,6 @@ public class RequestManager { | |
| public func delete(url: String, parameters: [String: Any]? = nil) async throws -> Any? { | ||
| return try await rawRequest(url: url, method: .delete, parameters: parameters, headers: authenticationHeaders()) | ||
| } | ||
| func authenticationHeaders() -> HTTPHeaders { | ||
| guard let accessToken = ServiceLocator.currentUserManager.accessToken else { return HTTPHeaders() } | ||
| return HTTPHeaders([HTTPHeader(name: "Authorization", value: "Bearer " + accessToken)]) | ||
| } | ||
|
|
||
| public func addDatapoint(urtext: String, slug: String, requestId: String? = nil) async throws -> Any? { | ||
| let params = ["urtext": urtext, "requestid": requestId].compactMapValues { $0 } | ||
| return try await post(url: "api/v1/users/{username}/goals/\(slug)/datapoints.json", parameters: params) | ||
|
|
@@ -128,3 +98,33 @@ extension HTTPHeaders { | |
| return HTTPHeaders(allHeaders) | ||
| } | ||
| } | ||
|
|
||
| extension RequestManager: SignedRequestManaging { | ||
| public func signedGET(url: String, parameters: [String: Any]?) async throws -> Any? { | ||
| let params = signedParameters(parameters) | ||
| return try await rawRequest(url: url, method: .get, parameters: params, headers: authenticationHeaders()) | ||
| } | ||
| public func signedPOST(url: String, parameters: [String: Any]?) async throws -> Any? { | ||
| let params = signedParameters(parameters) | ||
| return try await rawRequest(url: url, method: .post, parameters: params, headers: authenticationHeaders()) | ||
| } | ||
| fileprivate func signedParameters(_ params: [String: Any]?) -> [String: Any]? { | ||
| if params == nil { return params } | ||
| var signed = params | ||
| var base = "" | ||
| var keys = Array(params!.keys) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. force |
||
| keys.sort(by: { $0 < $1 }) | ||
| for key in keys { | ||
| let value: AnyObject? = params![key] as AnyObject? | ||
| if !(value is String) { return params! } | ||
| let allowedCharacterSet = (CharacterSet(charactersIn: "@/").inverted) | ||
| let escapedKey = key.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) | ||
| let escapedValue = (params![key] as AnyObject).addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) | ||
| if base.count > 0 { base += "&" } | ||
| base += "\(escapedKey!)=\(escapedValue!)" | ||
| } | ||
| let token = base.hmac(algorithm: HMACAlgorithm.SHA1, key: Config().requestSigningKey) | ||
| signed?["beemios_token"] = token | ||
| return signed! as [String: Any] | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // Part of BeeSwift. Copyright Beeminder | ||
|
|
||
| public protocol RequestManaging { | ||
| func get(url: String, parameters: [String: Any]?) async throws -> Any? | ||
| func put(url: String, parameters: [String: Any]?) async throws -> Any? | ||
| func post(url: String, parameters: [String: Any]?) async throws -> Any? | ||
| func delete(url: String, parameters: [String: Any]?) async throws -> Any? | ||
| func addDatapoint(urtext: String, slug: String, requestId: String?) async throws -> Any? | ||
| } | ||
|
|
||
| extension RequestManaging { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain what this is for? It seems like functions just calling themselves
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The protocol cannot have a default for the parameter and several places were calling them without specifying the parameter with a nil. This here is a version that does (pass nil as a default) and allows then others to make calls without specifying also the parameters: nil for each and every call.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. made obsolete by #744 |
||
| public func get(url: String, parameters: [String: Any]? = nil) async throws -> Any? { | ||
| try await get(url: url, parameters: parameters) | ||
| } | ||
| public func put(url: String, parameters: [String: Any]? = nil) async throws -> Any? { | ||
| try await put(url: url, parameters: parameters) | ||
| } | ||
| public func post(url: String, parameters: [String: Any]? = nil) async throws -> Any? { | ||
| try await post(url: url, parameters: parameters) | ||
| } | ||
| public func delete(url: String, parameters: [String: Any]? = nil) async throws -> Any? { | ||
| try await delete(url: url, parameters: parameters) | ||
| } | ||
| public func addDatapoint(urtext: String, slug: String, requestId: String? = nil) async throws -> Any? { | ||
| try await addDatapoint(urtext: urtext, slug: slug, requestId: requestId) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Part of BeeSwift. Copyright Beeminder | ||
|
|
||
| import Foundation | ||
|
|
||
| public enum ServerError: LocalizedError { | ||
| case notFound | ||
| case unauthorized | ||
| case forbidden | ||
| case serverError(Int) | ||
| case custom(String, requestError: Error?) | ||
| public var errorDescription: String? { | ||
| switch self { | ||
| case .notFound: return "Not found" | ||
| case .unauthorized: return "Unauthorized" | ||
| case .forbidden: return "Permission denied" | ||
| case .serverError(let code): return "Server error (\(code)). Please try again later" | ||
| case .custom(let message, _): return message | ||
| } | ||
| } | ||
| var requestError: Error? { | ||
| switch self { | ||
| case .custom(_, let error): return error | ||
| default: return nil | ||
| } | ||
| } | ||
| } |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Part of BeeSwift. Copyright Beeminder | ||
|
|
||
| public protocol SignedRequestManaging { | ||
| func signedGET(url: String, parameters: [String: Any]?) async throws -> Any? | ||
| func signedPOST(url: String, parameters: [String: Any]?) async throws -> Any? | ||
| } | ||
|
|
||
| extension SignedRequestManaging { | ||
| public func signedGET(url: String, parameters: [String: Any]? = nil) async throws -> Any? { | ||
| try await signedGET(url: url, parameters: parameters) | ||
| } | ||
| public func signedPOST(url: String, parameters: [String: Any]? = nil) async throws -> Any? { | ||
| try await signedPOST(url: url, parameters: parameters) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
guard