Skip to content

Commit 3ae6cbd

Browse files
authored
Merge pull request #5 from darrarski/feature/delete-file
Delete file
2 parents 727cdbb + 2ae8037 commit 3ae6cbd

File tree

5 files changed

+255
-1
lines changed

5 files changed

+255
-1
lines changed

Example/DropboxClientExampleApp/Dependencies.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ extension DropboxClient.Client: DependencyKey {
6262
},
6363
downloadFile: .init { params in
6464
"Preview file content for \(params.path)".data(using: .utf8)!
65+
},
66+
deleteFile: .init { params in
67+
let entry = await entries.withValue {
68+
let index = $0.firstIndex { $0.pathDisplay == params.path }!
69+
let entry = $0[index]
70+
$0.remove(at: index)
71+
return entry
72+
}
73+
return DeleteFile.Result(metadata: entry)
6574
}
6675
)
6776
}()

Example/DropboxClientExampleApp/ExampleView.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,24 @@ struct ExampleView: View {
164164
} label: {
165165
Text("Download File")
166166
}
167+
168+
Button(role: .destructive) {
169+
Task<Void, Never> {
170+
do {
171+
_ = try await client.deleteFile(path: entry.pathDisplay)
172+
if let entries = list?.entries {
173+
list?.entries = entries.filter { $0.pathDisplay != entry.pathDisplay }
174+
}
175+
} catch {
176+
log.error("DeleteFile failure", metadata: [
177+
"error": "\(error)",
178+
"localizedDescription": "\(error.localizedDescription)"
179+
])
180+
}
181+
}
182+
} label: {
183+
Text("Delete File")
184+
}
167185
}
168186
}
169187
}

Sources/DropboxClient/Client.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ public struct Client: Sendable {
22
public init(
33
auth: Auth,
44
listFolder: ListFolder,
5-
downloadFile: DownloadFile
5+
downloadFile: DownloadFile,
6+
deleteFile: DeleteFile
67
) {
78
self.auth = auth
89
self.listFolder = listFolder
910
self.downloadFile = downloadFile
11+
self.deleteFile = deleteFile
1012
}
1113

1214
public var auth: Auth
1315
public var listFolder: ListFolder
1416
public var downloadFile: DownloadFile
17+
public var deleteFile: DeleteFile
1518
}
1619

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

0 commit comments

Comments
 (0)