react-native-kookit 0.2.2 → 0.2.4
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/ios/ReactNativeKookitModule.swift +386 -82
- package/package.json +1 -1
|
@@ -68,9 +68,152 @@ enum FtpError: Error, LocalizedError {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
class
|
|
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 {
|
|
72
215
|
private var controlConnection: NWConnection?
|
|
73
|
-
|
|
216
|
+
var isConnected = false
|
|
74
217
|
private var config: FtpConnectionConfig?
|
|
75
218
|
private weak var progressDelegate: FtpProgressDelegate?
|
|
76
219
|
|
|
@@ -84,36 +227,24 @@ class FtpClient {
|
|
|
84
227
|
controlConnection = NWConnection(to: endpoint, using: .tcp)
|
|
85
228
|
|
|
86
229
|
return try await withCheckedThrowingContinuation { continuation in
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
try await self.performAuthentication()
|
|
93
|
-
self.isConnected = true
|
|
94
|
-
continuation.resume()
|
|
95
|
-
} catch {
|
|
96
|
-
continuation.resume(throwing: error)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
case .failed(let error):
|
|
100
|
-
continuation.resume(throwing: error)
|
|
101
|
-
case .cancelled:
|
|
102
|
-
continuation.resume(throwing: FtpError.connectionCancelled)
|
|
103
|
-
default:
|
|
104
|
-
break
|
|
230
|
+
let connectionBox = FtpConnectionBox(ftpClient: self, continuation: continuation)
|
|
231
|
+
|
|
232
|
+
let timeoutTask = DispatchWorkItem {
|
|
233
|
+
if !self.isConnected {
|
|
234
|
+
self.controlConnection?.cancel()
|
|
105
235
|
}
|
|
106
236
|
}
|
|
107
237
|
|
|
238
|
+
connectionBox.setTimeoutTask(timeoutTask)
|
|
239
|
+
|
|
240
|
+
controlConnection?.stateUpdateHandler = { state in
|
|
241
|
+
connectionBox.handleStateUpdate(state)
|
|
242
|
+
}
|
|
243
|
+
|
|
108
244
|
controlConnection?.start(queue: .global())
|
|
109
245
|
|
|
110
246
|
// Set timeout
|
|
111
|
-
DispatchQueue.global().asyncAfter(deadline: .now() + config.timeout)
|
|
112
|
-
if !self.isConnected {
|
|
113
|
-
self.controlConnection?.cancel()
|
|
114
|
-
continuation.resume(throwing: FtpError.connectionTimeout)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
247
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + config.timeout, execute: timeoutTask)
|
|
117
248
|
}
|
|
118
249
|
}
|
|
119
250
|
|
|
@@ -133,57 +264,100 @@ class FtpClient {
|
|
|
133
264
|
|
|
134
265
|
func listFiles(path: String? = nil) async throws -> [FtpFileInfo] {
|
|
135
266
|
guard isConnected else { throw FtpError.notConnected }
|
|
136
|
-
|
|
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
|
+
|
|
137
276
|
let dataConnection = try await enterPassiveMode()
|
|
138
|
-
|
|
277
|
+
|
|
139
278
|
let command = path != nil ? "LIST \(path!)" : "LIST"
|
|
279
|
+
print("FTP DEBUG: Sending command: \(command)")
|
|
140
280
|
let response = try await sendCommand(command)
|
|
141
|
-
|
|
281
|
+
print("FTP DEBUG: LIST command response: \(response)")
|
|
282
|
+
|
|
142
283
|
guard response.hasPrefix("150") || response.hasPrefix("125") else {
|
|
143
284
|
dataConnection.cancel()
|
|
144
285
|
throw FtpError.commandFailed("LIST failed: \(response)")
|
|
145
286
|
}
|
|
146
|
-
|
|
287
|
+
|
|
147
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
|
+
}
|
|
148
293
|
dataConnection.cancel()
|
|
149
|
-
|
|
294
|
+
|
|
150
295
|
let finalResponse = try await readResponse()
|
|
296
|
+
print("FTP DEBUG: Final response: \(finalResponse)")
|
|
151
297
|
guard finalResponse.hasPrefix("226") else {
|
|
152
298
|
throw FtpError.commandFailed("LIST completion failed: \(finalResponse)")
|
|
153
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)")
|
|
154
307
|
|
|
155
|
-
return
|
|
308
|
+
return files
|
|
156
309
|
}
|
|
157
310
|
|
|
158
311
|
func downloadFile(remotePath: String, localPath: String) async throws {
|
|
159
312
|
guard isConnected else { throw FtpError.notConnected }
|
|
160
313
|
|
|
314
|
+
print("FTP DEBUG: Starting download - Remote: \(remotePath), Local: \(localPath)")
|
|
315
|
+
|
|
161
316
|
let dataConnection = try await enterPassiveMode()
|
|
317
|
+
print("FTP DEBUG: Passive mode established for download")
|
|
162
318
|
|
|
163
319
|
let response = try await sendCommand("RETR \(remotePath)")
|
|
164
|
-
|
|
320
|
+
print("FTP DEBUG: RETR command response: \(response)")
|
|
321
|
+
|
|
322
|
+
// Extract the first line for status check
|
|
323
|
+
let firstLine = response.components(separatedBy: .newlines).first ?? response
|
|
324
|
+
guard firstLine.hasPrefix("150") || firstLine.hasPrefix("125") else {
|
|
165
325
|
dataConnection.cancel()
|
|
166
|
-
throw FtpError.commandFailed("Download failed: \(
|
|
326
|
+
throw FtpError.commandFailed("Download failed: \(firstLine)")
|
|
167
327
|
}
|
|
168
328
|
|
|
169
329
|
let localURL = URL(fileURLWithPath: localPath)
|
|
170
330
|
|
|
171
331
|
// Create parent directories if needed
|
|
332
|
+
print("FTP DEBUG: Creating parent directories for: \(localURL.deletingLastPathComponent().path)")
|
|
172
333
|
try FileManager.default.createDirectory(at: localURL.deletingLastPathComponent(),
|
|
173
334
|
withIntermediateDirectories: true,
|
|
174
335
|
attributes: nil)
|
|
175
336
|
|
|
337
|
+
print("FTP DEBUG: Starting data reception...")
|
|
176
338
|
let data = try await receiveData(from: dataConnection, reportProgress: true)
|
|
339
|
+
print("FTP DEBUG: Data reception completed, received \(data.count) bytes")
|
|
177
340
|
dataConnection.cancel()
|
|
178
341
|
|
|
342
|
+
print("FTP DEBUG: Writing data to file...")
|
|
179
343
|
try data.write(to: localURL)
|
|
344
|
+
print("FTP DEBUG: File written successfully")
|
|
180
345
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
346
|
+
// Check if the response already contained the completion message (226)
|
|
347
|
+
if response.contains("226") {
|
|
348
|
+
print("FTP DEBUG: Transfer completion already received in RETR response - skipping final response read")
|
|
349
|
+
} else {
|
|
350
|
+
print("FTP DEBUG: Reading final response...")
|
|
351
|
+
let finalResponse = try await readResponse()
|
|
352
|
+
let finalFirstLine = finalResponse.components(separatedBy: .newlines).first ?? finalResponse
|
|
353
|
+
print("FTP DEBUG: Final download response: \(finalFirstLine)")
|
|
354
|
+
guard finalFirstLine.hasPrefix("226") else {
|
|
355
|
+
try? FileManager.default.removeItem(at: localURL)
|
|
356
|
+
throw FtpError.commandFailed("Download completion failed: \(finalFirstLine)")
|
|
357
|
+
}
|
|
185
358
|
}
|
|
186
359
|
|
|
360
|
+
print("FTP DEBUG: Download completed successfully")
|
|
187
361
|
progressDelegate?.onComplete()
|
|
188
362
|
}
|
|
189
363
|
|
|
@@ -268,7 +442,7 @@ class FtpClient {
|
|
|
268
442
|
|
|
269
443
|
// MARK: - Private Methods
|
|
270
444
|
|
|
271
|
-
|
|
445
|
+
func performAuthentication() async throws {
|
|
272
446
|
// Read welcome message
|
|
273
447
|
let welcome = try await readResponse()
|
|
274
448
|
guard welcome.hasPrefix("220") else {
|
|
@@ -301,19 +475,33 @@ class FtpClient {
|
|
|
301
475
|
throw FtpError.notConnected
|
|
302
476
|
}
|
|
303
477
|
|
|
478
|
+
print("FTP DEBUG: Sending command: \(command)")
|
|
304
479
|
let commandData = "\(command)\r\n".data(using: .utf8)!
|
|
305
480
|
|
|
306
481
|
return try await withCheckedThrowingContinuation { continuation in
|
|
482
|
+
let connectionBox = ConnectionBox<String>(continuation: continuation)
|
|
483
|
+
|
|
307
484
|
connection.send(content: commandData, completion: .contentProcessed { error in
|
|
308
485
|
if let error = error {
|
|
309
|
-
|
|
486
|
+
print("FTP DEBUG: Send command error: \(error)")
|
|
487
|
+
connectionBox.resume(throwing: error)
|
|
310
488
|
} else {
|
|
489
|
+
print("FTP DEBUG: Command sent successfully, waiting for response...")
|
|
311
490
|
Task {
|
|
312
491
|
do {
|
|
313
492
|
let response = try await self.readResponse()
|
|
314
|
-
|
|
493
|
+
print("FTP DEBUG: Command response received: \(response)")
|
|
494
|
+
|
|
495
|
+
// Store the full response for multi-line responses
|
|
496
|
+
if response.contains("\n") {
|
|
497
|
+
print("FTP DEBUG: Multi-line response detected")
|
|
498
|
+
connectionBox.resume(returning: response)
|
|
499
|
+
} else {
|
|
500
|
+
connectionBox.resume(returning: response)
|
|
501
|
+
}
|
|
315
502
|
} catch {
|
|
316
|
-
|
|
503
|
+
print("FTP DEBUG: Error reading response: \(error)")
|
|
504
|
+
connectionBox.resume(throwing: error)
|
|
317
505
|
}
|
|
318
506
|
}
|
|
319
507
|
}
|
|
@@ -326,21 +514,57 @@ class FtpClient {
|
|
|
326
514
|
throw FtpError.notConnected
|
|
327
515
|
}
|
|
328
516
|
|
|
517
|
+
print("FTP DEBUG: Reading response...")
|
|
329
518
|
return try await withCheckedThrowingContinuation { continuation in
|
|
519
|
+
let connectionBox = ConnectionBox<String>(continuation: continuation)
|
|
520
|
+
|
|
330
521
|
connection.receive(minimumIncompleteLength: 1, maximumLength: 4096) { data, _, isComplete, error in
|
|
331
522
|
if let error = error {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
523
|
+
print("FTP DEBUG: Read response error: \(error)")
|
|
524
|
+
connectionBox.resume(throwing: error)
|
|
525
|
+
} else if let data = data {
|
|
526
|
+
print("FTP DEBUG: Received response data: \(data.count) bytes")
|
|
527
|
+
// Try multiple encodings for FTP responses
|
|
528
|
+
var response: String?
|
|
529
|
+
|
|
530
|
+
// Try UTF-8 first
|
|
531
|
+
if let utf8Response = String(data: data, encoding: .utf8) {
|
|
532
|
+
response = utf8Response
|
|
533
|
+
print("FTP DEBUG: Response decoded with UTF-8")
|
|
534
|
+
}
|
|
535
|
+
// Try GBK for Chinese FTP servers
|
|
536
|
+
else if let gbkResponse = String(data: data, encoding: String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))) {
|
|
537
|
+
response = gbkResponse
|
|
538
|
+
print("FTP DEBUG: Response decoded with GBK")
|
|
539
|
+
}
|
|
540
|
+
// Try ASCII as fallback
|
|
541
|
+
else if let asciiResponse = String(data: data, encoding: .ascii) {
|
|
542
|
+
response = asciiResponse
|
|
543
|
+
print("FTP DEBUG: Response decoded with ASCII")
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if let validResponse = response {
|
|
547
|
+
let trimmedResponse = validResponse.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
548
|
+
print("FTP DEBUG: Response content: '\(trimmedResponse)'")
|
|
549
|
+
|
|
550
|
+
// Return the full response for multi-line processing
|
|
551
|
+
connectionBox.resume(returning: trimmedResponse)
|
|
552
|
+
} else {
|
|
553
|
+
print("FTP DEBUG: Failed to decode response")
|
|
554
|
+
connectionBox.resume(throwing: FtpError.invalidResponse)
|
|
555
|
+
}
|
|
335
556
|
} else {
|
|
336
|
-
|
|
557
|
+
print("FTP DEBUG: No data received")
|
|
558
|
+
connectionBox.resume(throwing: FtpError.invalidResponse)
|
|
337
559
|
}
|
|
338
560
|
}
|
|
339
561
|
}
|
|
340
562
|
}
|
|
341
563
|
|
|
342
564
|
private func enterPassiveMode() async throws -> NWConnection {
|
|
565
|
+
print("FTP DEBUG: Entering passive mode...")
|
|
343
566
|
let response = try await sendCommand("PASV")
|
|
567
|
+
print("FTP DEBUG: PASV response: \(response)")
|
|
344
568
|
guard response.hasPrefix("227") else {
|
|
345
569
|
throw FtpError.commandFailed("Failed to enter passive mode: \(response)")
|
|
346
570
|
}
|
|
@@ -364,6 +588,8 @@ class FtpClient {
|
|
|
364
588
|
let host = "\(h1).\(h2).\(h3).\(h4)"
|
|
365
589
|
let port = p1 * 256 + p2
|
|
366
590
|
|
|
591
|
+
print("FTP DEBUG: Parsed passive mode - Host: \(host), Port: \(port)")
|
|
592
|
+
|
|
367
593
|
let dataHost = NWEndpoint.Host(host)
|
|
368
594
|
let dataPort = NWEndpoint.Port(integerLiteral: UInt16(port))
|
|
369
595
|
let dataEndpoint = NWEndpoint.hostPort(host: dataHost, port: dataPort)
|
|
@@ -371,44 +597,48 @@ class FtpClient {
|
|
|
371
597
|
let dataConnection = NWConnection(to: dataEndpoint, using: .tcp)
|
|
372
598
|
|
|
373
599
|
return try await withCheckedThrowingContinuation { continuation in
|
|
600
|
+
let connectionBox = NWConnectionBox(connection: dataConnection, continuation: continuation)
|
|
601
|
+
|
|
374
602
|
dataConnection.stateUpdateHandler = { state in
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
continuation.resume(returning: dataConnection)
|
|
378
|
-
case .failed(let error):
|
|
379
|
-
continuation.resume(throwing: error)
|
|
380
|
-
case .cancelled:
|
|
381
|
-
continuation.resume(throwing: FtpError.connectionCancelled)
|
|
382
|
-
default:
|
|
383
|
-
break
|
|
384
|
-
}
|
|
603
|
+
print("FTP DEBUG: Data connection state: \(state)")
|
|
604
|
+
connectionBox.handleStateUpdate(state)
|
|
385
605
|
}
|
|
386
606
|
|
|
607
|
+
print("FTP DEBUG: Starting data connection...")
|
|
387
608
|
dataConnection.start(queue: .global())
|
|
388
609
|
}
|
|
389
610
|
}
|
|
390
611
|
|
|
391
612
|
private func receiveData(from connection: NWConnection, reportProgress: Bool = false) async throws -> Data {
|
|
392
613
|
var receivedData = Data()
|
|
614
|
+
print("FTP DEBUG: Starting data reception, reportProgress: \(reportProgress)")
|
|
393
615
|
|
|
394
616
|
return try await withCheckedThrowingContinuation { continuation in
|
|
617
|
+
let connectionBox = ConnectionBox<Data>(continuation: continuation)
|
|
618
|
+
|
|
395
619
|
func receiveMore() {
|
|
396
620
|
connection.receive(minimumIncompleteLength: 1, maximumLength: 8192) { data, _, isComplete, error in
|
|
397
621
|
if let error = error {
|
|
398
|
-
|
|
622
|
+
print("FTP DEBUG: receiveData error: \(error)")
|
|
623
|
+
connectionBox.resume(throwing: error)
|
|
399
624
|
return
|
|
400
625
|
}
|
|
401
626
|
|
|
402
627
|
if let data = data {
|
|
403
628
|
receivedData.append(data)
|
|
629
|
+
print("FTP DEBUG: Received chunk of \(data.count) bytes, total: \(receivedData.count) bytes")
|
|
404
630
|
if reportProgress {
|
|
405
|
-
|
|
631
|
+
DispatchQueue.main.async {
|
|
632
|
+
self.progressDelegate?.onProgress(transferred: Int64(receivedData.count), total: -1)
|
|
633
|
+
}
|
|
406
634
|
}
|
|
407
635
|
}
|
|
408
636
|
|
|
409
637
|
if isComplete {
|
|
410
|
-
|
|
638
|
+
print("FTP DEBUG: Data reception complete, total bytes: \(receivedData.count)")
|
|
639
|
+
connectionBox.resume(returning: receivedData)
|
|
411
640
|
} else {
|
|
641
|
+
print("FTP DEBUG: Receiving more data...")
|
|
412
642
|
receiveMore()
|
|
413
643
|
}
|
|
414
644
|
}
|
|
@@ -427,15 +657,19 @@ class FtpClient {
|
|
|
427
657
|
let chunk = data.subdata(in: i..<endIndex)
|
|
428
658
|
|
|
429
659
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
660
|
+
let connectionBox = ConnectionBox<Void>(continuation: continuation)
|
|
661
|
+
|
|
430
662
|
connection.send(content: chunk, completion: .contentProcessed { error in
|
|
431
663
|
if let error = error {
|
|
432
|
-
|
|
664
|
+
connectionBox.resume(throwing: error)
|
|
433
665
|
} else {
|
|
434
666
|
sentBytes += chunk.count
|
|
435
667
|
if reportProgress {
|
|
436
|
-
|
|
668
|
+
DispatchQueue.main.async {
|
|
669
|
+
self.progressDelegate?.onProgress(transferred: Int64(sentBytes), total: Int64(totalSize))
|
|
670
|
+
}
|
|
437
671
|
}
|
|
438
|
-
|
|
672
|
+
connectionBox.resume()
|
|
439
673
|
}
|
|
440
674
|
})
|
|
441
675
|
}
|
|
@@ -443,48 +677,122 @@ class FtpClient {
|
|
|
443
677
|
}
|
|
444
678
|
|
|
445
679
|
private func parseListingData(_ data: Data) -> [FtpFileInfo] {
|
|
446
|
-
|
|
680
|
+
print("FTP DEBUG: Raw data length: \(data.count) bytes")
|
|
681
|
+
print("FTP DEBUG: Raw data hex: \(data.map { String(format: "%02x", $0) }.joined())")
|
|
447
682
|
|
|
448
|
-
|
|
449
|
-
var
|
|
683
|
+
// Try different encodings, prioritizing Chinese encodings
|
|
684
|
+
var string: String?
|
|
685
|
+
var encoding: String.Encoding = .utf8
|
|
686
|
+
|
|
687
|
+
// Try UTF-8 first
|
|
688
|
+
if let utf8String = String(data: data, encoding: .utf8) {
|
|
689
|
+
string = utf8String
|
|
690
|
+
encoding = .utf8
|
|
691
|
+
print("FTP DEBUG: Successfully decoded with UTF-8")
|
|
692
|
+
}
|
|
693
|
+
// Try GBK/GB2312 for Chinese
|
|
694
|
+
else if let gbkString = String(data: data, encoding: String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))) {
|
|
695
|
+
string = gbkString
|
|
696
|
+
encoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))
|
|
697
|
+
print("FTP DEBUG: Successfully decoded with GBK/GB18030")
|
|
698
|
+
}
|
|
699
|
+
// Try Big5 for Traditional Chinese
|
|
700
|
+
else if let big5String = String(data: data, encoding: String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.big5.rawValue)))) {
|
|
701
|
+
string = big5String
|
|
702
|
+
encoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.big5.rawValue)))
|
|
703
|
+
print("FTP DEBUG: Successfully decoded with Big5")
|
|
704
|
+
}
|
|
705
|
+
// Try ASCII
|
|
706
|
+
else if let asciiString = String(data: data, encoding: .ascii) {
|
|
707
|
+
string = asciiString
|
|
708
|
+
encoding = .ascii
|
|
709
|
+
print("FTP DEBUG: Successfully decoded with ASCII")
|
|
710
|
+
}
|
|
711
|
+
// Try Latin1 (ISO-8859-1)
|
|
712
|
+
else if let latin1String = String(data: data, encoding: .isoLatin1) {
|
|
713
|
+
string = latin1String
|
|
714
|
+
encoding = .isoLatin1
|
|
715
|
+
print("FTP DEBUG: Successfully decoded with Latin1")
|
|
716
|
+
}
|
|
717
|
+
// Try Windows-1252
|
|
718
|
+
else if let windowsString = String(data: data, encoding: .windowsCP1252) {
|
|
719
|
+
string = windowsString
|
|
720
|
+
encoding = .windowsCP1252
|
|
721
|
+
print("FTP DEBUG: Successfully decoded with Windows-1252")
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
print("FTP DEBUG: Failed to decode data with any encoding")
|
|
725
|
+
print("FTP DEBUG: Raw bytes: \(Array(data))")
|
|
726
|
+
return []
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
guard let decodedString = string else {
|
|
730
|
+
print("FTP DEBUG: No valid string found")
|
|
731
|
+
return []
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
print("FTP DEBUG: Decoded string with \(encoding): '\(decodedString)'")
|
|
735
|
+
print("FTP DEBUG: String length: \(decodedString.count) characters")
|
|
450
736
|
|
|
451
|
-
|
|
737
|
+
let lines = decodedString.components(separatedBy: .newlines)
|
|
738
|
+
print("FTP DEBUG: Found \(lines.count) lines")
|
|
739
|
+
var files: [FtpFileInfo] = []
|
|
740
|
+
|
|
741
|
+
for (index, line) in lines.enumerated() {
|
|
742
|
+
print("FTP DEBUG: Processing line \(index): '\(line)'")
|
|
452
743
|
if let fileInfo = parseListing(line) {
|
|
453
744
|
files.append(fileInfo)
|
|
745
|
+
print("FTP DEBUG: Parsed file: \(fileInfo.name), isDirectory: \(fileInfo.isDirectory)")
|
|
746
|
+
} else {
|
|
747
|
+
print("FTP DEBUG: Failed to parse line \(index)")
|
|
454
748
|
}
|
|
455
749
|
}
|
|
456
|
-
|
|
750
|
+
|
|
751
|
+
print("FTP DEBUG: Successfully parsed \(files.count) files")
|
|
457
752
|
return files
|
|
458
753
|
}
|
|
459
754
|
|
|
460
755
|
private func parseListing(_ line: String) -> FtpFileInfo? {
|
|
461
756
|
let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
462
|
-
guard !trimmedLine.isEmpty else {
|
|
463
|
-
|
|
757
|
+
guard !trimmedLine.isEmpty else {
|
|
758
|
+
print("FTP DEBUG: Line is empty after trimming")
|
|
759
|
+
return nil
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
print("FTP DEBUG: Parsing line: '\(trimmedLine)'")
|
|
763
|
+
|
|
464
764
|
// Parse Unix-style listing: drwxr-xr-x 3 user group 4096 Mar 15 10:30 dirname
|
|
465
765
|
let components = trimmedLine.components(separatedBy: .whitespaces).filter { !$0.isEmpty }
|
|
466
|
-
|
|
467
|
-
|
|
766
|
+
print("FTP DEBUG: Components count: \(components.count), components: \(components)")
|
|
767
|
+
|
|
768
|
+
guard components.count >= 9 else {
|
|
769
|
+
print("FTP DEBUG: Not enough components (\(components.count)), expected at least 9")
|
|
770
|
+
return nil
|
|
771
|
+
}
|
|
772
|
+
|
|
468
773
|
let permissions = components[0]
|
|
469
774
|
let isDirectory = permissions.hasPrefix("d")
|
|
470
775
|
let size = isDirectory ? 0 : Int64(components[4]) ?? 0
|
|
471
|
-
|
|
776
|
+
|
|
472
777
|
// Reconstruct filename (can contain spaces)
|
|
473
778
|
let name = components[8...].joined(separator: " ")
|
|
474
|
-
|
|
779
|
+
|
|
475
780
|
// Parse date
|
|
476
781
|
let month = components[5]
|
|
477
782
|
let day = components[6]
|
|
478
783
|
let yearOrTime = components[7]
|
|
479
784
|
let lastModified = "\(month) \(day) \(yearOrTime)"
|
|
480
|
-
|
|
481
|
-
|
|
785
|
+
|
|
786
|
+
let fileInfo = FtpFileInfo(
|
|
482
787
|
name: name,
|
|
483
788
|
isDirectory: isDirectory,
|
|
484
789
|
size: size,
|
|
485
790
|
lastModified: lastModified,
|
|
486
791
|
permissions: permissions
|
|
487
792
|
)
|
|
793
|
+
|
|
794
|
+
print("FTP DEBUG: Successfully parsed file: \(fileInfo)")
|
|
795
|
+
return fileInfo
|
|
488
796
|
}
|
|
489
797
|
}
|
|
490
798
|
|
|
@@ -565,13 +873,9 @@ public class ReactNativeKookitModule: Module {
|
|
|
565
873
|
|
|
566
874
|
AsyncFunction("ftpDisconnect") { (promise: Promise) in
|
|
567
875
|
Task {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
promise.resolve()
|
|
572
|
-
} catch {
|
|
573
|
-
promise.reject("FTP_DISCONNECT_ERROR", error.localizedDescription)
|
|
574
|
-
}
|
|
876
|
+
await self.ftpClient?.disconnect()
|
|
877
|
+
self.ftpClient = nil
|
|
878
|
+
promise.resolve()
|
|
575
879
|
}
|
|
576
880
|
}
|
|
577
881
|
|
package/package.json
CHANGED