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/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 许可证
@@ -41,3 +41,10 @@ android {
41
41
  abortOnError false
42
42
  }
43
43
  }
44
+
45
+ dependencies {
46
+ // SMB client library
47
+ implementation 'com.hierynomus:smbj:0.13.0'
48
+ // Suppress SLF4J binding warnings
49
+ implementation 'org.slf4j:slf4j-nop:2.0.13'
50
+ }
@@ -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("onChange", "onVolumeButtonPressed", "onFtpProgress", "onFtpComplete", "onFtpError")
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
+