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.
@@ -68,9 +68,152 @@ enum FtpError: Error, LocalizedError {
68
68
  }
69
69
  }
70
70
 
71
- class FtpClient {
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
- private var isConnected = false
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
- controlConnection?.stateUpdateHandler = { state in
88
- switch state {
89
- case .ready:
90
- Task {
91
- do {
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 parseListingData(data)
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
- guard response.hasPrefix("150") || response.hasPrefix("125") else {
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: \(response)")
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
- let finalResponse = try await readResponse()
182
- guard finalResponse.hasPrefix("226") else {
183
- try? FileManager.default.removeItem(at: localURL)
184
- throw FtpError.commandFailed("Download completion failed: \(finalResponse)")
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
- private func performAuthentication() async throws {
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
- continuation.resume(throwing: error)
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
- continuation.resume(returning: response)
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
- continuation.resume(throwing: error)
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
- continuation.resume(throwing: error)
333
- } else if let data = data, let response = String(data: data, encoding: .utf8) {
334
- continuation.resume(returning: response.trimmingCharacters(in: .whitespacesAndNewlines))
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
- continuation.resume(throwing: FtpError.invalidResponse)
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
- switch state {
376
- case .ready:
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
- continuation.resume(throwing: error)
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
- self.progressDelegate?.onProgress(transferred: Int64(receivedData.count), total: -1)
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
- continuation.resume(returning: receivedData)
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
- continuation.resume(throwing: error)
664
+ connectionBox.resume(throwing: error)
433
665
  } else {
434
666
  sentBytes += chunk.count
435
667
  if reportProgress {
436
- self.progressDelegate?.onProgress(transferred: Int64(sentBytes), total: Int64(totalSize))
668
+ DispatchQueue.main.async {
669
+ self.progressDelegate?.onProgress(transferred: Int64(sentBytes), total: Int64(totalSize))
670
+ }
437
671
  }
438
- continuation.resume()
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
- guard let string = String(data: data, encoding: .utf8) else { return [] }
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
- let lines = string.components(separatedBy: .newlines)
449
- var files: [FtpFileInfo] = []
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
- for line in lines {
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 { return nil }
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
- guard components.count >= 9 else { return nil }
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
- return FtpFileInfo(
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
- do {
569
- await self.ftpClient?.disconnect()
570
- self.ftpClient = nil
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-kookit",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "React Native module for intercepting volume button presses on iOS and Android, with FTP client functionality",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",