react-native-kookit 0.2.6 → 0.2.8

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.
@@ -4,6 +4,7 @@ import MediaPlayer
4
4
  import AVFoundation
5
5
  import Foundation
6
6
  import Network
7
+ import SMBClient
7
8
 
8
9
  // MARK: - FTP Related Types and Classes
9
10
 
@@ -292,10 +293,17 @@ class FtpClient: @unchecked Sendable {
292
293
  }
293
294
  dataConnection.cancel()
294
295
 
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)")
296
+ // Check if the response already contained the completion message (226)
297
+ if response.contains("226") {
298
+ print("FTP DEBUG: Transfer completion already received in LIST response - skipping final response read")
299
+ } else {
300
+ print("FTP DEBUG: Reading final response...")
301
+ let finalResponse = try await readResponse()
302
+ let finalFirstLine = finalResponse.components(separatedBy: .newlines).first ?? finalResponse
303
+ print("FTP DEBUG: Final response: \(finalFirstLine)")
304
+ guard finalFirstLine.hasPrefix("226") else {
305
+ throw FtpError.commandFailed("LIST completion failed: \(finalFirstLine)")
306
+ }
299
307
  }
300
308
 
301
309
  let files = parseListingData(data)
@@ -796,7 +804,171 @@ class FtpClient: @unchecked Sendable {
796
804
  }
797
805
  }
798
806
 
799
- // MARK: - Main Module
807
+ // MARK: - SMB Client with SMBClient library
808
+
809
+ struct SmbFileInfo {
810
+ let name: String
811
+ let isDirectory: Bool
812
+ let size: Int64
813
+ let lastModified: String
814
+ let attributes: String?
815
+ }
816
+
817
+ class SmbClient: @unchecked Sendable {
818
+ private var client: SMBClient?
819
+ private(set) var isConnected: Bool = false
820
+ private var currentShare: String?
821
+ private weak var progressDelegate: SmbProgressDelegate?
822
+
823
+ func connect(host: String, username: String, password: String, domain: String? = nil, share: String? = nil, timeout: TimeInterval = 10.0) async throws {
824
+ client = SMBClient(host: host)
825
+
826
+ try await client!.login(username: username, password: password)
827
+ isConnected = true
828
+
829
+ if let share = share {
830
+ try await client!.connectShare(share)
831
+ currentShare = share
832
+ }
833
+ }
834
+
835
+ func connectShare(_ shareName: String) async throws {
836
+ guard let client = client, isConnected else {
837
+ throw FtpError.notConnected
838
+ }
839
+
840
+ if currentShare != nil {
841
+ try await client.disconnectShare()
842
+ }
843
+
844
+ try await client.connectShare(shareName)
845
+ currentShare = shareName
846
+ }
847
+
848
+ func listFiles(path: String? = nil) async throws -> [SmbFileInfo] {
849
+ guard let client = client, isConnected else {
850
+ throw FtpError.notConnected
851
+ }
852
+
853
+ guard currentShare != nil else {
854
+ throw FtpError.commandFailed("No share connected. Connect to a share first.")
855
+ }
856
+
857
+ let directoryPath = path?.isEmpty == false ? path! : ""
858
+ let files = try await client.listDirectory(path: directoryPath)
859
+
860
+ return files.map { file in
861
+ SmbFileInfo(
862
+ name: file.name,
863
+ isDirectory: file.isDirectory,
864
+ size: Int64(file.size),
865
+ lastModified: formatDate(file.lastWriteTime),
866
+ attributes: nil
867
+ )
868
+ }
869
+ }
870
+
871
+ func downloadFile(remotePath: String, localPath: String) async throws {
872
+ guard let client = client, isConnected else {
873
+ throw FtpError.notConnected
874
+ }
875
+
876
+ guard currentShare != nil else {
877
+ throw FtpError.commandFailed("No share connected. Connect to a share first.")
878
+ }
879
+
880
+ let localURL = URL(fileURLWithPath: localPath)
881
+ try FileManager.default.createDirectory(at: localURL.deletingLastPathComponent(),
882
+ withIntermediateDirectories: true,
883
+ attributes: nil)
884
+
885
+ let data = try await client.download(path: remotePath)
886
+ try data.write(to: localURL)
887
+
888
+ progressDelegate?.onComplete()
889
+ }
890
+
891
+ func uploadFile(localPath: String, remotePath: String) async throws {
892
+ guard let client = client, isConnected else {
893
+ throw FtpError.notConnected
894
+ }
895
+
896
+ guard currentShare != nil else {
897
+ throw FtpError.commandFailed("No share connected. Connect to a share first.")
898
+ }
899
+
900
+ let localURL = URL(fileURLWithPath: localPath)
901
+ guard FileManager.default.fileExists(atPath: localPath) else {
902
+ throw FtpError.fileNotFound("Local file not found: \(localPath)")
903
+ }
904
+
905
+ let data = try Data(contentsOf: localURL)
906
+ try await client.upload(content: data, path: remotePath)
907
+
908
+ progressDelegate?.onComplete()
909
+ }
910
+
911
+ func deleteFile(remotePath: String, isDirectory: Bool = false) async throws {
912
+ guard let client = client, isConnected else {
913
+ throw FtpError.notConnected
914
+ }
915
+
916
+ guard currentShare != nil else {
917
+ throw FtpError.commandFailed("No share connected. Connect to a share first.")
918
+ }
919
+
920
+ if isDirectory {
921
+ try await client.deleteDirectory(path: remotePath)
922
+ } else {
923
+ try await client.deleteFile(path: remotePath)
924
+ }
925
+ }
926
+
927
+ func createDirectory(remotePath: String) async throws {
928
+ guard let client = client, isConnected else {
929
+ throw FtpError.notConnected
930
+ }
931
+
932
+ guard currentShare != nil else {
933
+ throw FtpError.commandFailed("No share connected. Connect to a share first.")
934
+ }
935
+
936
+ try await client.createDirectory(path: remotePath)
937
+ }
938
+
939
+ func disconnect() async {
940
+ guard let client = client else { return }
941
+
942
+ do {
943
+ if currentShare != nil {
944
+ try await client.disconnectShare()
945
+ }
946
+ try await client.logoff()
947
+ } catch {
948
+ // Ignore errors during disconnect
949
+ }
950
+
951
+ self.client = nil
952
+ isConnected = false
953
+ currentShare = nil
954
+ }
955
+
956
+ func setProgressDelegate(_ delegate: SmbProgressDelegate?) {
957
+ self.progressDelegate = delegate
958
+ }
959
+
960
+ private func formatDate(_ date: Date) -> String {
961
+ let formatter = DateFormatter()
962
+ formatter.dateFormat = "MMM dd HH:mm"
963
+ return formatter.string(from: date)
964
+ }
965
+ }
966
+
967
+ protocol SmbProgressDelegate: AnyObject {
968
+ func onProgress(transferred: Int64, total: Int64)
969
+ func onComplete()
970
+ func onError(error: String)
971
+ }// MARK: - Main Module
800
972
 
801
973
  public class ReactNativeKookitModule: Module {
802
974
  private var volumeView: MPVolumeView?
@@ -804,6 +976,7 @@ public class ReactNativeKookitModule: Module {
804
976
  private var isVolumeKeyInterceptionEnabled = false
805
977
  private var previousVolume: Float = 0.0
806
978
  private var ftpClients: [String: FtpClient] = [:] // Store multiple FTP clients by ID
979
+ private var smbClients: [String: SmbClient] = [:] // Store multiple SMB clients by ID
807
980
 
808
981
  // Each module class must implement the definition function. The definition consists of components
809
982
  // that describes the module's functionality and behavior.
@@ -820,7 +993,9 @@ public class ReactNativeKookitModule: Module {
820
993
  ])
821
994
 
822
995
  // Defines event names that the module can send to JavaScript.
823
- Events("onChange", "onVolumeButtonPressed", "onFtpProgress", "onFtpComplete", "onFtpError")
996
+ Events("onChange", "onVolumeButtonPressed",
997
+ "onFtpProgress", "onFtpComplete", "onFtpError",
998
+ "onSmbProgress", "onSmbComplete", "onSmbError")
824
999
 
825
1000
  // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
826
1001
  Function("hello") {
@@ -1109,6 +1284,209 @@ public class ReactNativeKookitModule: Module {
1109
1284
  ])
1110
1285
  }
1111
1286
 
1287
+ // MARK: - SMB Client API
1288
+
1289
+ AsyncFunction("createSmbClient") { (clientId: String, promise: Promise) in
1290
+ if self.smbClients[clientId] != nil {
1291
+ promise.reject("SMB_CLIENT_EXISTS", "SMB client with ID '\(clientId)' already exists")
1292
+ return
1293
+ }
1294
+ self.smbClients[clientId] = SmbClient()
1295
+ promise.resolve(["clientId": clientId])
1296
+ }
1297
+
1298
+ AsyncFunction("disposeSmbClient") { (clientId: String, promise: Promise) in
1299
+ if let client = self.smbClients[clientId] {
1300
+ Task {
1301
+ await client.disconnect()
1302
+ self.smbClients.removeValue(forKey: clientId)
1303
+ promise.resolve([:])
1304
+ }
1305
+ } else {
1306
+ promise.resolve([:])
1307
+ }
1308
+ }
1309
+
1310
+ AsyncFunction("getSmbClientStatus") { (clientId: String, promise: Promise) in
1311
+ guard let client = self.smbClients[clientId] else {
1312
+ promise.resolve(["exists": false, "connected": false])
1313
+ return
1314
+ }
1315
+ promise.resolve(["exists": true, "connected": client.isConnected])
1316
+ }
1317
+
1318
+ AsyncFunction("listSmbClients") { (promise: Promise) in
1319
+ let clientsInfo = self.smbClients.mapValues { client in
1320
+ ["connected": client.isConnected]
1321
+ }
1322
+ promise.resolve(["clients": clientsInfo, "count": self.smbClients.count])
1323
+ }
1324
+
1325
+ AsyncFunction("smbClientConnect") { (clientId: String, config: [String: Any], promise: Promise) in
1326
+ guard let client = self.smbClients[clientId] else {
1327
+ promise.reject("SMB_CLIENT_NOT_FOUND", "SMB client with ID '\(clientId)' not found")
1328
+ return
1329
+ }
1330
+ let host = config["host"] as? String ?? ""
1331
+ let username = config["username"] as? String ?? ""
1332
+ let password = config["password"] as? String ?? ""
1333
+ let domain = config["domain"] as? String
1334
+ let share = config["share"] as? String
1335
+ let timeout = (config["timeout"] as? TimeInterval) ?? 10.0
1336
+ if host.isEmpty || username.isEmpty {
1337
+ promise.reject("SMB_INVALID_CONFIG", "Missing host or username")
1338
+ return
1339
+ }
1340
+ Task {
1341
+ do {
1342
+ try await client.connect(host: host, username: username, password: password, domain: domain, share: share, timeout: timeout)
1343
+ promise.resolve(["clientId": clientId, "connected": true])
1344
+ } catch {
1345
+ promise.reject("SMB_CONNECT_ERROR", error.localizedDescription)
1346
+ }
1347
+ }
1348
+ }
1349
+
1350
+ AsyncFunction("smbClientDisconnect") { (clientId: String, promise: Promise) in
1351
+ guard let client = self.smbClients[clientId] else {
1352
+ promise.reject("SMB_CLIENT_NOT_FOUND", "SMB client with ID '\(clientId)' not found")
1353
+ return
1354
+ }
1355
+ Task {
1356
+ await client.disconnect()
1357
+ promise.resolve(["clientId": clientId, "disconnected": true])
1358
+ }
1359
+ }
1360
+
1361
+ AsyncFunction("smbClientConnectShare") { (clientId: String, shareName: String, promise: Promise) in
1362
+ guard let client = self.smbClients[clientId] else {
1363
+ promise.reject("SMB_CLIENT_NOT_FOUND", "SMB client with ID '\(clientId)' not found")
1364
+ return
1365
+ }
1366
+ Task {
1367
+ do {
1368
+ try await client.connectShare(shareName)
1369
+ promise.resolve(["clientId": clientId, "share": shareName, "connected": true])
1370
+ } catch {
1371
+ promise.reject("SMB_CONNECT_SHARE_ERROR", error.localizedDescription)
1372
+ }
1373
+ }
1374
+ }
1375
+
1376
+ AsyncFunction("smbClientList") { (clientId: String, path: String?, promise: Promise) in
1377
+ guard let client = self.smbClients[clientId] else {
1378
+ promise.reject("SMB_CLIENT_NOT_FOUND", "SMB client with ID '\(clientId)' not found")
1379
+ return
1380
+ }
1381
+ Task {
1382
+ do {
1383
+ let files = try await client.listFiles(path: path)
1384
+ let result = files.map { file in
1385
+ [
1386
+ "name": file.name,
1387
+ "isDirectory": file.isDirectory,
1388
+ "size": file.size,
1389
+ "lastModified": file.lastModified,
1390
+ "attributes": file.attributes as Any
1391
+ ]
1392
+ }
1393
+ promise.resolve(result)
1394
+ } catch {
1395
+ promise.reject("SMB_LIST_ERROR", error.localizedDescription)
1396
+ }
1397
+ }
1398
+ }
1399
+
1400
+ AsyncFunction("smbClientDownload") { (clientId: String, remotePath: String, localPath: String, promise: Promise) in
1401
+ guard let client = self.smbClients[clientId] else {
1402
+ promise.reject("SMB_CLIENT_NOT_FOUND", "SMB client with ID '\(clientId)' not found")
1403
+ return
1404
+ }
1405
+ Task {
1406
+ do {
1407
+ let progressDelegate = SmbProgressDelegateImpl(module: self, clientId: clientId)
1408
+ client.setProgressDelegate(progressDelegate)
1409
+ try await client.downloadFile(remotePath: remotePath, localPath: localPath)
1410
+ promise.resolve([
1411
+ "clientId": clientId,
1412
+ "remotePath": remotePath,
1413
+ "localPath": localPath,
1414
+ "downloaded": true
1415
+ ])
1416
+ } catch {
1417
+ self.sendEvent("onSmbError", [
1418
+ "clientId": clientId,
1419
+ "error": error.localizedDescription
1420
+ ])
1421
+ promise.reject("SMB_DOWNLOAD_ERROR", error.localizedDescription)
1422
+ }
1423
+ }
1424
+ }
1425
+
1426
+ AsyncFunction("smbClientUpload") { (clientId: String, localPath: String, remotePath: String, promise: Promise) in
1427
+ guard let client = self.smbClients[clientId] else {
1428
+ promise.reject("SMB_CLIENT_NOT_FOUND", "SMB client with ID '\(clientId)' not found")
1429
+ return
1430
+ }
1431
+ Task {
1432
+ do {
1433
+ let progressDelegate = SmbProgressDelegateImpl(module: self, clientId: clientId)
1434
+ client.setProgressDelegate(progressDelegate)
1435
+ try await client.uploadFile(localPath: localPath, remotePath: remotePath)
1436
+ promise.resolve([
1437
+ "clientId": clientId,
1438
+ "localPath": localPath,
1439
+ "remotePath": remotePath,
1440
+ "uploaded": true
1441
+ ])
1442
+ } catch {
1443
+ self.sendEvent("onSmbError", [
1444
+ "clientId": clientId,
1445
+ "error": error.localizedDescription
1446
+ ])
1447
+ promise.reject("SMB_UPLOAD_ERROR", error.localizedDescription)
1448
+ }
1449
+ }
1450
+ }
1451
+
1452
+ AsyncFunction("smbClientDelete") { (clientId: String, remotePath: String, isDirectory: Bool?, promise: Promise) in
1453
+ guard let client = self.smbClients[clientId] else {
1454
+ promise.reject("SMB_CLIENT_NOT_FOUND", "SMB client with ID '\(clientId)' not found")
1455
+ return
1456
+ }
1457
+ Task {
1458
+ do {
1459
+ try await client.deleteFile(remotePath: remotePath, isDirectory: isDirectory ?? false)
1460
+ promise.resolve([
1461
+ "clientId": clientId,
1462
+ "remotePath": remotePath,
1463
+ "deleted": true
1464
+ ])
1465
+ } catch {
1466
+ promise.reject("SMB_DELETE_ERROR", error.localizedDescription)
1467
+ }
1468
+ }
1469
+ }
1470
+
1471
+ AsyncFunction("smbClientCreateDirectory") { (clientId: String, remotePath: String, promise: Promise) in
1472
+ guard let client = self.smbClients[clientId] else {
1473
+ promise.reject("SMB_CLIENT_NOT_FOUND", "SMB client with ID '\(clientId)' not found")
1474
+ return
1475
+ }
1476
+ Task {
1477
+ do {
1478
+ try await client.createDirectory(remotePath: remotePath)
1479
+ promise.resolve([
1480
+ "clientId": clientId,
1481
+ "remotePath": remotePath,
1482
+ "created": true
1483
+ ])
1484
+ } catch {
1485
+ promise.reject("SMB_CREATE_DIR_ERROR", error.localizedDescription)
1486
+ }
1487
+ }
1488
+ }
1489
+
1112
1490
  // Enables the module to be used as a native view. Definition components that are accepted as part of
1113
1491
  // the view definition: Prop, Events.
1114
1492
  View(ReactNativeKookitView.self) {
@@ -1236,4 +1614,38 @@ private class FtpProgressDelegateImpl: FtpProgressDelegate {
1236
1614
  "error": error
1237
1615
  ])
1238
1616
  }
1617
+ }
1618
+
1619
+ // Helper class to handle SMB progress callbacks
1620
+ private class SmbProgressDelegateImpl: SmbProgressDelegate {
1621
+ weak var module: ReactNativeKookitModule?
1622
+ let clientId: String
1623
+
1624
+ init(module: ReactNativeKookitModule, clientId: String) {
1625
+ self.module = module
1626
+ self.clientId = clientId
1627
+ }
1628
+
1629
+ func onProgress(transferred: Int64, total: Int64) {
1630
+ let percentage = total > 0 ? Int((transferred * 100) / total) : 0
1631
+ module?.sendEvent("onSmbProgress", [
1632
+ "clientId": clientId,
1633
+ "transferred": transferred,
1634
+ "total": total,
1635
+ "percentage": percentage
1636
+ ])
1637
+ }
1638
+
1639
+ func onComplete() {
1640
+ module?.sendEvent("onSmbComplete", [
1641
+ "clientId": clientId
1642
+ ])
1643
+ }
1644
+
1645
+ func onError(error: String) {
1646
+ module?.sendEvent("onSmbError", [
1647
+ "clientId": clientId,
1648
+ "error": error
1649
+ ])
1650
+ }
1239
1651
  }
@@ -0,0 +1,18 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'SMBClient'
3
+ s.version = '0.3.1'
4
+ s.summary = 'SMB2/3 client implementation for iOS/macOS'
5
+ s.description = 'A Swift implementation of SMB2/3 client protocol for iOS and macOS'
6
+ s.homepage = 'https://github.com/kishikawakatsumi/SMBClient'
7
+ s.license = { :type => 'MIT', :file => 'LICENSE' }
8
+ s.author = { 'Kishikawa Katsumi' => 'kishikawakatsumi@mac.com' }
9
+ s.source = { :git => 'https://github.com/kishikawakatsumi/SMBClient.git', :tag => s.version.to_s }
10
+
11
+ s.ios.deployment_target = '13.0'
12
+ s.osx.deployment_target = '10.15'
13
+ s.swift_version = '5.9'
14
+
15
+ s.source_files = 'Sources/SMBClient/**/*'
16
+
17
+ s.frameworks = 'Foundation', 'Network'
18
+ end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-kookit",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",