Skip to content

Commit

Permalink
Merge pull request #3 from darrarski/feature/download-file
Browse files Browse the repository at this point in the history
Download File
  • Loading branch information
darrarski authored Jul 7, 2023
2 parents d11dd54 + 3c0502a commit f66c94a
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 1 deletion.
3 changes: 3 additions & 0 deletions Example/DropboxClientExampleApp/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ extension DropboxClient.Client: DependencyKey {
entries: await entries.value,
hasMore: false
)
},
downloadFile: .init { params in
"Preview file content for \(params.path)".data(using: .utf8)!
}
)
}()
Expand Down
35 changes: 35 additions & 0 deletions Example/DropboxClientExampleApp/ExampleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct ExampleView: View {
let log = Logger(label: Bundle.main.bundleIdentifier!)
@State var isSignedIn = false
@State var list: ListFolder.Result?
@State var fileContentAlert: String?

var body: some View {
Form {
Expand All @@ -33,6 +34,20 @@ struct ExampleView: View {
}
}
}
.alert(
"File content",
isPresented: Binding(
get: { fileContentAlert != nil },
set: { isPresented in
if !isPresented {
fileContentAlert = nil
}
}
),
presenting: fileContentAlert,
actions: { _ in Button("OK") {} },
message: { Text($0) }
)
}

var authSection: some View {
Expand Down Expand Up @@ -129,6 +144,26 @@ struct ExampleView: View {
Text("Server modified").font(.caption).foregroundColor(.secondary)
Text(entry.serverModified.formatted(date: .complete, time: .complete))
}

Button {
Task<Void, Never> {
do {
let data = try await client.downloadFile(path: entry.id)
if let string = String(data: data, encoding: .utf8) {
fileContentAlert = string
} else {
fileContentAlert = data.base64EncodedString()
}
} catch {
log.error("DownloadFile failure", metadata: [
"error": "\(error)",
"localizedDescription": "\(error.localizedDescription)"
])
}
}
} label: {
Text("Download File")
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
Basic Dropbox HTTP API client that does not depend on Dropbox's SDK. No external dependencies.

- Authorize access
- List folder
- Download file
- ...

## 📖 Usage
Expand Down
9 changes: 8 additions & 1 deletion Sources/DropboxClient/Client.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
public struct Client: Sendable {
public init(
auth: Auth,
listFolder: ListFolder
listFolder: ListFolder,
downloadFile: DownloadFile
) {
self.auth = auth
self.listFolder = listFolder
self.downloadFile = downloadFile
}

public var auth: Auth
public var listFolder: ListFolder
public var downloadFile: DownloadFile
}

extension Client {
Expand All @@ -32,6 +35,10 @@ extension Client {
listFolder: .live(
keychain: keychain,
httpClient: httpClient
),
downloadFile: .live(
keychain: keychain,
httpClient: httpClient
)
)
}
Expand Down
74 changes: 74 additions & 0 deletions Sources/DropboxClient/DownloadFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Foundation

public struct DownloadFile: Sendable {
public struct Params: Sendable, Equatable, Encodable {
public init(path: String) {
self.path = path
}

public var path: String
}

public enum Error: Swift.Error, Sendable, Equatable {
case notAuthorized
case response(statusCode: Int?, data: Data)
}

public typealias Run = @Sendable (Params) async throws -> Data

public init(run: @escaping Run) {
self.run = run
}

public var run: Run

public func callAsFunction(_ params: Params) async throws -> Data {
try await run(params)
}

public func callAsFunction(path: String) async throws -> Data {
try await run(.init(path: path))
}
}

extension DownloadFile {
public static func live(
keychain: Keychain,
httpClient: HTTPClient
) -> DownloadFile {
DownloadFile { params in
guard let credentials = await keychain.loadCredentials() else {
throw Error.notAuthorized
}

let request: URLRequest = try {
var components = URLComponents()
components.scheme = "https"
components.host = "content.dropboxapi.com"
components.path = "/2/files/download"

var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
request.setValue(
"\(credentials.tokenType) \(credentials.accessToken)",
forHTTPHeaderField: "Authorization"
)
request.setValue(
String(data: try JSONEncoder.api.encode(params), encoding: .utf8),
forHTTPHeaderField: "Dropbox-API-Arg"
)

return request
}()

let (responseData, response) = try await httpClient.data(for: request)
let statusCode = (response as? HTTPURLResponse)?.statusCode

guard let statusCode, (200..<300).contains(statusCode) else {
throw Error.response(statusCode: statusCode, data: responseData)
}

return responseData
}
}
}
107 changes: 107 additions & 0 deletions Tests/DropboxClientTests/DownloadFileTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import XCTest
@testable import DropboxClient

final class DownloadFileTests: XCTestCase {
func testDownloadFile() async throws {
let credentials = Credentials.test
let httpRequests = ActorIsolated<[URLRequest]>([])
let downloadFile = DownloadFile.live(
keychain: {
var keychain = Keychain.unimplemented()
keychain.loadCredentials = { credentials }
return keychain
}(),
httpClient: .init { request in
await httpRequests.withValue { $0.append(request) }
return (
"test file content".data(using: .utf8)!,
HTTPURLResponse(
url: URL(filePath: "/"),
statusCode: 200,
httpVersion: nil,
headerFields: nil
)!
)
}
)
let params = DownloadFile.Params(path: "test-path")

let result = try await downloadFile(params)

await httpRequests.withValue {
let expectedRequest: URLRequest = {
let url = URL(string: "https://content.dropboxapi.com/2/files/download")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = [
"Authorization": "\(credentials.tokenType) \(credentials.accessToken)",
"Dropbox-API-Arg": String(
data: try! JSONEncoder.api.encode(params),
encoding: .utf8
)!
]
return request
}()

XCTAssertEqual($0, [expectedRequest])
XCTAssertNil($0.first?.httpBody)
}
XCTAssertEqual(result, "test file content".data(using: .utf8)!)
}

func testDownloadFileWhenNotAuthorized() async {
let downloadFile = DownloadFile.live(
keychain: {
var keychain = Keychain.unimplemented()
keychain.loadCredentials = { nil }
return keychain
}(),
httpClient: .unimplemented()
)

do {
_ = try await downloadFile(path: "")
XCTFail("Expected to throw, but didn't")
} catch {
XCTAssertEqual(
error as? DownloadFile.Error, .notAuthorized,
"Expected to throw .notAuthorized, got \(error)"
)
}
}

func testDownloadFileErrorResponse() async {
let downloadFile = DownloadFile.live(
keychain: {
var keychain = Keychain.unimplemented()
keychain.loadCredentials = { .test }
return keychain
}(),
httpClient: .init { _ in
(
"Error!!!".data(using: .utf8)!,
HTTPURLResponse(
url: URL(filePath: "/"),
statusCode: 500,
httpVersion: nil,
headerFields: nil
)!
)
}
)

do {
_ = try await downloadFile(path: "")
XCTFail("Expected to throw, but didn't")
} catch {
XCTAssertEqual(
error as? DownloadFile.Error,
.response(
statusCode: 500,
data: "Error!!!".data(using: .utf8)!
),
"Expected to throw response error, got \(error)"
)
}
}
}

0 comments on commit f66c94a

Please sign in to comment.