react-native-kookit 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FTP_README.md +322 -0
- package/android/src/main/java/expo/modules/kookit/FtpClient.kt +399 -0
- package/android/src/main/java/expo/modules/kookit/ReactNativeKookitModule.kt +163 -1
- package/build/ReactNativeKookit.types.d.ts +30 -0
- package/build/ReactNativeKookit.types.d.ts.map +1 -1
- package/build/ReactNativeKookit.types.js.map +1 -1
- package/build/ReactNativeKookitModule.d.ts +56 -1
- package/build/ReactNativeKookitModule.d.ts.map +1 -1
- package/build/ReactNativeKookitModule.js.map +1 -1
- package/build/ReactNativeKookitModule.web.d.ts +10 -1
- package/build/ReactNativeKookitModule.web.d.ts.map +1 -1
- package/build/ReactNativeKookitModule.web.js +28 -0
- package/build/ReactNativeKookitModule.web.js.map +1 -1
- package/ios/ReactNativeKookit.podspec +3 -0
- package/ios/ReactNativeKookitModule.swift +889 -3
- package/package.json +5 -2
|
@@ -2,12 +2,753 @@ import ExpoModulesCore
|
|
|
2
2
|
import UIKit
|
|
3
3
|
import MediaPlayer
|
|
4
4
|
import AVFoundation
|
|
5
|
+
import Foundation
|
|
6
|
+
import Network
|
|
7
|
+
|
|
8
|
+
// MARK: - FTP Related Types and Classes
|
|
9
|
+
|
|
10
|
+
struct FtpConnectionConfig {
|
|
11
|
+
let host: String
|
|
12
|
+
let port: Int
|
|
13
|
+
let username: String
|
|
14
|
+
let password: String
|
|
15
|
+
let passive: Bool
|
|
16
|
+
let timeout: TimeInterval
|
|
17
|
+
|
|
18
|
+
init(host: String, port: Int = 21, username: String, password: String, passive: Bool = true, timeout: TimeInterval = 30.0) {
|
|
19
|
+
self.host = host
|
|
20
|
+
self.port = port
|
|
21
|
+
self.username = username
|
|
22
|
+
self.password = password
|
|
23
|
+
self.passive = passive
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
struct FtpFileInfo {
|
|
29
|
+
let name: String
|
|
30
|
+
let isDirectory: Bool
|
|
31
|
+
let size: Int64
|
|
32
|
+
let lastModified: String
|
|
33
|
+
let permissions: String?
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protocol FtpProgressDelegate: AnyObject {
|
|
37
|
+
func onProgress(transferred: Int64, total: Int64)
|
|
38
|
+
func onComplete()
|
|
39
|
+
func onError(error: String)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
enum FtpError: Error, LocalizedError {
|
|
43
|
+
case notConnected
|
|
44
|
+
case connectionTimeout
|
|
45
|
+
case connectionCancelled
|
|
46
|
+
case authenticationFailed(String)
|
|
47
|
+
case commandFailed(String)
|
|
48
|
+
case fileNotFound(String)
|
|
49
|
+
case invalidResponse
|
|
50
|
+
|
|
51
|
+
var errorDescription: String? {
|
|
52
|
+
switch self {
|
|
53
|
+
case .notConnected:
|
|
54
|
+
return "Not connected to FTP server"
|
|
55
|
+
case .connectionTimeout:
|
|
56
|
+
return "Connection timeout"
|
|
57
|
+
case .connectionCancelled:
|
|
58
|
+
return "Connection cancelled"
|
|
59
|
+
case .authenticationFailed(let message):
|
|
60
|
+
return "Authentication failed: \(message)"
|
|
61
|
+
case .commandFailed(let message):
|
|
62
|
+
return "Command failed: \(message)"
|
|
63
|
+
case .fileNotFound(let message):
|
|
64
|
+
return "File not found: \(message)"
|
|
65
|
+
case .invalidResponse:
|
|
66
|
+
return "Invalid response from server"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Helper class to safely manage continuation resumption
|
|
72
|
+
private class ConnectionBox<T>: @unchecked Sendable {
|
|
73
|
+
private let continuation: CheckedContinuation<T, Error>
|
|
74
|
+
private var hasResumed = false
|
|
75
|
+
private let queue = DispatchQueue(label: "ConnectionBox")
|
|
76
|
+
|
|
77
|
+
init(continuation: CheckedContinuation<T, Error>) {
|
|
78
|
+
self.continuation = continuation
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func resume(returning value: T) {
|
|
82
|
+
queue.sync {
|
|
83
|
+
guard !hasResumed else { return }
|
|
84
|
+
hasResumed = true
|
|
85
|
+
continuation.resume(returning: value)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func resume(throwing error: Error) {
|
|
90
|
+
queue.sync {
|
|
91
|
+
guard !hasResumed else { return }
|
|
92
|
+
hasResumed = true
|
|
93
|
+
continuation.resume(throwing: error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func resume() where T == Void {
|
|
98
|
+
queue.sync {
|
|
99
|
+
guard !hasResumed else { return }
|
|
100
|
+
hasResumed = true
|
|
101
|
+
continuation.resume()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Helper class specifically for NWConnection state handling
|
|
107
|
+
private class NWConnectionBox: @unchecked Sendable {
|
|
108
|
+
private let connection: NWConnection
|
|
109
|
+
private let continuation: CheckedContinuation<NWConnection, Error>
|
|
110
|
+
private var hasResumed = false
|
|
111
|
+
private let queue = DispatchQueue(label: "NWConnectionBox")
|
|
112
|
+
|
|
113
|
+
init(connection: NWConnection, continuation: CheckedContinuation<NWConnection, Error>) {
|
|
114
|
+
self.connection = connection
|
|
115
|
+
self.continuation = continuation
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func handleStateUpdate(_ state: NWConnection.State) {
|
|
119
|
+
queue.sync {
|
|
120
|
+
guard !hasResumed else { return }
|
|
121
|
+
|
|
122
|
+
switch state {
|
|
123
|
+
case .ready:
|
|
124
|
+
hasResumed = true
|
|
125
|
+
continuation.resume(returning: connection)
|
|
126
|
+
case .failed(let error):
|
|
127
|
+
hasResumed = true
|
|
128
|
+
continuation.resume(throwing: error)
|
|
129
|
+
case .cancelled:
|
|
130
|
+
hasResumed = true
|
|
131
|
+
continuation.resume(throwing: FtpError.connectionCancelled)
|
|
132
|
+
default:
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Helper class for FTP connection authentication
|
|
140
|
+
private class FtpConnectionBox: @unchecked Sendable {
|
|
141
|
+
private let ftpClient: FtpClient
|
|
142
|
+
private let continuation: CheckedContinuation<Void, Error>
|
|
143
|
+
private var hasResumed = false
|
|
144
|
+
private let queue = DispatchQueue(label: "FtpConnectionBox")
|
|
145
|
+
private var timeoutTask: DispatchWorkItem?
|
|
146
|
+
|
|
147
|
+
init(ftpClient: FtpClient, continuation: CheckedContinuation<Void, Error>) {
|
|
148
|
+
self.ftpClient = ftpClient
|
|
149
|
+
self.continuation = continuation
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func setTimeoutTask(_ task: DispatchWorkItem) {
|
|
153
|
+
self.timeoutTask = task
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func handleStateUpdate(_ state: NWConnection.State) {
|
|
157
|
+
queue.async {
|
|
158
|
+
guard !self.hasResumed else { return }
|
|
159
|
+
|
|
160
|
+
switch state {
|
|
161
|
+
case .ready:
|
|
162
|
+
self.timeoutTask?.cancel()
|
|
163
|
+
Task {
|
|
164
|
+
do {
|
|
165
|
+
try await self.ftpClient.performAuthentication()
|
|
166
|
+
self.ftpClient.isConnected = true
|
|
167
|
+
self.queue.sync {
|
|
168
|
+
if !self.hasResumed {
|
|
169
|
+
self.hasResumed = true
|
|
170
|
+
self.continuation.resume()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
self.queue.sync {
|
|
175
|
+
if !self.hasResumed {
|
|
176
|
+
self.hasResumed = true
|
|
177
|
+
self.continuation.resume(throwing: error)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
case .failed(let error):
|
|
183
|
+
self.timeoutTask?.cancel()
|
|
184
|
+
self.queue.sync {
|
|
185
|
+
if !self.hasResumed {
|
|
186
|
+
self.hasResumed = true
|
|
187
|
+
self.continuation.resume(throwing: error)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
case .cancelled:
|
|
191
|
+
self.timeoutTask?.cancel()
|
|
192
|
+
self.queue.sync {
|
|
193
|
+
if !self.hasResumed {
|
|
194
|
+
self.hasResumed = true
|
|
195
|
+
self.continuation.resume(throwing: FtpError.connectionCancelled)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
default:
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func handleTimeout() {
|
|
205
|
+
queue.sync {
|
|
206
|
+
if !hasResumed {
|
|
207
|
+
hasResumed = true
|
|
208
|
+
continuation.resume(throwing: FtpError.connectionTimeout)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class FtpClient: @unchecked Sendable {
|
|
215
|
+
private var controlConnection: NWConnection?
|
|
216
|
+
var isConnected = false
|
|
217
|
+
private var config: FtpConnectionConfig?
|
|
218
|
+
private weak var progressDelegate: FtpProgressDelegate?
|
|
219
|
+
|
|
220
|
+
func connect(config: FtpConnectionConfig) async throws {
|
|
221
|
+
self.config = config
|
|
222
|
+
|
|
223
|
+
let host = NWEndpoint.Host(config.host)
|
|
224
|
+
let port = NWEndpoint.Port(integerLiteral: UInt16(config.port))
|
|
225
|
+
let endpoint = NWEndpoint.hostPort(host: host, port: port)
|
|
226
|
+
|
|
227
|
+
controlConnection = NWConnection(to: endpoint, using: .tcp)
|
|
228
|
+
|
|
229
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
230
|
+
let connectionBox = FtpConnectionBox(ftpClient: self, continuation: continuation)
|
|
231
|
+
|
|
232
|
+
let timeoutTask = DispatchWorkItem {
|
|
233
|
+
if !self.isConnected {
|
|
234
|
+
self.controlConnection?.cancel()
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
connectionBox.setTimeoutTask(timeoutTask)
|
|
239
|
+
|
|
240
|
+
controlConnection?.stateUpdateHandler = { state in
|
|
241
|
+
connectionBox.handleStateUpdate(state)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
controlConnection?.start(queue: .global())
|
|
245
|
+
|
|
246
|
+
// Set timeout
|
|
247
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + config.timeout, execute: timeoutTask)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func disconnect() async {
|
|
252
|
+
if isConnected {
|
|
253
|
+
do {
|
|
254
|
+
_ = try await sendCommand("QUIT")
|
|
255
|
+
} catch {
|
|
256
|
+
// Ignore errors during disconnect
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
controlConnection?.cancel()
|
|
261
|
+
controlConnection = nil
|
|
262
|
+
isConnected = false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func listFiles(path: String? = nil) async throws -> [FtpFileInfo] {
|
|
266
|
+
guard isConnected else { throw FtpError.notConnected }
|
|
267
|
+
|
|
268
|
+
// Set ASCII mode for directory listings
|
|
269
|
+
print("FTP DEBUG: Setting ASCII mode for directory listing")
|
|
270
|
+
let asciiResponse = try await sendCommand("TYPE A")
|
|
271
|
+
print("FTP DEBUG: ASCII mode response: \(asciiResponse)")
|
|
272
|
+
if !asciiResponse.hasPrefix("200") {
|
|
273
|
+
print("FTP DEBUG: Warning - Failed to set ASCII mode: \(asciiResponse)")
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let dataConnection = try await enterPassiveMode()
|
|
277
|
+
|
|
278
|
+
let command = path != nil ? "LIST \(path!)" : "LIST"
|
|
279
|
+
print("FTP DEBUG: Sending command: \(command)")
|
|
280
|
+
let response = try await sendCommand(command)
|
|
281
|
+
print("FTP DEBUG: LIST command response: \(response)")
|
|
282
|
+
|
|
283
|
+
guard response.hasPrefix("150") || response.hasPrefix("125") else {
|
|
284
|
+
dataConnection.cancel()
|
|
285
|
+
throw FtpError.commandFailed("LIST failed: \(response)")
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let data = try await receiveData(from: dataConnection)
|
|
289
|
+
print("FTP DEBUG: Received data length: \(data.count) bytes")
|
|
290
|
+
if let dataString = String(data: data, encoding: .utf8) {
|
|
291
|
+
print("FTP DEBUG: Received data content: '\(dataString)'")
|
|
292
|
+
}
|
|
293
|
+
dataConnection.cancel()
|
|
294
|
+
|
|
295
|
+
let finalResponse = try await readResponse()
|
|
296
|
+
print("FTP DEBUG: Final response: \(finalResponse)")
|
|
297
|
+
guard finalResponse.hasPrefix("226") else {
|
|
298
|
+
throw FtpError.commandFailed("LIST completion failed: \(finalResponse)")
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let files = parseListingData(data)
|
|
302
|
+
print("FTP DEBUG: Parsed \(files.count) files")
|
|
303
|
+
|
|
304
|
+
// Reset to binary mode for file transfers
|
|
305
|
+
let binaryResponse = try await sendCommand("TYPE I")
|
|
306
|
+
print("FTP DEBUG: Reset to binary mode response: \(binaryResponse)")
|
|
307
|
+
|
|
308
|
+
return files
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func downloadFile(remotePath: String, localPath: String) async throws {
|
|
312
|
+
guard isConnected else { throw FtpError.notConnected }
|
|
313
|
+
|
|
314
|
+
let dataConnection = try await enterPassiveMode()
|
|
315
|
+
|
|
316
|
+
let response = try await sendCommand("RETR \(remotePath)")
|
|
317
|
+
guard response.hasPrefix("150") || response.hasPrefix("125") else {
|
|
318
|
+
dataConnection.cancel()
|
|
319
|
+
throw FtpError.commandFailed("Download failed: \(response)")
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let localURL = URL(fileURLWithPath: localPath)
|
|
323
|
+
|
|
324
|
+
// Create parent directories if needed
|
|
325
|
+
try FileManager.default.createDirectory(at: localURL.deletingLastPathComponent(),
|
|
326
|
+
withIntermediateDirectories: true,
|
|
327
|
+
attributes: nil)
|
|
328
|
+
|
|
329
|
+
let data = try await receiveData(from: dataConnection, reportProgress: true)
|
|
330
|
+
dataConnection.cancel()
|
|
331
|
+
|
|
332
|
+
try data.write(to: localURL)
|
|
333
|
+
|
|
334
|
+
let finalResponse = try await readResponse()
|
|
335
|
+
guard finalResponse.hasPrefix("226") else {
|
|
336
|
+
try? FileManager.default.removeItem(at: localURL)
|
|
337
|
+
throw FtpError.commandFailed("Download completion failed: \(finalResponse)")
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
progressDelegate?.onComplete()
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
func uploadFile(localPath: String, remotePath: String) async throws {
|
|
344
|
+
guard isConnected else { throw FtpError.notConnected }
|
|
345
|
+
|
|
346
|
+
let localURL = URL(fileURLWithPath: localPath)
|
|
347
|
+
guard FileManager.default.fileExists(atPath: localPath) else {
|
|
348
|
+
throw FtpError.fileNotFound("Local file not found: \(localPath)")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let data = try Data(contentsOf: localURL)
|
|
352
|
+
let dataConnection = try await enterPassiveMode()
|
|
353
|
+
|
|
354
|
+
let response = try await sendCommand("STOR \(remotePath)")
|
|
355
|
+
guard response.hasPrefix("150") || response.hasPrefix("125") else {
|
|
356
|
+
dataConnection.cancel()
|
|
357
|
+
throw FtpError.commandFailed("Upload failed: \(response)")
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try await sendData(data, to: dataConnection, reportProgress: true)
|
|
361
|
+
dataConnection.cancel()
|
|
362
|
+
|
|
363
|
+
let finalResponse = try await readResponse()
|
|
364
|
+
guard finalResponse.hasPrefix("226") else {
|
|
365
|
+
throw FtpError.commandFailed("Upload completion failed: \(finalResponse)")
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
progressDelegate?.onComplete()
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
func deleteFile(remotePath: String, isDirectory: Bool = false) async throws {
|
|
372
|
+
guard isConnected else { throw FtpError.notConnected }
|
|
373
|
+
|
|
374
|
+
let command = isDirectory ? "RMD \(remotePath)" : "DELE \(remotePath)"
|
|
375
|
+
let response = try await sendCommand(command)
|
|
376
|
+
|
|
377
|
+
guard response.hasPrefix("250") else {
|
|
378
|
+
throw FtpError.commandFailed("Delete failed: \(response)")
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
func createDirectory(remotePath: String) async throws {
|
|
383
|
+
guard isConnected else { throw FtpError.notConnected }
|
|
384
|
+
|
|
385
|
+
let response = try await sendCommand("MKD \(remotePath)")
|
|
386
|
+
guard response.hasPrefix("257") else {
|
|
387
|
+
throw FtpError.commandFailed("Create directory failed: \(response)")
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
func changeDirectory(remotePath: String) async throws {
|
|
392
|
+
guard isConnected else { throw FtpError.notConnected }
|
|
393
|
+
|
|
394
|
+
let response = try await sendCommand("CWD \(remotePath)")
|
|
395
|
+
guard response.hasPrefix("250") else {
|
|
396
|
+
throw FtpError.commandFailed("Change directory failed: \(response)")
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
func getCurrentDirectory() async throws -> String {
|
|
401
|
+
guard isConnected else { throw FtpError.notConnected }
|
|
402
|
+
|
|
403
|
+
let response = try await sendCommand("PWD")
|
|
404
|
+
guard response.hasPrefix("257") else {
|
|
405
|
+
throw FtpError.commandFailed("Get current directory failed: \(response)")
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Extract directory from response like: 257 "/home/user" is current directory
|
|
409
|
+
let pattern = #""([^"]+)""#
|
|
410
|
+
if let range = response.range(of: pattern, options: .regularExpression) {
|
|
411
|
+
let match = String(response[range])
|
|
412
|
+
return String(match.dropFirst().dropLast())
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
throw FtpError.commandFailed("Failed to parse current directory response: \(response)")
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
func setProgressDelegate(_ delegate: FtpProgressDelegate?) {
|
|
419
|
+
self.progressDelegate = delegate
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// MARK: - Private Methods
|
|
423
|
+
|
|
424
|
+
func performAuthentication() async throws {
|
|
425
|
+
// Read welcome message
|
|
426
|
+
let welcome = try await readResponse()
|
|
427
|
+
guard welcome.hasPrefix("220") else {
|
|
428
|
+
throw FtpError.authenticationFailed("Server not ready: \(welcome)")
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Send username
|
|
432
|
+
let userResponse = try await sendCommand("USER \(config!.username)")
|
|
433
|
+
guard userResponse.hasPrefix("331") || userResponse.hasPrefix("230") else {
|
|
434
|
+
throw FtpError.authenticationFailed("Username rejected: \(userResponse)")
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Send password if needed
|
|
438
|
+
if userResponse.hasPrefix("331") {
|
|
439
|
+
let passResponse = try await sendCommand("PASS \(config!.password)")
|
|
440
|
+
guard passResponse.hasPrefix("230") else {
|
|
441
|
+
throw FtpError.authenticationFailed("Authentication failed: \(passResponse)")
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Set binary mode
|
|
446
|
+
let typeResponse = try await sendCommand("TYPE I")
|
|
447
|
+
guard typeResponse.hasPrefix("200") else {
|
|
448
|
+
throw FtpError.commandFailed("Failed to set binary mode: \(typeResponse)")
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private func sendCommand(_ command: String) async throws -> String {
|
|
453
|
+
guard let connection = controlConnection else {
|
|
454
|
+
throw FtpError.notConnected
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let commandData = "\(command)\r\n".data(using: .utf8)!
|
|
458
|
+
|
|
459
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
460
|
+
let connectionBox = ConnectionBox<String>(continuation: continuation)
|
|
461
|
+
|
|
462
|
+
connection.send(content: commandData, completion: .contentProcessed { error in
|
|
463
|
+
if let error = error {
|
|
464
|
+
connectionBox.resume(throwing: error)
|
|
465
|
+
} else {
|
|
466
|
+
Task {
|
|
467
|
+
do {
|
|
468
|
+
let response = try await self.readResponse()
|
|
469
|
+
connectionBox.resume(returning: response)
|
|
470
|
+
} catch {
|
|
471
|
+
connectionBox.resume(throwing: error)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private func readResponse() async throws -> String {
|
|
480
|
+
guard let connection = controlConnection else {
|
|
481
|
+
throw FtpError.notConnected
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
485
|
+
let connectionBox = ConnectionBox<String>(continuation: continuation)
|
|
486
|
+
|
|
487
|
+
connection.receive(minimumIncompleteLength: 1, maximumLength: 4096) { data, _, isComplete, error in
|
|
488
|
+
if let error = error {
|
|
489
|
+
connectionBox.resume(throwing: error)
|
|
490
|
+
} else if let data = data {
|
|
491
|
+
// Try multiple encodings for FTP responses
|
|
492
|
+
var response: String?
|
|
493
|
+
|
|
494
|
+
// Try UTF-8 first
|
|
495
|
+
if let utf8Response = String(data: data, encoding: .utf8) {
|
|
496
|
+
response = utf8Response
|
|
497
|
+
}
|
|
498
|
+
// Try GBK for Chinese FTP servers
|
|
499
|
+
else if let gbkResponse = String(data: data, encoding: String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))) {
|
|
500
|
+
response = gbkResponse
|
|
501
|
+
}
|
|
502
|
+
// Try ASCII as fallback
|
|
503
|
+
else if let asciiResponse = String(data: data, encoding: .ascii) {
|
|
504
|
+
response = asciiResponse
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if let validResponse = response {
|
|
508
|
+
connectionBox.resume(returning: validResponse.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
509
|
+
} else {
|
|
510
|
+
connectionBox.resume(throwing: FtpError.invalidResponse)
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
connectionBox.resume(throwing: FtpError.invalidResponse)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private func enterPassiveMode() async throws -> NWConnection {
|
|
520
|
+
let response = try await sendCommand("PASV")
|
|
521
|
+
guard response.hasPrefix("227") else {
|
|
522
|
+
throw FtpError.commandFailed("Failed to enter passive mode: \(response)")
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Parse passive mode response: 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
|
|
526
|
+
let pattern = #"\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)"#
|
|
527
|
+
let regex = try NSRegularExpression(pattern: pattern)
|
|
528
|
+
let nsString = response as NSString
|
|
529
|
+
|
|
530
|
+
guard let match = regex.firstMatch(in: response, range: NSRange(location: 0, length: nsString.length)) else {
|
|
531
|
+
throw FtpError.commandFailed("Failed to parse passive mode response: \(response)")
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
let h1 = Int(nsString.substring(with: match.range(at: 1)))!
|
|
535
|
+
let h2 = Int(nsString.substring(with: match.range(at: 2)))!
|
|
536
|
+
let h3 = Int(nsString.substring(with: match.range(at: 3)))!
|
|
537
|
+
let h4 = Int(nsString.substring(with: match.range(at: 4)))!
|
|
538
|
+
let p1 = Int(nsString.substring(with: match.range(at: 5)))!
|
|
539
|
+
let p2 = Int(nsString.substring(with: match.range(at: 6)))!
|
|
540
|
+
|
|
541
|
+
let host = "\(h1).\(h2).\(h3).\(h4)"
|
|
542
|
+
let port = p1 * 256 + p2
|
|
543
|
+
|
|
544
|
+
let dataHost = NWEndpoint.Host(host)
|
|
545
|
+
let dataPort = NWEndpoint.Port(integerLiteral: UInt16(port))
|
|
546
|
+
let dataEndpoint = NWEndpoint.hostPort(host: dataHost, port: dataPort)
|
|
547
|
+
|
|
548
|
+
let dataConnection = NWConnection(to: dataEndpoint, using: .tcp)
|
|
549
|
+
|
|
550
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
551
|
+
let connectionBox = NWConnectionBox(connection: dataConnection, continuation: continuation)
|
|
552
|
+
|
|
553
|
+
dataConnection.stateUpdateHandler = { state in
|
|
554
|
+
connectionBox.handleStateUpdate(state)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
dataConnection.start(queue: .global())
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private func receiveData(from connection: NWConnection, reportProgress: Bool = false) async throws -> Data {
|
|
562
|
+
var receivedData = Data()
|
|
563
|
+
|
|
564
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
565
|
+
let connectionBox = ConnectionBox<Data>(continuation: continuation)
|
|
566
|
+
|
|
567
|
+
func receiveMore() {
|
|
568
|
+
connection.receive(minimumIncompleteLength: 1, maximumLength: 8192) { data, _, isComplete, error in
|
|
569
|
+
if let error = error {
|
|
570
|
+
connectionBox.resume(throwing: error)
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if let data = data {
|
|
575
|
+
receivedData.append(data)
|
|
576
|
+
if reportProgress {
|
|
577
|
+
DispatchQueue.main.async {
|
|
578
|
+
self.progressDelegate?.onProgress(transferred: Int64(receivedData.count), total: -1)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if isComplete {
|
|
584
|
+
connectionBox.resume(returning: receivedData)
|
|
585
|
+
} else {
|
|
586
|
+
receiveMore()
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
receiveMore()
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private func sendData(_ data: Data, to connection: NWConnection, reportProgress: Bool = false) async throws {
|
|
595
|
+
let chunkSize = 8192
|
|
596
|
+
let totalSize = data.count
|
|
597
|
+
var sentBytes = 0
|
|
598
|
+
|
|
599
|
+
for i in stride(from: 0, to: totalSize, by: chunkSize) {
|
|
600
|
+
let endIndex = min(i + chunkSize, totalSize)
|
|
601
|
+
let chunk = data.subdata(in: i..<endIndex)
|
|
602
|
+
|
|
603
|
+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
604
|
+
let connectionBox = ConnectionBox<Void>(continuation: continuation)
|
|
605
|
+
|
|
606
|
+
connection.send(content: chunk, completion: .contentProcessed { error in
|
|
607
|
+
if let error = error {
|
|
608
|
+
connectionBox.resume(throwing: error)
|
|
609
|
+
} else {
|
|
610
|
+
sentBytes += chunk.count
|
|
611
|
+
if reportProgress {
|
|
612
|
+
DispatchQueue.main.async {
|
|
613
|
+
self.progressDelegate?.onProgress(transferred: Int64(sentBytes), total: Int64(totalSize))
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
connectionBox.resume()
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private func parseListingData(_ data: Data) -> [FtpFileInfo] {
|
|
624
|
+
print("FTP DEBUG: Raw data length: \(data.count) bytes")
|
|
625
|
+
print("FTP DEBUG: Raw data hex: \(data.map { String(format: "%02x", $0) }.joined())")
|
|
626
|
+
|
|
627
|
+
// Try different encodings, prioritizing Chinese encodings
|
|
628
|
+
var string: String?
|
|
629
|
+
var encoding: String.Encoding = .utf8
|
|
630
|
+
|
|
631
|
+
// Try UTF-8 first
|
|
632
|
+
if let utf8String = String(data: data, encoding: .utf8) {
|
|
633
|
+
string = utf8String
|
|
634
|
+
encoding = .utf8
|
|
635
|
+
print("FTP DEBUG: Successfully decoded with UTF-8")
|
|
636
|
+
}
|
|
637
|
+
// Try GBK/GB2312 for Chinese
|
|
638
|
+
else if let gbkString = String(data: data, encoding: String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))) {
|
|
639
|
+
string = gbkString
|
|
640
|
+
encoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))
|
|
641
|
+
print("FTP DEBUG: Successfully decoded with GBK/GB18030")
|
|
642
|
+
}
|
|
643
|
+
// Try Big5 for Traditional Chinese
|
|
644
|
+
else if let big5String = String(data: data, encoding: String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.big5.rawValue)))) {
|
|
645
|
+
string = big5String
|
|
646
|
+
encoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.big5.rawValue)))
|
|
647
|
+
print("FTP DEBUG: Successfully decoded with Big5")
|
|
648
|
+
}
|
|
649
|
+
// Try ASCII
|
|
650
|
+
else if let asciiString = String(data: data, encoding: .ascii) {
|
|
651
|
+
string = asciiString
|
|
652
|
+
encoding = .ascii
|
|
653
|
+
print("FTP DEBUG: Successfully decoded with ASCII")
|
|
654
|
+
}
|
|
655
|
+
// Try Latin1 (ISO-8859-1)
|
|
656
|
+
else if let latin1String = String(data: data, encoding: .isoLatin1) {
|
|
657
|
+
string = latin1String
|
|
658
|
+
encoding = .isoLatin1
|
|
659
|
+
print("FTP DEBUG: Successfully decoded with Latin1")
|
|
660
|
+
}
|
|
661
|
+
// Try Windows-1252
|
|
662
|
+
else if let windowsString = String(data: data, encoding: .windowsCP1252) {
|
|
663
|
+
string = windowsString
|
|
664
|
+
encoding = .windowsCP1252
|
|
665
|
+
print("FTP DEBUG: Successfully decoded with Windows-1252")
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
print("FTP DEBUG: Failed to decode data with any encoding")
|
|
669
|
+
print("FTP DEBUG: Raw bytes: \(Array(data))")
|
|
670
|
+
return []
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
guard let decodedString = string else {
|
|
674
|
+
print("FTP DEBUG: No valid string found")
|
|
675
|
+
return []
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
print("FTP DEBUG: Decoded string with \(encoding): '\(decodedString)'")
|
|
679
|
+
print("FTP DEBUG: String length: \(decodedString.count) characters")
|
|
680
|
+
|
|
681
|
+
let lines = decodedString.components(separatedBy: .newlines)
|
|
682
|
+
print("FTP DEBUG: Found \(lines.count) lines")
|
|
683
|
+
var files: [FtpFileInfo] = []
|
|
684
|
+
|
|
685
|
+
for (index, line) in lines.enumerated() {
|
|
686
|
+
print("FTP DEBUG: Processing line \(index): '\(line)'")
|
|
687
|
+
if let fileInfo = parseListing(line) {
|
|
688
|
+
files.append(fileInfo)
|
|
689
|
+
print("FTP DEBUG: Parsed file: \(fileInfo.name), isDirectory: \(fileInfo.isDirectory)")
|
|
690
|
+
} else {
|
|
691
|
+
print("FTP DEBUG: Failed to parse line \(index)")
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
print("FTP DEBUG: Successfully parsed \(files.count) files")
|
|
696
|
+
return files
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private func parseListing(_ line: String) -> FtpFileInfo? {
|
|
700
|
+
let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
701
|
+
guard !trimmedLine.isEmpty else {
|
|
702
|
+
print("FTP DEBUG: Line is empty after trimming")
|
|
703
|
+
return nil
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
print("FTP DEBUG: Parsing line: '\(trimmedLine)'")
|
|
707
|
+
|
|
708
|
+
// Parse Unix-style listing: drwxr-xr-x 3 user group 4096 Mar 15 10:30 dirname
|
|
709
|
+
let components = trimmedLine.components(separatedBy: .whitespaces).filter { !$0.isEmpty }
|
|
710
|
+
print("FTP DEBUG: Components count: \(components.count), components: \(components)")
|
|
711
|
+
|
|
712
|
+
guard components.count >= 9 else {
|
|
713
|
+
print("FTP DEBUG: Not enough components (\(components.count)), expected at least 9")
|
|
714
|
+
return nil
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let permissions = components[0]
|
|
718
|
+
let isDirectory = permissions.hasPrefix("d")
|
|
719
|
+
let size = isDirectory ? 0 : Int64(components[4]) ?? 0
|
|
720
|
+
|
|
721
|
+
// Reconstruct filename (can contain spaces)
|
|
722
|
+
let name = components[8...].joined(separator: " ")
|
|
723
|
+
|
|
724
|
+
// Parse date
|
|
725
|
+
let month = components[5]
|
|
726
|
+
let day = components[6]
|
|
727
|
+
let yearOrTime = components[7]
|
|
728
|
+
let lastModified = "\(month) \(day) \(yearOrTime)"
|
|
729
|
+
|
|
730
|
+
let fileInfo = FtpFileInfo(
|
|
731
|
+
name: name,
|
|
732
|
+
isDirectory: isDirectory,
|
|
733
|
+
size: size,
|
|
734
|
+
lastModified: lastModified,
|
|
735
|
+
permissions: permissions
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
print("FTP DEBUG: Successfully parsed file: \(fileInfo)")
|
|
739
|
+
return fileInfo
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// MARK: - Main Module
|
|
5
744
|
|
|
6
745
|
public class ReactNativeKookitModule: Module {
|
|
7
746
|
private var volumeView: MPVolumeView?
|
|
8
747
|
private var volumeObserver: NSKeyValueObservation?
|
|
9
748
|
private var isVolumeKeyInterceptionEnabled = false
|
|
10
749
|
private var previousVolume: Float = 0.0
|
|
750
|
+
private var ftpClient: FtpClient?
|
|
751
|
+
|
|
11
752
|
// Each module class must implement the definition function. The definition consists of components
|
|
12
753
|
// that describes the module's functionality and behavior.
|
|
13
754
|
// See https://docs.expo.dev/modules/module-api for more details about available components.
|
|
@@ -23,7 +764,7 @@ public class ReactNativeKookitModule: Module {
|
|
|
23
764
|
])
|
|
24
765
|
|
|
25
766
|
// Defines event names that the module can send to JavaScript.
|
|
26
|
-
Events("onChange", "onVolumeButtonPressed")
|
|
767
|
+
Events("onChange", "onVolumeButtonPressed", "onFtpProgress", "onFtpComplete", "onFtpError")
|
|
27
768
|
|
|
28
769
|
// Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
|
|
29
770
|
Function("hello") {
|
|
@@ -49,8 +790,127 @@ public class ReactNativeKookitModule: Module {
|
|
|
49
790
|
self.disableVolumeKeyInterception()
|
|
50
791
|
}
|
|
51
792
|
|
|
52
|
-
//
|
|
53
|
-
|
|
793
|
+
// FTP Functions
|
|
794
|
+
AsyncFunction("ftpConnect") { (config: [String: Any], promise: Promise) in
|
|
795
|
+
Task {
|
|
796
|
+
do {
|
|
797
|
+
let ftpConfig = FtpConnectionConfig(
|
|
798
|
+
host: config["host"] as! String,
|
|
799
|
+
port: config["port"] as? Int ?? 21,
|
|
800
|
+
username: config["username"] as! String,
|
|
801
|
+
password: config["password"] as! String,
|
|
802
|
+
passive: config["passive"] as? Bool ?? true,
|
|
803
|
+
timeout: config["timeout"] as? TimeInterval ?? 30.0
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
self.ftpClient = FtpClient()
|
|
807
|
+
let progressDelegate = FtpProgressDelegateImpl(module: self)
|
|
808
|
+
self.ftpClient?.setProgressDelegate(progressDelegate)
|
|
809
|
+
|
|
810
|
+
try await self.ftpClient?.connect(config: ftpConfig)
|
|
811
|
+
promise.resolve()
|
|
812
|
+
} catch {
|
|
813
|
+
promise.reject("FTP_CONNECT_ERROR", error.localizedDescription)
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
AsyncFunction("ftpDisconnect") { (promise: Promise) in
|
|
819
|
+
Task {
|
|
820
|
+
await self.ftpClient?.disconnect()
|
|
821
|
+
self.ftpClient = nil
|
|
822
|
+
promise.resolve()
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
AsyncFunction("ftpList") { (path: String?, promise: Promise) in
|
|
827
|
+
Task {
|
|
828
|
+
do {
|
|
829
|
+
let files = try await self.ftpClient?.listFiles(path: path) ?? []
|
|
830
|
+
let result = files.map { file in
|
|
831
|
+
[
|
|
832
|
+
"name": file.name,
|
|
833
|
+
"isDirectory": file.isDirectory,
|
|
834
|
+
"size": file.size,
|
|
835
|
+
"lastModified": file.lastModified,
|
|
836
|
+
"permissions": file.permissions as Any
|
|
837
|
+
]
|
|
838
|
+
}
|
|
839
|
+
promise.resolve(result)
|
|
840
|
+
} catch {
|
|
841
|
+
promise.reject("FTP_LIST_ERROR", error.localizedDescription)
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
AsyncFunction("ftpDownload") { (remotePath: String, localPath: String, promise: Promise) in
|
|
847
|
+
Task {
|
|
848
|
+
do {
|
|
849
|
+
try await self.ftpClient?.downloadFile(remotePath: remotePath, localPath: localPath)
|
|
850
|
+
promise.resolve()
|
|
851
|
+
} catch {
|
|
852
|
+
promise.reject("FTP_DOWNLOAD_ERROR", error.localizedDescription)
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
AsyncFunction("ftpUpload") { (localPath: String, remotePath: String, promise: Promise) in
|
|
858
|
+
Task {
|
|
859
|
+
do {
|
|
860
|
+
try await self.ftpClient?.uploadFile(localPath: localPath, remotePath: remotePath)
|
|
861
|
+
promise.resolve()
|
|
862
|
+
} catch {
|
|
863
|
+
promise.reject("FTP_UPLOAD_ERROR", error.localizedDescription)
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
AsyncFunction("ftpDelete") { (remotePath: String, isDirectory: Bool?, promise: Promise) in
|
|
869
|
+
Task {
|
|
870
|
+
do {
|
|
871
|
+
try await self.ftpClient?.deleteFile(remotePath: remotePath, isDirectory: isDirectory ?? false)
|
|
872
|
+
promise.resolve()
|
|
873
|
+
} catch {
|
|
874
|
+
promise.reject("FTP_DELETE_ERROR", error.localizedDescription)
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
AsyncFunction("ftpCreateDirectory") { (remotePath: String, promise: Promise) in
|
|
880
|
+
Task {
|
|
881
|
+
do {
|
|
882
|
+
try await self.ftpClient?.createDirectory(remotePath: remotePath)
|
|
883
|
+
promise.resolve()
|
|
884
|
+
} catch {
|
|
885
|
+
promise.reject("FTP_CREATE_DIR_ERROR", error.localizedDescription)
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
AsyncFunction("ftpChangeDirectory") { (remotePath: String, promise: Promise) in
|
|
891
|
+
Task {
|
|
892
|
+
do {
|
|
893
|
+
try await self.ftpClient?.changeDirectory(remotePath: remotePath)
|
|
894
|
+
promise.resolve()
|
|
895
|
+
} catch {
|
|
896
|
+
promise.reject("FTP_CHANGE_DIR_ERROR", error.localizedDescription)
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
AsyncFunction("ftpGetCurrentDirectory") { (promise: Promise) in
|
|
902
|
+
Task {
|
|
903
|
+
do {
|
|
904
|
+
let currentDir = try await self.ftpClient?.getCurrentDirectory() ?? "/"
|
|
905
|
+
promise.resolve(currentDir)
|
|
906
|
+
} catch {
|
|
907
|
+
promise.reject("FTP_PWD_ERROR", error.localizedDescription)
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Enables the module to be used as a native view. Definition components that are accepted as part of
|
|
913
|
+
// the view definition: Prop, Events.
|
|
54
914
|
View(ReactNativeKookitView.self) {
|
|
55
915
|
// Defines a setter for the `url` prop.
|
|
56
916
|
Prop("url") { (view: ReactNativeKookitView, url: URL) in
|
|
@@ -142,4 +1002,30 @@ public class ReactNativeKookitModule: Module {
|
|
|
142
1002
|
}
|
|
143
1003
|
}
|
|
144
1004
|
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Helper class to handle FTP progress callbacks
|
|
1008
|
+
private class FtpProgressDelegateImpl: FtpProgressDelegate {
|
|
1009
|
+
weak var module: ReactNativeKookitModule?
|
|
1010
|
+
|
|
1011
|
+
init(module: ReactNativeKookitModule) {
|
|
1012
|
+
self.module = module
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
func onProgress(transferred: Int64, total: Int64) {
|
|
1016
|
+
let percentage = total > 0 ? Int((transferred * 100) / total) : 0
|
|
1017
|
+
module?.sendEvent("onFtpProgress", [
|
|
1018
|
+
"transferred": transferred,
|
|
1019
|
+
"total": total,
|
|
1020
|
+
"percentage": percentage
|
|
1021
|
+
])
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
func onComplete() {
|
|
1025
|
+
module?.sendEvent("onFtpComplete")
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
func onError(error: String) {
|
|
1029
|
+
module?.sendEvent("onFtpError", ["error": error])
|
|
1030
|
+
}
|
|
145
1031
|
}
|