react-native-kookit 0.2.7 → 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.
- package/ANDROID_BUILD_FIX.md +117 -0
- package/SMB_COMPLETE_GUIDE.md +308 -0
- package/SMB_IMPLEMENTATION_SUMMARY.md +229 -0
- package/SMB_USAGE.md +275 -0
- package/android/build.gradle +7 -0
- package/android/src/main/java/expo/modules/kookit/ReactNativeKookitModule.kt +224 -1
- package/android/src/main/java/expo/modules/kookit/SmbClient.kt +173 -0
- package/build/ReactNativeKookit.types.d.ts +39 -0
- package/build/ReactNativeKookit.types.d.ts.map +1 -1
- package/build/ReactNativeKookit.types.js.map +1 -1
- package/build/ReactNativeKookitModule.d.ts +23 -1
- package/build/ReactNativeKookitModule.d.ts.map +1 -1
- package/build/ReactNativeKookitModule.js.map +1 -1
- package/build/SmbClient.d.ts +45 -0
- package/build/SmbClient.d.ts.map +1 -0
- package/build/SmbClient.js +122 -0
- package/build/SmbClient.js.map +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/ReactNativeKookit.podspec +1 -0
- package/ios/ReactNativeKookitModule.swift +407 -2
- package/ios/SMBClient.podspec +18 -0
- package/package.json +1 -1
package/SMB_USAGE.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# SMB Client 完整功能使用指南
|
|
2
|
+
|
|
3
|
+
React Native Kookit 现在提供了完整的 SMB (Server Message Block) 客户端功能,支持 Android 和 iOS 平台。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
✅ **OOP 面向对象设计** - 与 FTP 客户端 API 一致的设计模式
|
|
8
|
+
✅ **多客户端管理** - 支持同时创建和管理多个 SMB 连接
|
|
9
|
+
✅ **完整文件操作** - 连接、列目录、下载、上传、删除、创建目录
|
|
10
|
+
✅ **进度事件** - 下载/上传进度回调和完成/错误事件
|
|
11
|
+
✅ **跨平台支持** - Android (SMBJ) 和 iOS (SMBClient) 原生实现
|
|
12
|
+
|
|
13
|
+
## 基本用法
|
|
14
|
+
|
|
15
|
+
### 1. 创建和连接
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { SmbClient } from "react-native-kookit";
|
|
19
|
+
|
|
20
|
+
// 创建 SMB 客户端实例
|
|
21
|
+
const client = await SmbClient.create();
|
|
22
|
+
|
|
23
|
+
// 设置事件监听器(可选)
|
|
24
|
+
client.setEventHandlers({
|
|
25
|
+
onProgress: (progress) => {
|
|
26
|
+
console.log(
|
|
27
|
+
`Progress: ${progress.percentage}% (${progress.transferred}/${progress.total})`
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
onComplete: () => {
|
|
31
|
+
console.log("操作完成");
|
|
32
|
+
},
|
|
33
|
+
onError: (error) => {
|
|
34
|
+
console.error("SMB 错误:", error.message);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 连接到 SMB 服务器
|
|
39
|
+
await client.connect({
|
|
40
|
+
host: "192.168.1.100",
|
|
41
|
+
port: 445, // 可选,默认 445
|
|
42
|
+
username: "myuser",
|
|
43
|
+
password: "mypassword",
|
|
44
|
+
domain: "WORKGROUP", // 可选,Windows 域
|
|
45
|
+
share: "Public", // 可选,默认共享
|
|
46
|
+
timeout: 10000, // 可选,超时时间(毫秒)
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. 文件和目录操作
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// 列出文件和目录
|
|
54
|
+
const files = await client.list("Documents");
|
|
55
|
+
files.forEach((file) => {
|
|
56
|
+
console.log(
|
|
57
|
+
`${file.isDirectory ? "📁" : "📄"} ${file.name} (${file.size} bytes)`
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 下载文件
|
|
62
|
+
await client.download(
|
|
63
|
+
"Documents/report.pdf", // 远程路径
|
|
64
|
+
"/storage/emulated/0/Download/report.pdf" // 本地路径
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// 上传文件
|
|
68
|
+
await client.upload(
|
|
69
|
+
"/storage/emulated/0/Pictures/photo.jpg", // 本地路径
|
|
70
|
+
"Photos/photo.jpg" // 远程路径
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 创建目录
|
|
74
|
+
await client.createDirectory("NewFolder");
|
|
75
|
+
|
|
76
|
+
// 删除文件
|
|
77
|
+
await client.delete("OldFile.txt");
|
|
78
|
+
|
|
79
|
+
// 删除目录
|
|
80
|
+
await client.delete("OldFolder", true);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. 连接管理
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// 检查连接状态
|
|
87
|
+
const isConnected = await client.isConnected();
|
|
88
|
+
|
|
89
|
+
// 获取客户端状态
|
|
90
|
+
const status = await client.getStatus();
|
|
91
|
+
console.log(
|
|
92
|
+
`Client ${status.clientId}: exists=${status.exists}, connected=${status.connected}`
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// 断开连接
|
|
96
|
+
await client.disconnect();
|
|
97
|
+
|
|
98
|
+
// 释放资源
|
|
99
|
+
await client.dispose();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 4. 多客户端管理
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// 创建多个客户端
|
|
106
|
+
const client1 = await SmbClient.create("server1");
|
|
107
|
+
const client2 = await SmbClient.create("server2");
|
|
108
|
+
|
|
109
|
+
// 分别连接到不同服务器
|
|
110
|
+
await client1.connect({
|
|
111
|
+
host: "192.168.1.100",
|
|
112
|
+
username: "user1",
|
|
113
|
+
password: "pass1",
|
|
114
|
+
share: "Share1",
|
|
115
|
+
});
|
|
116
|
+
await client2.connect({
|
|
117
|
+
host: "192.168.1.101",
|
|
118
|
+
username: "user2",
|
|
119
|
+
password: "pass2",
|
|
120
|
+
share: "Share2",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 列出所有客户端
|
|
124
|
+
const allClients = await SmbClient.listClients();
|
|
125
|
+
console.log(`管理中的客户端数量: ${allClients.count}`);
|
|
126
|
+
|
|
127
|
+
// 清理资源
|
|
128
|
+
await client1.dispose();
|
|
129
|
+
await client2.dispose();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## React Native 组件示例
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import React, { useState, useEffect } from "react";
|
|
136
|
+
import { View, Text, Button, FlatList } from "react-native";
|
|
137
|
+
import { SmbClient, SmbFileInfo } from "react-native-kookit";
|
|
138
|
+
|
|
139
|
+
export function SmbBrowser() {
|
|
140
|
+
const [client, setClient] = useState<SmbClient | null>(null);
|
|
141
|
+
const [files, setFiles] = useState<SmbFileInfo[]>([]);
|
|
142
|
+
const [currentPath, setCurrentPath] = useState("");
|
|
143
|
+
|
|
144
|
+
const connect = async () => {
|
|
145
|
+
const smbClient = await SmbClient.create();
|
|
146
|
+
|
|
147
|
+
smbClient.setEventHandlers({
|
|
148
|
+
onProgress: (p) => console.log(`进度: ${p.percentage}%`),
|
|
149
|
+
onComplete: () => console.log("操作完成"),
|
|
150
|
+
onError: (e) => console.error("SMB 错误:", e.message),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await smbClient.connect({
|
|
154
|
+
host: "192.168.1.100",
|
|
155
|
+
username: "user",
|
|
156
|
+
password: "password",
|
|
157
|
+
share: "Public",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
setClient(smbClient);
|
|
161
|
+
await listFiles("");
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const listFiles = async (path: string) => {
|
|
165
|
+
if (!client) return;
|
|
166
|
+
const fileList = await client.list(path);
|
|
167
|
+
setFiles(fileList);
|
|
168
|
+
setCurrentPath(path);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const downloadFile = async (fileName: string) => {
|
|
172
|
+
if (!client) return;
|
|
173
|
+
const remotePath = currentPath ? `${currentPath}/${fileName}` : fileName;
|
|
174
|
+
const localPath = `/storage/emulated/0/Download/${fileName}`;
|
|
175
|
+
await client.download(remotePath, localPath);
|
|
176
|
+
alert(`文件已下载到: ${localPath}`);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
return () => {
|
|
181
|
+
client?.dispose();
|
|
182
|
+
};
|
|
183
|
+
}, [client]);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<View style={{ flex: 1, padding: 16 }}>
|
|
187
|
+
<Text style={{ fontSize: 18, marginBottom: 16 }}>SMB 文件浏览器</Text>
|
|
188
|
+
|
|
189
|
+
{!client ? (
|
|
190
|
+
<Button title="连接 SMB 服务器" onPress={connect} />
|
|
191
|
+
) : (
|
|
192
|
+
<>
|
|
193
|
+
<Text>当前路径: /{currentPath}</Text>
|
|
194
|
+
<Button title="返回上级" onPress={() => listFiles("")} />
|
|
195
|
+
|
|
196
|
+
<FlatList
|
|
197
|
+
data={files}
|
|
198
|
+
keyExtractor={(item, index) => index.toString()}
|
|
199
|
+
renderItem={({ item }) => (
|
|
200
|
+
<View
|
|
201
|
+
style={{
|
|
202
|
+
padding: 8,
|
|
203
|
+
borderBottomWidth: 1,
|
|
204
|
+
borderColor: "#eee",
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<Text
|
|
208
|
+
style={{ fontWeight: item.isDirectory ? "bold" : "normal" }}
|
|
209
|
+
>
|
|
210
|
+
{item.isDirectory ? "📁" : "📄"} {item.name}
|
|
211
|
+
</Text>
|
|
212
|
+
<Text style={{ fontSize: 12, color: "#666" }}>
|
|
213
|
+
大小: {item.size} | 修改时间: {item.lastModified}
|
|
214
|
+
</Text>
|
|
215
|
+
{!item.isDirectory && (
|
|
216
|
+
<Button
|
|
217
|
+
title="下载"
|
|
218
|
+
onPress={() => downloadFile(item.name)}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
</View>
|
|
222
|
+
)}
|
|
223
|
+
/>
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
</View>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## 平台特定说明
|
|
232
|
+
|
|
233
|
+
### Android
|
|
234
|
+
|
|
235
|
+
- 基于 **SMBJ** 库实现
|
|
236
|
+
- 支持完整的 SMB2/3 协议
|
|
237
|
+
- 包含文件上传/下载进度回调
|
|
238
|
+
- 自动处理认证和会话管理
|
|
239
|
+
|
|
240
|
+
### iOS
|
|
241
|
+
|
|
242
|
+
- 基于 **SMBClient** (kishikawakatsumi) 库实现
|
|
243
|
+
- 支持 SMB2 协议
|
|
244
|
+
- 完整的文件操作和进度事件
|
|
245
|
+
- 自动处理连接和共享管理
|
|
246
|
+
|
|
247
|
+
## 错误处理
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
try {
|
|
251
|
+
await client.connect(config);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error.message.includes("authentication")) {
|
|
254
|
+
console.error("认证失败,请检查用户名和密码");
|
|
255
|
+
} else if (error.message.includes("timeout")) {
|
|
256
|
+
console.error("连接超时,请检查网络和主机地址");
|
|
257
|
+
} else {
|
|
258
|
+
console.error("连接错误:", error.message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## 性能和最佳实践
|
|
264
|
+
|
|
265
|
+
1. **连接管理**: 不用时及时调用 `disconnect()` 和 `dispose()`
|
|
266
|
+
2. **批量操作**: 对于多个文件操作,复用同一个客户端连接
|
|
267
|
+
3. **进度监听**: 对大文件操作使用进度事件提升用户体验
|
|
268
|
+
4. **错误处理**: 始终包装 SMB 操作在 try-catch 块中
|
|
269
|
+
5. **内存管理**: 在组件卸载时清理客户端资源
|
|
270
|
+
|
|
271
|
+
## 许可和依赖
|
|
272
|
+
|
|
273
|
+
- **Android**: SMBJ (Apache License 2.0)
|
|
274
|
+
- **iOS**: SMBClient (MIT License)
|
|
275
|
+
- 该模块遵循 MIT 许可证
|
package/android/build.gradle
CHANGED
|
@@ -18,6 +18,7 @@ class ReactNativeKookitModule : Module() {
|
|
|
18
18
|
private var originalStreamVolume: Int = 0
|
|
19
19
|
private var ftpClient: FtpClient? = null
|
|
20
20
|
private val ftpClients = mutableMapOf<String, FtpClient>()
|
|
21
|
+
private val smbClients = mutableMapOf<String, SmbClient>()
|
|
21
22
|
private val moduleScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
22
23
|
|
|
23
24
|
// Each module class must implement the definition function. The definition consists of components
|
|
@@ -35,7 +36,11 @@ class ReactNativeKookitModule : Module() {
|
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
// Defines event names that the module can send to JavaScript.
|
|
38
|
-
Events(
|
|
39
|
+
Events(
|
|
40
|
+
"onChange", "onVolumeButtonPressed",
|
|
41
|
+
"onFtpProgress", "onFtpComplete", "onFtpError",
|
|
42
|
+
"onSmbProgress", "onSmbComplete", "onSmbError"
|
|
43
|
+
)
|
|
39
44
|
|
|
40
45
|
// Function to enable volume key interception
|
|
41
46
|
Function("enableVolumeKeyInterception") {
|
|
@@ -470,6 +475,224 @@ class ReactNativeKookitModule : Module() {
|
|
|
470
475
|
}
|
|
471
476
|
}
|
|
472
477
|
|
|
478
|
+
// SMB Client API
|
|
479
|
+
AsyncFunction("createSmbClient") { clientId: String, promise: Promise ->
|
|
480
|
+
moduleScope.launch {
|
|
481
|
+
try {
|
|
482
|
+
if (smbClients.containsKey(clientId)) {
|
|
483
|
+
promise.reject("SMB_CLIENT_EXISTS", "SMB client with ID '$clientId' already exists", null)
|
|
484
|
+
return@launch
|
|
485
|
+
}
|
|
486
|
+
smbClients[clientId] = SmbClient()
|
|
487
|
+
promise.resolve(mapOf("clientId" to clientId))
|
|
488
|
+
} catch (e: Exception) {
|
|
489
|
+
promise.reject("SMB_CREATE_CLIENT_ERROR", e.message, e)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
AsyncFunction("disposeSmbClient") { clientId: String, promise: Promise ->
|
|
495
|
+
moduleScope.launch {
|
|
496
|
+
try {
|
|
497
|
+
smbClients[clientId]?.disconnect()
|
|
498
|
+
smbClients.remove(clientId)
|
|
499
|
+
promise.resolve(null)
|
|
500
|
+
} catch (e: Exception) {
|
|
501
|
+
promise.reject("SMB_DISPOSE_CLIENT_ERROR", e.message, e)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
AsyncFunction("getSmbClientStatus") { clientId: String, promise: Promise ->
|
|
507
|
+
moduleScope.launch {
|
|
508
|
+
try {
|
|
509
|
+
val client = smbClients[clientId]
|
|
510
|
+
val status = mapOf(
|
|
511
|
+
"exists" to (client != null),
|
|
512
|
+
"connected" to (client?.isConnected() ?: false)
|
|
513
|
+
)
|
|
514
|
+
promise.resolve(status)
|
|
515
|
+
} catch (e: Exception) {
|
|
516
|
+
promise.reject("SMB_CLIENT_STATUS_ERROR", e.message, e)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
AsyncFunction("listSmbClients") { promise: Promise ->
|
|
522
|
+
moduleScope.launch {
|
|
523
|
+
try {
|
|
524
|
+
val clients = mutableMapOf<String, Map<String, Any>>()
|
|
525
|
+
smbClients.forEach { (id, client) ->
|
|
526
|
+
clients[id] = mapOf("connected" to client.isConnected())
|
|
527
|
+
}
|
|
528
|
+
val result = mapOf(
|
|
529
|
+
"clients" to clients,
|
|
530
|
+
"count" to smbClients.size
|
|
531
|
+
)
|
|
532
|
+
promise.resolve(result)
|
|
533
|
+
} catch (e: Exception) {
|
|
534
|
+
promise.reject("SMB_LIST_CLIENTS_ERROR", e.message, e)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
AsyncFunction("smbClientConnect") { clientId: String, config: Map<String, Any>, promise: Promise ->
|
|
540
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
541
|
+
try {
|
|
542
|
+
val client = smbClients[clientId]
|
|
543
|
+
?: throw Exception("SMB client with ID '$clientId' not found")
|
|
544
|
+
val cfg = SmbConnectionConfig(
|
|
545
|
+
host = config["host"] as String,
|
|
546
|
+
port = (config["port"] as? Double)?.toInt() ?: 445,
|
|
547
|
+
username = config["username"] as String,
|
|
548
|
+
password = config["password"] as String,
|
|
549
|
+
domain = config["domain"] as? String,
|
|
550
|
+
share = config["share"] as? String,
|
|
551
|
+
timeoutMs = (config["timeout"] as? Double)?.toInt() ?: 10000
|
|
552
|
+
)
|
|
553
|
+
client.connect(cfg)
|
|
554
|
+
// If share not provided in connect, allow later openShare via list/upload APIs
|
|
555
|
+
withContext(Dispatchers.Main) { promise.resolve(null) }
|
|
556
|
+
} catch (e: Exception) {
|
|
557
|
+
withContext(Dispatchers.Main) { promise.reject("SMB_CLIENT_CONNECT_ERROR", e.message, e) }
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
AsyncFunction("smbClientDisconnect") { clientId: String, promise: Promise ->
|
|
563
|
+
moduleScope.launch {
|
|
564
|
+
try {
|
|
565
|
+
val client = smbClients[clientId]
|
|
566
|
+
?: throw Exception("SMB client with ID '$clientId' not found")
|
|
567
|
+
client.disconnect()
|
|
568
|
+
promise.resolve(null)
|
|
569
|
+
} catch (e: Exception) {
|
|
570
|
+
promise.reject("SMB_CLIENT_DISCONNECT_ERROR", e.message, e)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
AsyncFunction("smbClientConnectShare") { clientId: String, shareName: String, promise: Promise ->
|
|
576
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
577
|
+
try {
|
|
578
|
+
val client = smbClients[clientId]
|
|
579
|
+
?: throw Exception("SMB client with ID '$clientId' not found")
|
|
580
|
+
client.openShare(shareName)
|
|
581
|
+
withContext(Dispatchers.Main) {
|
|
582
|
+
promise.resolve(mapOf(
|
|
583
|
+
"clientId" to clientId,
|
|
584
|
+
"share" to shareName,
|
|
585
|
+
"connected" to true
|
|
586
|
+
))
|
|
587
|
+
}
|
|
588
|
+
} catch (e: Exception) {
|
|
589
|
+
withContext(Dispatchers.Main) {
|
|
590
|
+
promise.reject("SMB_CONNECT_SHARE_ERROR", e.message, e)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
AsyncFunction("smbClientList") { clientId: String, path: String?, promise: Promise ->
|
|
597
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
598
|
+
try {
|
|
599
|
+
val client = smbClients[clientId]
|
|
600
|
+
?: throw Exception("SMB client with ID '$clientId' not found")
|
|
601
|
+
val items = client.list(path)
|
|
602
|
+
val mapped = items.map {
|
|
603
|
+
mapOf(
|
|
604
|
+
"name" to it.name,
|
|
605
|
+
"isDirectory" to it.isDirectory,
|
|
606
|
+
"size" to it.size,
|
|
607
|
+
"lastModified" to it.lastModified,
|
|
608
|
+
"attributes" to it.attributes
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
withContext(Dispatchers.Main) { promise.resolve(mapped) }
|
|
612
|
+
} catch (e: Exception) {
|
|
613
|
+
withContext(Dispatchers.Main) { promise.reject("SMB_CLIENT_LIST_ERROR", e.message, e) }
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
AsyncFunction("smbClientDownload") { clientId: String, remotePath: String, localPath: String, promise: Promise ->
|
|
619
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
620
|
+
try {
|
|
621
|
+
val client = smbClients[clientId]
|
|
622
|
+
?: throw Exception("SMB client with ID '$clientId' not found")
|
|
623
|
+
client.download(remotePath, localPath) { transferred, total ->
|
|
624
|
+
val percentage = if (total > 0) ((transferred * 100) / total).toInt() else 0
|
|
625
|
+
sendEvent("onSmbProgress", mapOf(
|
|
626
|
+
"clientId" to clientId,
|
|
627
|
+
"transferred" to transferred,
|
|
628
|
+
"total" to total,
|
|
629
|
+
"percentage" to percentage
|
|
630
|
+
))
|
|
631
|
+
}
|
|
632
|
+
sendEvent("onSmbComplete", mapOf("clientId" to clientId))
|
|
633
|
+
withContext(Dispatchers.Main) { promise.resolve(null) }
|
|
634
|
+
} catch (e: Exception) {
|
|
635
|
+
sendEvent("onSmbError", mapOf(
|
|
636
|
+
"clientId" to clientId,
|
|
637
|
+
"error" to (e.message ?: "SMB download error")
|
|
638
|
+
))
|
|
639
|
+
withContext(Dispatchers.Main) { promise.reject("SMB_CLIENT_DOWNLOAD_ERROR", e.message, e) }
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
AsyncFunction("smbClientUpload") { clientId: String, localPath: String, remotePath: String, promise: Promise ->
|
|
645
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
646
|
+
try {
|
|
647
|
+
val client = smbClients[clientId]
|
|
648
|
+
?: throw Exception("SMB client with ID '$clientId' not found")
|
|
649
|
+
client.upload(localPath, remotePath) { transferred, total ->
|
|
650
|
+
val percentage = if (total > 0) ((transferred * 100) / total).toInt() else 0
|
|
651
|
+
sendEvent("onSmbProgress", mapOf(
|
|
652
|
+
"clientId" to clientId,
|
|
653
|
+
"transferred" to transferred,
|
|
654
|
+
"total" to total,
|
|
655
|
+
"percentage" to percentage
|
|
656
|
+
))
|
|
657
|
+
}
|
|
658
|
+
sendEvent("onSmbComplete", mapOf("clientId" to clientId))
|
|
659
|
+
withContext(Dispatchers.Main) { promise.resolve(null) }
|
|
660
|
+
} catch (e: Exception) {
|
|
661
|
+
sendEvent("onSmbError", mapOf(
|
|
662
|
+
"clientId" to clientId,
|
|
663
|
+
"error" to (e.message ?: "SMB upload error")
|
|
664
|
+
))
|
|
665
|
+
withContext(Dispatchers.Main) { promise.reject("SMB_CLIENT_UPLOAD_ERROR", e.message, e) }
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
AsyncFunction("smbClientDelete") { clientId: String, remotePath: String, isDirectory: Boolean?, promise: Promise ->
|
|
671
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
672
|
+
try {
|
|
673
|
+
val client = smbClients[clientId]
|
|
674
|
+
?: throw Exception("SMB client with ID '$clientId' not found")
|
|
675
|
+
client.delete(remotePath, isDirectory ?: false)
|
|
676
|
+
withContext(Dispatchers.Main) { promise.resolve(null) }
|
|
677
|
+
} catch (e: Exception) {
|
|
678
|
+
withContext(Dispatchers.Main) { promise.reject("SMB_CLIENT_DELETE_ERROR", e.message, e) }
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
AsyncFunction("smbClientCreateDirectory") { clientId: String, remotePath: String, promise: Promise ->
|
|
684
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
685
|
+
try {
|
|
686
|
+
val client = smbClients[clientId]
|
|
687
|
+
?: throw Exception("SMB client with ID '$clientId' not found")
|
|
688
|
+
client.mkdir(remotePath)
|
|
689
|
+
withContext(Dispatchers.Main) { promise.resolve(null) }
|
|
690
|
+
} catch (e: Exception) {
|
|
691
|
+
withContext(Dispatchers.Main) { promise.reject("SMB_CLIENT_CREATE_DIR_ERROR", e.message, e) }
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
473
696
|
// Enables the module to be used as a native view. Definition components that are accepted as part of
|
|
474
697
|
// the view definition: Prop, Events.
|
|
475
698
|
View(ReactNativeKookitView::class) {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
package expo.modules.kookit
|
|
2
|
+
|
|
3
|
+
import com.hierynomus.msdtyp.AccessMask
|
|
4
|
+
import com.hierynomus.msfscc.fileinformation.FileIdBothDirectoryInformation
|
|
5
|
+
import com.hierynomus.msfscc.FileAttributes
|
|
6
|
+
import com.hierynomus.mssmb2.SMB2CreateDisposition
|
|
7
|
+
import com.hierynomus.mssmb2.SMB2ShareAccess
|
|
8
|
+
import com.hierynomus.smbj.SMBClient
|
|
9
|
+
import com.hierynomus.smbj.auth.AuthenticationContext
|
|
10
|
+
import com.hierynomus.smbj.connection.Connection
|
|
11
|
+
import com.hierynomus.smbj.session.Session
|
|
12
|
+
import com.hierynomus.smbj.share.DiskShare
|
|
13
|
+
import com.hierynomus.smbj.share.File
|
|
14
|
+
import kotlinx.coroutines.Dispatchers
|
|
15
|
+
import kotlinx.coroutines.withContext
|
|
16
|
+
import java.io.FileOutputStream
|
|
17
|
+
import java.io.FileInputStream
|
|
18
|
+
import java.io.IOException
|
|
19
|
+
|
|
20
|
+
data class SmbConnectionConfig(
|
|
21
|
+
val host: String,
|
|
22
|
+
val port: Int = 445,
|
|
23
|
+
val username: String,
|
|
24
|
+
val password: String,
|
|
25
|
+
val domain: String? = null,
|
|
26
|
+
val share: String? = null,
|
|
27
|
+
val timeoutMs: Int = 10000
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
data class SmbFileInfo(
|
|
31
|
+
val name: String,
|
|
32
|
+
val isDirectory: Boolean,
|
|
33
|
+
val size: Long,
|
|
34
|
+
val lastModified: Long,
|
|
35
|
+
val attributes: String? = null
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
class SmbClient {
|
|
39
|
+
private val smb = SMBClient()
|
|
40
|
+
private var connection: Connection? = null
|
|
41
|
+
private var session: Session? = null
|
|
42
|
+
private var share: DiskShare? = null
|
|
43
|
+
private var connected = false
|
|
44
|
+
|
|
45
|
+
@Synchronized
|
|
46
|
+
@Throws(IOException::class)
|
|
47
|
+
fun connect(config: SmbConnectionConfig) {
|
|
48
|
+
disconnect()
|
|
49
|
+
val conn = smb.connect(config.host)
|
|
50
|
+
connection = conn
|
|
51
|
+
val auth = AuthenticationContext(
|
|
52
|
+
config.username,
|
|
53
|
+
config.password.toCharArray(),
|
|
54
|
+
config.domain
|
|
55
|
+
)
|
|
56
|
+
session = conn.authenticate(auth)
|
|
57
|
+
connected = true
|
|
58
|
+
if (!config.share.isNullOrBlank()) {
|
|
59
|
+
share = session!!.connectShare(config.share) as DiskShare
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Synchronized
|
|
64
|
+
fun openShare(shareName: String) {
|
|
65
|
+
val s = session ?: throw IOException("Not authenticated")
|
|
66
|
+
share?.close()
|
|
67
|
+
share = s.connectShare(shareName) as DiskShare
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Synchronized
|
|
71
|
+
fun disconnect() {
|
|
72
|
+
try { share?.close() } catch (_: Exception) {}
|
|
73
|
+
try { session?.logoff() } catch (_: Exception) {}
|
|
74
|
+
try { connection?.close() } catch (_: Exception) {}
|
|
75
|
+
share = null
|
|
76
|
+
session = null
|
|
77
|
+
connection = null
|
|
78
|
+
connected = false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Synchronized
|
|
82
|
+
fun isConnected(): Boolean = connected
|
|
83
|
+
|
|
84
|
+
private fun requireShare(): DiskShare {
|
|
85
|
+
return share ?: throw IOException("No share is opened. Call connect with share or openShare().")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
suspend fun list(path: String?): List<SmbFileInfo> = withContext(Dispatchers.IO) {
|
|
89
|
+
val disk = requireShare()
|
|
90
|
+
val dirPath = path?.trim()?.ifEmpty { null } ?: ""
|
|
91
|
+
val results = mutableListOf<SmbFileInfo>()
|
|
92
|
+
for (info in disk.list(dirPath)) {
|
|
93
|
+
val name = info.fileName
|
|
94
|
+
if (name == "." || name == "..") continue
|
|
95
|
+
val isDir = info.fileAttributes and FileAttributes.FILE_ATTRIBUTE_DIRECTORY.value != 0L
|
|
96
|
+
val size = if (isDir) 0L else (info as? FileIdBothDirectoryInformation)?.endOfFile ?: 0L
|
|
97
|
+
val lastModified = (info as? FileIdBothDirectoryInformation)?.lastWriteTime?.toEpochMillis() ?: 0L
|
|
98
|
+
val attrs = info.fileAttributes.toString()
|
|
99
|
+
results.add(SmbFileInfo(name, isDir, size, lastModified, attrs))
|
|
100
|
+
}
|
|
101
|
+
results
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
suspend fun download(remotePath: String, localPath: String, onProgress: ((Long, Long) -> Unit)? = null) = withContext(Dispatchers.IO) {
|
|
105
|
+
val disk = requireShare()
|
|
106
|
+
java.io.File(localPath).parentFile?.mkdirs()
|
|
107
|
+
disk.openFile(
|
|
108
|
+
remotePath,
|
|
109
|
+
setOf(AccessMask.FILE_READ_DATA),
|
|
110
|
+
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
|
|
111
|
+
SMB2ShareAccess.ALL,
|
|
112
|
+
SMB2CreateDisposition.FILE_OPEN,
|
|
113
|
+
null
|
|
114
|
+
).use { f: File ->
|
|
115
|
+
FileOutputStream(localPath).use { out ->
|
|
116
|
+
val buffer = ByteArray(8192)
|
|
117
|
+
var total = 0L
|
|
118
|
+
var read: Int
|
|
119
|
+
val fileSize = f.fileInformation.standardInformation.endOfFile
|
|
120
|
+
while (true) {
|
|
121
|
+
read = f.read(buffer, total)
|
|
122
|
+
if (read <= 0) break
|
|
123
|
+
out.write(buffer, 0, read)
|
|
124
|
+
total += read
|
|
125
|
+
onProgress?.invoke(total, fileSize)
|
|
126
|
+
}
|
|
127
|
+
out.flush()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
suspend fun upload(localPath: String, remotePath: String, onProgress: ((Long, Long) -> Unit)? = null) = withContext(Dispatchers.IO) {
|
|
133
|
+
val disk = requireShare()
|
|
134
|
+
val file = java.io.File(localPath)
|
|
135
|
+
val size = file.length()
|
|
136
|
+
file.inputStream().use { input ->
|
|
137
|
+
disk.openFile(
|
|
138
|
+
remotePath,
|
|
139
|
+
setOf(AccessMask.FILE_WRITE_DATA),
|
|
140
|
+
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
|
|
141
|
+
SMB2ShareAccess.ALL,
|
|
142
|
+
SMB2CreateDisposition.FILE_OVERWRITE_IF,
|
|
143
|
+
null
|
|
144
|
+
).use { f: File ->
|
|
145
|
+
val buffer = ByteArray(8192)
|
|
146
|
+
var offset = 0L
|
|
147
|
+
var read: Int
|
|
148
|
+
while (input.read(buffer).also { read = it } > 0) {
|
|
149
|
+
f.write(buffer, offset, 0, read)
|
|
150
|
+
offset += read
|
|
151
|
+
onProgress?.invoke(offset, size)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
suspend fun delete(path: String, isDirectory: Boolean = false) = withContext(Dispatchers.IO) {
|
|
158
|
+
val disk = requireShare()
|
|
159
|
+
if (isDirectory) {
|
|
160
|
+
disk.rmdir(path, false)
|
|
161
|
+
} else {
|
|
162
|
+
disk.rm(path)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
suspend fun mkdir(path: String) = withContext(Dispatchers.IO) {
|
|
167
|
+
val disk = requireShare()
|
|
168
|
+
if (!disk.folderExists(path)) {
|
|
169
|
+
disk.mkdir(path)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|