react-native-kookit 0.2.7 → 0.2.9

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
 
@@ -803,7 +804,171 @@ class FtpClient: @unchecked Sendable {
803
804
  }
804
805
  }
805
806
 
806
- // 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
807
972
 
808
973
  public class ReactNativeKookitModule: Module {
809
974
  private var volumeView: MPVolumeView?
@@ -811,6 +976,7 @@ public class ReactNativeKookitModule: Module {
811
976
  private var isVolumeKeyInterceptionEnabled = false
812
977
  private var previousVolume: Float = 0.0
813
978
  private var ftpClients: [String: FtpClient] = [:] // Store multiple FTP clients by ID
979
+ private var smbClients: [String: SmbClient] = [:] // Store multiple SMB clients by ID
814
980
 
815
981
  // Each module class must implement the definition function. The definition consists of components
816
982
  // that describes the module's functionality and behavior.
@@ -827,7 +993,9 @@ public class ReactNativeKookitModule: Module {
827
993
  ])
828
994
 
829
995
  // Defines event names that the module can send to JavaScript.
830
- Events("onChange", "onVolumeButtonPressed", "onFtpProgress", "onFtpComplete", "onFtpError")
996
+ Events("onChange", "onVolumeButtonPressed",
997
+ "onFtpProgress", "onFtpComplete", "onFtpError",
998
+ "onSmbProgress", "onSmbComplete", "onSmbError")
831
999
 
832
1000
  // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
833
1001
  Function("hello") {
@@ -1116,6 +1284,209 @@ public class ReactNativeKookitModule: Module {
1116
1284
  ])
1117
1285
  }
1118
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
+
1119
1490
  // Enables the module to be used as a native view. Definition components that are accepted as part of
1120
1491
  // the view definition: Prop, Events.
1121
1492
  View(ReactNativeKookitView.self) {
@@ -1243,4 +1614,38 @@ private class FtpProgressDelegateImpl: FtpProgressDelegate {
1243
1614
  "error": error
1244
1615
  ])
1245
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
+ }
1246
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.7",
3
+ "version": "0.2.9",
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",