Skip to content

Commit d11dd54

Browse files
authored
Merge pull request #2 from darrarski/feature/list-files
List Folder
2 parents b3c08fc + 6532503 commit d11dd54

File tree

10 files changed

+479
-13
lines changed

10 files changed

+479
-13
lines changed

Example/DropboxClientExampleApp/Dependencies.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Dependencies
22
import DropboxClient
3+
import Foundation
34

45
extension DropboxClient.Client: DependencyKey {
56
public static let liveValue = Client.live(
@@ -11,14 +12,54 @@ extension DropboxClient.Client: DependencyKey {
1112

1213
public static let previewValue: Client = {
1314
let isSignedIn = CurrentValueAsyncSequence(false)
15+
let entries = ActorIsolated<[Metadata]>([
16+
Metadata(
17+
tag: .file,
18+
id: "id:preview-1",
19+
name: "Preview-1.txt",
20+
pathDisplay: "/Preview-1.txt",
21+
pathLower: "/preview-1.txt",
22+
clientModified: Date(),
23+
serverModified: Date(),
24+
isDownloadable: true
25+
),
26+
Metadata(
27+
tag: .file,
28+
id: "id:preview-2",
29+
name: "Preview-2.txt",
30+
pathDisplay: "/Preview-2.txt",
31+
pathLower: "/preview-2.txt",
32+
clientModified: Date(),
33+
serverModified: Date(),
34+
isDownloadable: true
35+
),
36+
Metadata(
37+
tag: .file,
38+
id: "id:preview-3",
39+
name: "Preview-3.txt",
40+
pathDisplay: "/Preview-3.txt",
41+
pathLower: "/preview-3.txt",
42+
clientModified: Date(),
43+
serverModified: Date(),
44+
isDownloadable: true
45+
),
46+
])
47+
1448
return Client(
1549
auth: .init(
1650
isSignedIn: { await isSignedIn.value },
1751
isSignedInStream: { isSignedIn.eraseToStream() },
1852
signIn: { await isSignedIn.setValue(true) },
1953
handleRedirect: { _ in },
2054
signOut: { await isSignedIn.setValue(false) }
21-
)
55+
),
56+
listFolder: .init { _ in
57+
ListFolder.Result(
58+
cursor: "curor-1",
59+
entries: await entries.value,
60+
hasMore: false
61+
)
62+
}
2263
)
2364
}()
2465
}

Example/DropboxClientExampleApp/ExampleView.swift

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ struct ExampleView: View {
77
@Dependency(\.dropboxClient) var client
88
let log = Logger(label: Bundle.main.bundleIdentifier!)
99
@State var isSignedIn = false
10+
@State var list: ListFolder.Result?
1011

1112
var body: some View {
1213
Form {
1314
authSection
15+
filesSection
1416
}
1517
.textSelection(.enabled)
1618
.navigationTitle("Example")
@@ -58,6 +60,77 @@ struct ExampleView: View {
5860
}
5961
}
6062
}
63+
64+
@ViewBuilder
65+
var filesSection: some View {
66+
Section("Files") {
67+
Button {
68+
Task<Void, Never> {
69+
do {
70+
list = try await client.listFolder(path: "")
71+
} catch {
72+
log.error("ListFolder failure", metadata: [
73+
"error": "\(error)",
74+
"localizedDescription": "\(error.localizedDescription)"
75+
])
76+
}
77+
}
78+
} label: {
79+
Text("List Folder")
80+
}
81+
}
82+
83+
if let list {
84+
if list.entries.isEmpty {
85+
Section {
86+
Text("No entries")
87+
}
88+
} else {
89+
ForEach(list.entries) { entry in
90+
listEntrySection(entry)
91+
}
92+
}
93+
}
94+
}
95+
96+
func listEntrySection(_ entry: Metadata) -> some View {
97+
Section {
98+
VStack(alignment: .leading) {
99+
Text("Tag").font(.caption).foregroundColor(.secondary)
100+
Text(entry.tag.rawValue)
101+
}
102+
103+
VStack(alignment: .leading) {
104+
Text("ID").font(.caption).foregroundColor(.secondary)
105+
Text(entry.id)
106+
}
107+
108+
VStack(alignment: .leading) {
109+
Text("Name").font(.caption).foregroundColor(.secondary)
110+
Text(entry.name)
111+
}
112+
113+
VStack(alignment: .leading) {
114+
Text("Path (display)").font(.caption).foregroundColor(.secondary)
115+
Text(entry.pathDisplay)
116+
}
117+
118+
VStack(alignment: .leading) {
119+
Text("Path (lower)").font(.caption).foregroundColor(.secondary)
120+
Text(entry.pathLower)
121+
}
122+
123+
VStack(alignment: .leading) {
124+
Text("Client modified").font(.caption).foregroundColor(.secondary)
125+
Text(entry.clientModified.formatted(date: .complete, time: .complete))
126+
}
127+
128+
VStack(alignment: .leading) {
129+
Text("Server modified").font(.caption).foregroundColor(.secondary)
130+
Text(entry.serverModified.formatted(date: .complete, time: .complete))
131+
}
132+
}
133+
}
61134
}
62135

63136
#if DEBUG

Sources/DropboxClient/Auth.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,12 @@ extension Auth {
132132
forHTTPHeaderField: "Content-Type"
133133
)
134134
request.httpBody = [
135-
"code": code,
136-
"grant_type": "authorization_code",
137-
"redirect_uri": config.redirectURI,
138-
"code_verifier": codeVerifier,
139-
"client_id": config.appKey,
140-
].map { key, value in "\(key)=\(value)" }
141-
.joined(separator: "&")
142-
.data(using: .utf8)
135+
"code=\(code)",
136+
"grant_type=authorization_code",
137+
"redirect_uri=\(config.redirectURI)",
138+
"code_verifier=\(codeVerifier)",
139+
"client_id=\(config.appKey)",
140+
].joined(separator: "&").data(using: .utf8)
143141

144142
return request
145143
}()
@@ -161,7 +159,7 @@ extension Auth {
161159
var accountId: String
162160
}
163161

164-
let responseBody = try JSONDecoder.auth.decode(
162+
let responseBody = try JSONDecoder.api.decode(
165163
ResponseBody.self,
166164
from: responseData
167165
)

Sources/DropboxClient/Client.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
public struct Client: Sendable {
22
public init(
3-
auth: Auth
3+
auth: Auth,
4+
listFolder: ListFolder
45
) {
56
self.auth = auth
7+
self.listFolder = listFolder
68
}
79

810
public var auth: Auth
11+
public var listFolder: ListFolder
912
}
1013

1114
extension Client {
@@ -25,6 +28,10 @@ extension Client {
2528
openURL: openURL,
2629
dateGenerator: dateGenerator,
2730
pkceUtils: pkceUtils
31+
),
32+
listFolder: .live(
33+
keychain: keychain,
34+
httpClient: httpClient
2835
)
2936
)
3037
}

Sources/DropboxClient/Coding.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
import Foundation
22

3+
extension DateFormatter {
4+
static let api: DateFormatter = {
5+
let formatter = DateFormatter()
6+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
7+
formatter.timeZone = TimeZone(secondsFromGMT: 0)
8+
return formatter
9+
}()
10+
}
11+
312
extension JSONDecoder {
4-
static let auth: JSONDecoder = {
13+
static let api: JSONDecoder = {
514
let decoder = JSONDecoder()
615
decoder.keyDecodingStrategy = .convertFromSnakeCase
16+
decoder.dateDecodingStrategy = .formatted(.api)
717
return decoder
818
}()
919
}
20+
21+
extension JSONEncoder {
22+
static let api: JSONEncoder = {
23+
let encoder = JSONEncoder()
24+
encoder.keyEncodingStrategy = .convertToSnakeCase
25+
encoder.dateEncodingStrategy = .formatted(.api)
26+
return encoder
27+
}()
28+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import Foundation
2+
3+
public struct ListFolder: Sendable {
4+
public struct Params: Sendable, Equatable, Encodable {
5+
public init(
6+
path: String,
7+
recursive: Bool? = nil,
8+
includeDeleted: Bool? = nil,
9+
limit: Int? = nil,
10+
includeNonDownloadableFiles: Bool? = nil
11+
) {
12+
self.path = path
13+
self.recursive = recursive
14+
self.includeDeleted = includeDeleted
15+
self.limit = limit
16+
self.includeNonDownloadableFiles = includeNonDownloadableFiles
17+
}
18+
19+
public var path: String
20+
public var recursive: Bool?
21+
public var includeDeleted: Bool?
22+
public var limit: Int?
23+
public var includeNonDownloadableFiles: Bool?
24+
}
25+
26+
public struct Result: Sendable, Equatable, Decodable {
27+
public init(cursor: String, entries: [Metadata], hasMore: Bool) {
28+
self.cursor = cursor
29+
self.entries = entries
30+
self.hasMore = hasMore
31+
}
32+
33+
public var cursor: String
34+
public var entries: [Metadata]
35+
public var hasMore: Bool
36+
}
37+
38+
public enum Error: Swift.Error, Sendable, Equatable {
39+
case notAuthorized
40+
case response(statusCode: Int?, data: Data)
41+
}
42+
43+
public typealias Run = @Sendable (Params) async throws -> Result
44+
45+
public init(run: @escaping Run) {
46+
self.run = run
47+
}
48+
49+
public var run: Run
50+
51+
public func callAsFunction(_ params: Params) async throws -> Result {
52+
try await run(params)
53+
}
54+
55+
public func callAsFunction(
56+
path: String,
57+
recursive: Bool? = nil,
58+
includeDeleted: Bool? = nil,
59+
limit: Int? = nil,
60+
includeNonDownloadableFiles: Bool? = nil
61+
) async throws -> Result {
62+
try await run(.init(
63+
path: path,
64+
recursive: recursive,
65+
includeDeleted: includeDeleted,
66+
limit: limit,
67+
includeNonDownloadableFiles: includeNonDownloadableFiles
68+
))
69+
}
70+
}
71+
72+
extension ListFolder {
73+
public static func live(
74+
keychain: Keychain,
75+
httpClient: HTTPClient
76+
) -> ListFolder {
77+
ListFolder { params in
78+
guard let credentials = await keychain.loadCredentials() else {
79+
throw Error.notAuthorized
80+
}
81+
82+
let request: URLRequest = try {
83+
var components = URLComponents()
84+
components.scheme = "https"
85+
components.host = "api.dropboxapi.com"
86+
components.path = "/2/files/list_folder"
87+
88+
var request = URLRequest(url: components.url!)
89+
request.httpMethod = "POST"
90+
request.setValue(
91+
"\(credentials.tokenType) \(credentials.accessToken)",
92+
forHTTPHeaderField: "Authorization"
93+
)
94+
request.setValue(
95+
"application/json",
96+
forHTTPHeaderField: "Content-Type"
97+
)
98+
request.httpBody = try JSONEncoder.api.encode(params)
99+
100+
return request
101+
}()
102+
103+
let (responseData, response) = try await httpClient.data(for: request)
104+
let statusCode = (response as? HTTPURLResponse)?.statusCode
105+
106+
guard let statusCode, (200..<300).contains(statusCode) else {
107+
throw Error.response(statusCode: statusCode, data: responseData)
108+
}
109+
110+
return try JSONDecoder.api.decode(Result.self, from: responseData)
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)