Skip to content

Commit 324481f

Browse files
authored
Merge pull request #6 from darrarski/feature/upload-file
Upload file
2 parents 3ae6cbd + 1eff35a commit 324481f

File tree

7 files changed

+358
-2
lines changed

7 files changed

+358
-2
lines changed

Example/DropboxClientExampleApp/Dependencies.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ extension DropboxClient.Client: DependencyKey {
7171
return entry
7272
}
7373
return DeleteFile.Result(metadata: entry)
74+
},
75+
uploadFile: .init { params in
76+
FileMetadata(
77+
id: "id:preview-uploaded",
78+
name: "Preview-uploaded.txt",
79+
pathDisplay: "/Preview-uploaded.txt",
80+
pathLower: "/preview-uploaded.txt",
81+
clientModified: Date(),
82+
serverModified: Date(),
83+
isDownloadable: true
84+
)
7485
}
7586
)
7687
}()

Example/DropboxClientExampleApp/ExampleView.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,26 @@ struct ExampleView: View {
9393
} label: {
9494
Text("List Folder")
9595
}
96+
97+
Button {
98+
Task<Void, Never> {
99+
do {
100+
_ = try await client.uploadFile(
101+
path: "/example-upload.txt",
102+
data: "swift-dropbox-client example uploaded file".data(using: .utf8)!,
103+
mode: .add,
104+
autorename: true
105+
)
106+
} catch {
107+
log.error("UploadFile failure", metadata: [
108+
"error": "\(error)",
109+
"localizedDescription": "\(error.localizedDescription)"
110+
])
111+
}
112+
}
113+
} label: {
114+
Text("Upload file")
115+
}
96116
}
97117

98118
if let list {
@@ -165,6 +185,27 @@ struct ExampleView: View {
165185
Text("Download File")
166186
}
167187

188+
Button {
189+
Task<Void, Never> {
190+
do {
191+
_ = try await client.uploadFile(
192+
path: entry.pathDisplay,
193+
data: "Uploaded with overwrite at \(Date().formatted(date: .complete, time: .complete))"
194+
.data(using: .utf8)!,
195+
mode: .overwrite,
196+
autorename: false
197+
)
198+
} catch {
199+
log.error("UploadFile failure", metadata: [
200+
"error": "\(error)",
201+
"localizedDescription": "\(error.localizedDescription)"
202+
])
203+
}
204+
}
205+
} label: {
206+
Text("Upload file (overwrite)")
207+
}
208+
168209
Button(role: .destructive) {
169210
Task<Void, Never> {
170211
do {

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ Basic Dropbox HTTP API client that does not depend on Dropbox's SDK. No external
77

88
- Authorize access
99
- List folder
10+
- Upload file
1011
- Download file
11-
- ...
12+
- Delete file
1213

1314
## 📖 Usage
1415

Sources/DropboxClient/Client.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ public struct Client: Sendable {
33
auth: Auth,
44
listFolder: ListFolder,
55
downloadFile: DownloadFile,
6-
deleteFile: DeleteFile
6+
deleteFile: DeleteFile,
7+
uploadFile: UploadFile
78
) {
89
self.auth = auth
910
self.listFolder = listFolder
1011
self.downloadFile = downloadFile
1112
self.deleteFile = deleteFile
13+
self.uploadFile = uploadFile
1214
}
1315

1416
public var auth: Auth
1517
public var listFolder: ListFolder
1618
public var downloadFile: DownloadFile
1719
public var deleteFile: DeleteFile
20+
public var uploadFile: UploadFile
1821
}
1922

2023
extension Client {
@@ -46,6 +49,10 @@ extension Client {
4649
deleteFile: .live(
4750
keychain: keychain,
4851
httpClient: httpClient
52+
),
53+
uploadFile: .live(
54+
keychain: keychain,
55+
httpClient: httpClient
4956
)
5057
)
5158
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Foundation
2+
3+
public struct FileMetadata: Sendable, Equatable, Identifiable, Decodable {
4+
public init(
5+
id: String,
6+
name: String,
7+
pathDisplay: String,
8+
pathLower: String,
9+
clientModified: Date,
10+
serverModified: Date,
11+
isDownloadable: Bool
12+
) {
13+
self.id = id
14+
self.name = name
15+
self.pathDisplay = pathDisplay
16+
self.pathLower = pathLower
17+
self.clientModified = clientModified
18+
self.serverModified = serverModified
19+
self.isDownloadable = isDownloadable
20+
}
21+
22+
public var id: String
23+
public var name: String
24+
public var pathDisplay: String
25+
public var pathLower: String
26+
public var clientModified: Date
27+
public var serverModified: Date
28+
public var isDownloadable: Bool
29+
}
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+
}

0 commit comments

Comments
 (0)