Skip to content

Commit ed18d97

Browse files
committed
Implement Upload File
1 parent be07ab9 commit ed18d97

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import Foundation
2+
3+
public struct UploadFile: Sendable {
4+
public struct Params: Sendable, Equatable, Encodable {
5+
public enum Mode: String, Sendable, Equatable, Encodable {
6+
case add, overwrite
7+
}
8+
9+
public init(
10+
path: String,
11+
data: Data,
12+
mode: Mode? = nil,
13+
autorename: Bool? = nil
14+
) {
15+
self.path = path
16+
self.data = data
17+
self.mode = mode
18+
self.autorename = autorename
19+
}
20+
21+
public var path: String
22+
public var data: Data
23+
public var mode: Mode?
24+
public var autorename: Bool?
25+
}
26+
27+
public enum Error: Swift.Error, Sendable, Equatable {
28+
case notAuthorized
29+
case response(statusCode: Int?, data: Data)
30+
}
31+
32+
public typealias Run = @Sendable (Params) async throws -> FileMetadata
33+
34+
public init(run: @escaping Run) {
35+
self.run = run
36+
}
37+
38+
public var run: Run
39+
40+
public func callAsFunction(_ params: Params) async throws -> FileMetadata {
41+
try await run(params)
42+
}
43+
44+
public func callAsFunction(
45+
path: String,
46+
data: Data,
47+
mode: Params.Mode? = nil,
48+
autorename: Bool? = nil
49+
) async throws -> FileMetadata {
50+
try await run(.init(
51+
path: path,
52+
data: data,
53+
mode: mode,
54+
autorename: autorename
55+
))
56+
}
57+
}
58+
59+
extension UploadFile {
60+
public static func live(
61+
keychain: Keychain,
62+
httpClient: HTTPClient
63+
) -> UploadFile {
64+
UploadFile { params in
65+
guard let credentials = await keychain.loadCredentials() else {
66+
throw Error.notAuthorized
67+
}
68+
69+
let request: URLRequest = try {
70+
var components = URLComponents()
71+
components.scheme = "https"
72+
components.host = "content.dropboxapi.com"
73+
components.path = "/2/files/upload"
74+
75+
var request = URLRequest(url: components.url!)
76+
request.httpMethod = "POST"
77+
request.setValue(
78+
"\(credentials.tokenType) \(credentials.accessToken)",
79+
forHTTPHeaderField: "Authorization"
80+
)
81+
82+
struct Args: Encodable {
83+
var path: String
84+
var mode: String?
85+
var autorename: Bool?
86+
}
87+
let args = Args(
88+
path: params.path,
89+
mode: params.mode?.rawValue,
90+
autorename: params.autorename
91+
)
92+
let argsData = try JSONEncoder.api.encode(args)
93+
let argsString = String(data: argsData, encoding: .utf8)
94+
95+
request.setValue(
96+
argsString,
97+
forHTTPHeaderField: "Dropbox-API-Arg"
98+
)
99+
request.setValue(
100+
"application/octet-stream",
101+
forHTTPHeaderField: "Content-Type"
102+
)
103+
request.httpBody = params.data
104+
105+
return request
106+
}()
107+
108+
let (responseData, response) = try await httpClient.data(for: request)
109+
let statusCode = (response as? HTTPURLResponse)?.statusCode
110+
111+
guard let statusCode, (200..<300).contains(statusCode) else {
112+
throw Error.response(statusCode: statusCode, data: responseData)
113+
}
114+
115+
let metadata = try JSONDecoder.api.decode(FileMetadata.self, from: responseData)
116+
return metadata
117+
}
118+
}
119+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import XCTest
2+
@testable import DropboxClient
3+
4+
final class UploadFileTests: XCTestCase {
5+
func testUploadFile() async throws {
6+
let params = UploadFile.Params(
7+
path: "/Prime_Numbers.txt",
8+
data: "2,3,5,7...".data(using: .utf8)!,
9+
mode: .add,
10+
autorename: true
11+
)
12+
let credentials = Credentials.test
13+
let httpRequests = ActorIsolated<[URLRequest]>([])
14+
let uploadFile = UploadFile.live(
15+
keychain: {
16+
var keychain = Keychain.unimplemented()
17+
keychain.loadCredentials = { credentials }
18+
return keychain
19+
}(),
20+
httpClient: .init { request in
21+
await httpRequests.withValue { $0.append(request) }
22+
return (
23+
"""
24+
{
25+
"id": "id:a4ayc_80_OEAAAAAAAAAXw",
26+
"name": "Prime_Numbers.txt",
27+
"path_display": "/Homework/math/Prime_Numbers.txt",
28+
"path_lower": "/homework/math/prime_numbers.txt",
29+
"client_modified": "2023-07-06T15:50:38Z",
30+
"server_modified": "2023-07-06T22:10:00Z",
31+
"is_downloadable": true
32+
}
33+
""".data(using: .utf8)!,
34+
HTTPURLResponse(
35+
url: URL(filePath: "/"),
36+
statusCode: 200,
37+
httpVersion: nil,
38+
headerFields: nil
39+
)!
40+
)
41+
}
42+
)
43+
44+
let result = try await uploadFile(params)
45+
46+
try await httpRequests.withValue {
47+
let url = URL(string: "https://content.dropboxapi.com/2/files/upload")!
48+
var expectedRequest = URLRequest(url: url)
49+
expectedRequest.httpMethod = "POST"
50+
struct Args: Encodable {
51+
var path: String
52+
var mode: String?
53+
var autorename: Bool?
54+
}
55+
expectedRequest.allHTTPHeaderFields = [
56+
"Authorization": "\(credentials.tokenType) \(credentials.accessToken)",
57+
"Dropbox-API-Arg": String(
58+
data: try JSONEncoder.api.encode(Args(
59+
path: params.path,
60+
mode: params.mode!.rawValue,
61+
autorename: params.autorename!
62+
)),
63+
encoding: .utf8
64+
)!,
65+
"Content-Type": "application/octet-stream"
66+
]
67+
expectedRequest.httpBody = params.data
68+
69+
XCTAssertEqual($0, [expectedRequest])
70+
XCTAssertEqual($0.first?.httpBody, expectedRequest.httpBody!)
71+
}
72+
XCTAssertEqual(result, FileMetadata(
73+
id: "id:a4ayc_80_OEAAAAAAAAAXw",
74+
name: "Prime_Numbers.txt",
75+
pathDisplay: "/Homework/math/Prime_Numbers.txt",
76+
pathLower: "/homework/math/prime_numbers.txt",
77+
clientModified: Calendar(identifier: .gregorian)
78+
.date(from: DateComponents(
79+
timeZone: TimeZone(secondsFromGMT: 0),
80+
year: 2023, month: 7, day: 6,
81+
hour: 15, minute: 50, second: 38
82+
))!,
83+
serverModified: Calendar(identifier: .gregorian)
84+
.date(from: DateComponents(
85+
timeZone: TimeZone(secondsFromGMT: 0),
86+
year: 2023, month: 7, day: 6,
87+
hour: 22, minute: 10
88+
))!,
89+
isDownloadable: true
90+
))
91+
}
92+
93+
func testUploadFileErrorResponse() async {
94+
let uploadFile = UploadFile.live(
95+
keychain: {
96+
var keychain = Keychain.unimplemented()
97+
keychain.loadCredentials = { .test }
98+
return keychain
99+
}(),
100+
httpClient: .init { request in
101+
(
102+
"Error!!!".data(using: .utf8)!,
103+
HTTPURLResponse(
104+
url: URL(filePath: "/"),
105+
statusCode: 500,
106+
httpVersion: nil,
107+
headerFields: nil
108+
)!
109+
)
110+
}
111+
)
112+
113+
do {
114+
_ = try await uploadFile(path: "/test.txt", data: Data())
115+
XCTFail("Expected to throw, but didn't")
116+
} catch {
117+
XCTAssertEqual(
118+
error as? UploadFile.Error,
119+
.response(
120+
statusCode: 500,
121+
data: "Error!!!".data(using: .utf8)!
122+
),
123+
"Expected to throw response error, got \(error)"
124+
)
125+
}
126+
}
127+
128+
func testUploadFileWhenNotAuthorized() async {
129+
let uploadFile = UploadFile.live(
130+
keychain: {
131+
var keychain = Keychain.unimplemented()
132+
keychain.loadCredentials = { nil }
133+
return keychain
134+
}(),
135+
httpClient: .unimplemented()
136+
)
137+
138+
do {
139+
_ = try await uploadFile(path: "/test.txt", data: Data())
140+
XCTFail("Expected to throw, but didn't")
141+
} catch {
142+
XCTAssertEqual(
143+
error as? UploadFile.Error, .notAuthorized,
144+
"Expected to throw .notAuthorized, got \(error)"
145+
)
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)