Skip to content

Commit f66c94a

Browse files
authored
Merge pull request #3 from darrarski/feature/download-file
Download File
2 parents d11dd54 + 3c0502a commit f66c94a

File tree

6 files changed

+229
-1
lines changed

6 files changed

+229
-1
lines changed

Example/DropboxClientExampleApp/Dependencies.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ extension DropboxClient.Client: DependencyKey {
5959
entries: await entries.value,
6060
hasMore: false
6161
)
62+
},
63+
downloadFile: .init { params in
64+
"Preview file content for \(params.path)".data(using: .utf8)!
6265
}
6366
)
6467
}()

Example/DropboxClientExampleApp/ExampleView.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ struct ExampleView: View {
88
let log = Logger(label: Bundle.main.bundleIdentifier!)
99
@State var isSignedIn = false
1010
@State var list: ListFolder.Result?
11+
@State var fileContentAlert: String?
1112

1213
var body: some View {
1314
Form {
@@ -33,6 +34,20 @@ struct ExampleView: View {
3334
}
3435
}
3536
}
37+
.alert(
38+
"File content",
39+
isPresented: Binding(
40+
get: { fileContentAlert != nil },
41+
set: { isPresented in
42+
if !isPresented {
43+
fileContentAlert = nil
44+
}
45+
}
46+
),
47+
presenting: fileContentAlert,
48+
actions: { _ in Button("OK") {} },
49+
message: { Text($0) }
50+
)
3651
}
3752

3853
var authSection: some View {
@@ -129,6 +144,26 @@ struct ExampleView: View {
129144
Text("Server modified").font(.caption).foregroundColor(.secondary)
130145
Text(entry.serverModified.formatted(date: .complete, time: .complete))
131146
}
147+
148+
Button {
149+
Task<Void, Never> {
150+
do {
151+
let data = try await client.downloadFile(path: entry.id)
152+
if let string = String(data: data, encoding: .utf8) {
153+
fileContentAlert = string
154+
} else {
155+
fileContentAlert = data.base64EncodedString()
156+
}
157+
} catch {
158+
log.error("DownloadFile failure", metadata: [
159+
"error": "\(error)",
160+
"localizedDescription": "\(error.localizedDescription)"
161+
])
162+
}
163+
}
164+
} label: {
165+
Text("Download File")
166+
}
132167
}
133168
}
134169
}

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
Basic Dropbox HTTP API client that does not depend on Dropbox's SDK. No external dependencies.
77

88
- Authorize access
9+
- List folder
10+
- Download file
911
- ...
1012

1113
## 📖 Usage

Sources/DropboxClient/Client.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
public struct Client: Sendable {
22
public init(
33
auth: Auth,
4-
listFolder: ListFolder
4+
listFolder: ListFolder,
5+
downloadFile: DownloadFile
56
) {
67
self.auth = auth
78
self.listFolder = listFolder
9+
self.downloadFile = downloadFile
810
}
911

1012
public var auth: Auth
1113
public var listFolder: ListFolder
14+
public var downloadFile: DownloadFile
1215
}
1316

1417
extension Client {
@@ -32,6 +35,10 @@ extension Client {
3235
listFolder: .live(
3336
keychain: keychain,
3437
httpClient: httpClient
38+
),
39+
downloadFile: .live(
40+
keychain: keychain,
41+
httpClient: httpClient
3542
)
3643
)
3744
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import Foundation
2+
3+
public struct DownloadFile: Sendable {
4+
public struct Params: Sendable, Equatable, Encodable {
5+
public init(path: String) {
6+
self.path = path
7+
}
8+
9+
public var path: String
10+
}
11+
12+
public enum Error: Swift.Error, Sendable, Equatable {
13+
case notAuthorized
14+
case response(statusCode: Int?, data: Data)
15+
}
16+
17+
public typealias Run = @Sendable (Params) async throws -> Data
18+
19+
public init(run: @escaping Run) {
20+
self.run = run
21+
}
22+
23+
public var run: Run
24+
25+
public func callAsFunction(_ params: Params) async throws -> Data {
26+
try await run(params)
27+
}
28+
29+
public func callAsFunction(path: String) async throws -> Data {
30+
try await run(.init(path: path))
31+
}
32+
}
33+
34+
extension DownloadFile {
35+
public static func live(
36+
keychain: Keychain,
37+
httpClient: HTTPClient
38+
) -> DownloadFile {
39+
DownloadFile { params in
40+
guard let credentials = await keychain.loadCredentials() else {
41+
throw Error.notAuthorized
42+
}
43+
44+
let request: URLRequest = try {
45+
var components = URLComponents()
46+
components.scheme = "https"
47+
components.host = "content.dropboxapi.com"
48+
components.path = "/2/files/download"
49+
50+
var request = URLRequest(url: components.url!)
51+
request.httpMethod = "POST"
52+
request.setValue(
53+
"\(credentials.tokenType) \(credentials.accessToken)",
54+
forHTTPHeaderField: "Authorization"
55+
)
56+
request.setValue(
57+
String(data: try JSONEncoder.api.encode(params), encoding: .utf8),
58+
forHTTPHeaderField: "Dropbox-API-Arg"
59+
)
60+
61+
return request
62+
}()
63+
64+
let (responseData, response) = try await httpClient.data(for: request)
65+
let statusCode = (response as? HTTPURLResponse)?.statusCode
66+
67+
guard let statusCode, (200..<300).contains(statusCode) else {
68+
throw Error.response(statusCode: statusCode, data: responseData)
69+
}
70+
71+
return responseData
72+
}
73+
}
74+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import XCTest
2+
@testable import DropboxClient
3+
4+
final class DownloadFileTests: XCTestCase {
5+
func testDownloadFile() async throws {
6+
let credentials = Credentials.test
7+
let httpRequests = ActorIsolated<[URLRequest]>([])
8+
let downloadFile = DownloadFile.live(
9+
keychain: {
10+
var keychain = Keychain.unimplemented()
11+
keychain.loadCredentials = { credentials }
12+
return keychain
13+
}(),
14+
httpClient: .init { request in
15+
await httpRequests.withValue { $0.append(request) }
16+
return (
17+
"test file content".data(using: .utf8)!,
18+
HTTPURLResponse(
19+
url: URL(filePath: "/"),
20+
statusCode: 200,
21+
httpVersion: nil,
22+
headerFields: nil
23+
)!
24+
)
25+
}
26+
)
27+
let params = DownloadFile.Params(path: "test-path")
28+
29+
let result = try await downloadFile(params)
30+
31+
await httpRequests.withValue {
32+
let expectedRequest: URLRequest = {
33+
let url = URL(string: "https://content.dropboxapi.com/2/files/download")!
34+
var request = URLRequest(url: url)
35+
request.httpMethod = "POST"
36+
request.allHTTPHeaderFields = [
37+
"Authorization": "\(credentials.tokenType) \(credentials.accessToken)",
38+
"Dropbox-API-Arg": String(
39+
data: try! JSONEncoder.api.encode(params),
40+
encoding: .utf8
41+
)!
42+
]
43+
return request
44+
}()
45+
46+
XCTAssertEqual($0, [expectedRequest])
47+
XCTAssertNil($0.first?.httpBody)
48+
}
49+
XCTAssertEqual(result, "test file content".data(using: .utf8)!)
50+
}
51+
52+
func testDownloadFileWhenNotAuthorized() async {
53+
let downloadFile = DownloadFile.live(
54+
keychain: {
55+
var keychain = Keychain.unimplemented()
56+
keychain.loadCredentials = { nil }
57+
return keychain
58+
}(),
59+
httpClient: .unimplemented()
60+
)
61+
62+
do {
63+
_ = try await downloadFile(path: "")
64+
XCTFail("Expected to throw, but didn't")
65+
} catch {
66+
XCTAssertEqual(
67+
error as? DownloadFile.Error, .notAuthorized,
68+
"Expected to throw .notAuthorized, got \(error)"
69+
)
70+
}
71+
}
72+
73+
func testDownloadFileErrorResponse() async {
74+
let downloadFile = DownloadFile.live(
75+
keychain: {
76+
var keychain = Keychain.unimplemented()
77+
keychain.loadCredentials = { .test }
78+
return keychain
79+
}(),
80+
httpClient: .init { _ in
81+
(
82+
"Error!!!".data(using: .utf8)!,
83+
HTTPURLResponse(
84+
url: URL(filePath: "/"),
85+
statusCode: 500,
86+
httpVersion: nil,
87+
headerFields: nil
88+
)!
89+
)
90+
}
91+
)
92+
93+
do {
94+
_ = try await downloadFile(path: "")
95+
XCTFail("Expected to throw, but didn't")
96+
} catch {
97+
XCTAssertEqual(
98+
error as? DownloadFile.Error,
99+
.response(
100+
statusCode: 500,
101+
data: "Error!!!".data(using: .utf8)!
102+
),
103+
"Expected to throw response error, got \(error)"
104+
)
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)