cj-tauri 0.8.0 → 1.1.0
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/README.md +3 -3
- package/cjpm.toml +52 -25
- package/cli/cjpm.toml +1 -1
- package/cli/src/doctor.cj +304 -0
- package/cli/src/main.cj +33 -7
- package/cli/src/project.cj +59 -1
- package/cli/src/resolve.cj +348 -304
- package/cli/src/scaffold.cj +139 -22
- package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/README.md +79 -0
- package/cli/templates/ohos-pc/build-hap.sh +190 -0
- package/cli/templates/ohos-pc/build-profile.json5 +46 -0
- package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
- package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
- package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
- package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
- package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
- package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/oh-package.json5 +10 -0
- package/native/build_linux.sh +30 -2
- package/native/build_win.bat +62 -61
- package/native/core/bridge_core.c +1626 -0
- package/native/{bridge_core.h → core/bridge_core.h} +246 -6
- package/native/platforms/harmonyos/README.md +106 -0
- package/native/platforms/linux/bridge_linux.c +2485 -0
- package/native/platforms/windows/bridge_win.c +3248 -0
- package/native/tests/test_bridge_core.c +528 -1
- package/package.json +12 -3
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/api_system.cj +10 -2
- package/src/app.cj +248 -85
- package/src/arkweb_proxy.cj +56 -0
- package/src/arkweb_script.cj +154 -0
- package/src/capability.cj +140 -0
- package/src/clipboard_bridge.cj +55 -0
- package/src/dragdrop.cj +26 -0
- package/src/embedded_assets.cj +98 -0
- package/src/hexutil.cj +53 -0
- package/src/host.cj +197 -26
- package/src/host_arkweb.cj +740 -0
- package/src/host_factory.cj +48 -0
- package/src/host_webkit.cj +202 -26
- package/src/host_webview2.cj +176 -3
- package/src/hotkey.cj +66 -0
- package/src/ipc_hub.cj +49 -0
- package/src/ipc_message.cj +2 -1
- package/src/jsutil.cj +33 -0
- package/src/log_bridge.cj +107 -0
- package/src/logging.cj +127 -0
- package/src/menu.cj +197 -196
- package/src/plugin.cj +40 -0
- package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
- package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
- package/src/plugins/fs/plugin_fs.cj +194 -0
- package/src/plugins/http/plugin_http.cj +459 -0
- package/src/plugins/log/plugin_log.cj +105 -0
- package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
- package/src/plugins/mqtt/mqtt_codec.cj +661 -0
- package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
- package/src/plugins/notification/plugin_notification.cj +146 -0
- package/src/plugins/plugins.cj +5 -0
- package/src/plugins/redis/plugin_redis.cj +2409 -0
- package/src/plugins/redis/redis_codec.cj +556 -0
- package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
- package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
- package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
- package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
- package/src/plugins/store/plugin_store.cj +287 -0
- package/src/plugins/ws/plugin_ws.cj +470 -0
- package/src/process_hidden.cj +174 -0
- package/src/scope.cj +391 -0
- package/src/shell_event.cj +69 -68
- package/src/tests/capability_test.cj +53 -0
- package/src/tests/fake_host.cj +85 -20
- package/src/tests/ipc_hub_test.cj +13 -0
- package/src/tests/jsutil_test.cj +62 -0
- package/src/tests/logging_test.cj +62 -0
- package/src/tests/mqtt_codec_test.cj +405 -0
- package/src/tests/plugin_clipboard_test.cj +222 -0
- package/src/tests/plugin_dialog_test.cj +98 -4
- package/src/tests/plugin_http_test.cj +124 -0
- package/src/tests/plugin_menu_test.cj +1 -0
- package/src/tests/plugin_notification_test.cj +205 -0
- package/src/tests/plugin_serial_test.cj +269 -267
- package/src/tests/plugin_shell_test.cj +189 -188
- package/src/tests/plugin_sqlite_test.cj +230 -0
- package/src/tests/plugin_store_test.cj +260 -0
- package/src/tests/plugin_test.cj +542 -524
- package/src/tests/plugin_ws_test.cj +40 -0
- package/src/tests/process_hidden_test.cj +129 -0
- package/src/tests/redis_codec_test.cj +396 -0
- package/src/tests/scope_test.cj +350 -0
- package/src/tests/shell_event_test.cj +0 -1
- package/src/tests/system_host_test.cj +81 -79
- package/src/tests/urlutil_test.cj +53 -0
- package/src/tests/version_test.cj +75 -65
- package/src/tray.cj +90 -0
- package/src/urlutil.cj +159 -0
- package/src/version.cj +23 -2
- package/src/window.cj +51 -42
- package/native/bridge_core.c +0 -797
- package/native/bridge_linux.c +0 -1268
- package/native/bridge_win.c +0 -1633
- package/src/plugin_fs.cj +0 -118
- /package/native/{bridge_js.h → core/bridge_js.h} +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
package cjTauri
|
|
1
|
+
package cjTauri.plugins.dialog
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
2
4
|
|
|
3
5
|
import std.collection.*
|
|
4
6
|
import stdx.encoding.json.*
|
|
@@ -6,18 +8,19 @@ import stdx.encoding.json.*
|
|
|
6
8
|
/**
|
|
7
9
|
* 官方插件 dialog:原生对话框(对标 Tauri 的 dialog 插件)
|
|
8
10
|
*
|
|
9
|
-
* 命令:dialog:open / dialog:save / dialog:message
|
|
11
|
+
* 命令:dialog:open / dialog:save / dialog:pickFolder / dialog:message
|
|
10
12
|
* 权限:**不自动放行**,应用要在 capabilities 里显式声明,两种写法等价:
|
|
11
13
|
* "commands": ["dialog:open", "dialog:message"] // 明文逐条
|
|
12
14
|
* "permissions": ["dialog:default"] // 命名权限集(v1.1,一次放行一组)
|
|
13
15
|
* 前端:window.__CJ_TAURI__.dialog.open({ filter: "文本文件|*.txt|所有文件|*.*" })
|
|
16
|
+
* window.__CJ_TAURI__.dialog.pickFolder({ title: "选择目录" }) // 选目录(返回选中目录的绝对路径)
|
|
14
17
|
*
|
|
15
18
|
* 与 fs 插件的差别:这里要落到**系统对话框**,所以弹窗归宿主层(Windows 走 Win32 通用对话框、
|
|
16
19
|
* Linux 走 GTK),插件只做参数校验与命令分发——平台差异不进插件,与 AGENTS.md §2 的架构契约一致。
|
|
17
20
|
* 对话框需要父窗口,故必须等应用启动(start 之后)再调用;更早调用得到的是空结果(取消)。
|
|
18
21
|
*/
|
|
19
22
|
public class DialogPlugin <: Plugin {
|
|
20
|
-
//
|
|
23
|
+
// 四个命令共用一个处理器实例:setup() 里把宿主交给它(装配必须在 run()/runUrl() 之前完成)
|
|
21
24
|
private let handler: DialogCommands = DialogCommands()
|
|
22
25
|
|
|
23
26
|
public init() {}
|
|
@@ -30,23 +33,27 @@ public class DialogPlugin <: Plugin {
|
|
|
30
33
|
let m = HashMap<String, CommandHandler>()
|
|
31
34
|
m.add("open", this.handler)
|
|
32
35
|
m.add("save", this.handler)
|
|
36
|
+
m.add("pickFolder", this.handler)
|
|
33
37
|
m.add("message", this.handler)
|
|
34
38
|
return m
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
|
-
* 命名权限集(v1.1):files
|
|
39
|
-
*
|
|
42
|
+
* 命名权限集(v1.1):files 放行三个「挑路径」命令(open / save / pickFolder),
|
|
43
|
+
* message(模态提示框)不在其中。只想让用户挑个文件或目录的应用引用 files 即可,
|
|
44
|
+
* 不必顺带拿到「能弹任意提示框」的能力。
|
|
40
45
|
*/
|
|
41
46
|
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
42
47
|
let m = HashMap<String, ArrayList<String>>()
|
|
43
48
|
let files = ArrayList<String>()
|
|
44
49
|
files.add("open")
|
|
45
50
|
files.add("save")
|
|
51
|
+
files.add("pickFolder")
|
|
46
52
|
m.add("files", files)
|
|
47
53
|
let all = ArrayList<String>()
|
|
48
54
|
all.add("open")
|
|
49
55
|
all.add("save")
|
|
56
|
+
all.add("pickFolder")
|
|
50
57
|
all.add("message")
|
|
51
58
|
m.add("default", all)
|
|
52
59
|
return m
|
|
@@ -60,6 +67,7 @@ public class DialogPlugin <: Plugin {
|
|
|
60
67
|
" t.dialog = {" +
|
|
61
68
|
" open: function (args) { return window.__CJ_TAURI__.invoke('dialog:open', args); }," +
|
|
62
69
|
" save: function (args) { return window.__CJ_TAURI__.invoke('dialog:save', args); }," +
|
|
70
|
+
" pickFolder: function (args) { return window.__CJ_TAURI__.invoke('dialog:pickFolder', args); }," +
|
|
63
71
|
" message: function (args) { return window.__CJ_TAURI__.invoke('dialog:message', args); }" +
|
|
64
72
|
" }; })();\n"
|
|
65
73
|
}
|
|
@@ -67,37 +75,77 @@ public class DialogPlugin <: Plugin {
|
|
|
67
75
|
/**
|
|
68
76
|
* 装配宿主:命令要靠它弹原生对话框。
|
|
69
77
|
* 这里只是把宿主对象记下来(宿主在构造 TauriApp 时就存在),真正弹窗要等 start 之后。
|
|
78
|
+
*
|
|
79
|
+
* **两个槽都装**:`app.webviewHost()` 只是默认窗那一个,多窗口下拿它等于
|
|
80
|
+
* 「谁先装配谁收所有弹窗」(第二扇窗的框排进默认窗队列,实测见 docs/鸿蒙ArkWeb接入踩坑.md §10.12);
|
|
81
|
+
* `app.hostOf` 才是按报文里的窗口取正确宿主的那条路,命令执行时用。
|
|
70
82
|
*/
|
|
71
83
|
public func setup(app: TauriApp): Unit {
|
|
72
84
|
this.handler.attachHost(app.webviewHost())
|
|
85
|
+
this.handler.attachHostResolver({ label => app.hostOf(label) })
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
/**
|
|
77
90
|
* dialog 插件的命令处理器(三个命令共用一份,按全名分派,与内置 SystemCommands 同形)
|
|
78
91
|
*/
|
|
79
|
-
|
|
92
|
+
// public 是给单测用的(跨包要能直接构造命令处理器,见 AGENTS §2「单测要直接构造的命令 handler 须 public」)
|
|
93
|
+
public class DialogCommands <: CommandHandler {
|
|
80
94
|
// 宿主层(弹原生对话框);未经 DialogPlugin 装配时为 None,命令给出可读的错误而不是崩
|
|
81
95
|
private var host: Option<WebViewHost> = None
|
|
82
96
|
|
|
97
|
+
/* 按 label 解析宿主(多窗口的正路):单槽 `host` 只记着**装配时那一个**宿主,
|
|
98
|
+
多窗口下用它就是「谁先装配谁收所有弹窗」——2026-10-20 鸿蒙真机实测:两扇窗同时弹框,
|
|
99
|
+
hilog 两次 `dialog take label=main`,第二次的 title 却是 second,且 main 的 @State
|
|
100
|
+
被覆盖成对面内容、第二窗自己的框从未显示(判据与过程见 docs/鸿蒙ArkWeb接入踩坑.md §10.12)。
|
|
101
|
+
单测也靠它注入桩宿主,不必构造真实 TauriApp。 */
|
|
102
|
+
private var hostByWindow: Option<(String) -> Option<WebViewHost>> = None
|
|
103
|
+
|
|
83
104
|
public init() {}
|
|
84
105
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
106
|
+
/**
|
|
107
|
+
* 装配**单个**宿主(单窗口 / 单测用)
|
|
108
|
+
*
|
|
109
|
+
* 多窗口一律走 `attachHostResolver`:这个槽不带窗口身份,第二扇窗的命令会落到这里那一个宿主上。
|
|
110
|
+
* 两者同时装时可以共存——解析器优先,`host` 退成兜底(解析器给出 None 时才用)。
|
|
111
|
+
*/
|
|
112
|
+
public func attachHost(h: WebViewHost): Unit {
|
|
87
113
|
this.host = Some(h)
|
|
88
114
|
}
|
|
89
115
|
|
|
116
|
+
/** 装配「label → 宿主」解析器;由 DialogPlugin.setup() 传入 `app.hostOf` */
|
|
117
|
+
public func attachHostResolver(resolve: (String) -> Option<WebViewHost>): Unit {
|
|
118
|
+
this.hostByWindow = Some(resolve)
|
|
119
|
+
}
|
|
120
|
+
|
|
90
121
|
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
91
|
-
let _ = ipc
|
|
92
122
|
match (cmd) {
|
|
93
|
-
case "dialog:open" => JsonString(this.pickFile(args, false))
|
|
94
|
-
case "dialog:save" => JsonString(this.pickFile(args, true))
|
|
95
|
-
case "dialog:
|
|
123
|
+
case "dialog:open" => JsonString(this.pickFile(args, false, ipc))
|
|
124
|
+
case "dialog:save" => JsonString(this.pickFile(args, true, ipc))
|
|
125
|
+
case "dialog:pickFolder" => JsonString(this.pickFolder(args, ipc))
|
|
126
|
+
case "dialog:message" => JsonBool(this.message(args, ipc))
|
|
96
127
|
case _ => throw CommandException("unknown dialog command: ${cmd}")
|
|
97
128
|
}
|
|
98
129
|
}
|
|
99
130
|
|
|
100
|
-
|
|
131
|
+
/**
|
|
132
|
+
* 取**发起这条命令的窗口**的宿主
|
|
133
|
+
*
|
|
134
|
+
* 多窗口下必须按 `ipc.window` 解析:用装配时那一个宿主,第二扇窗的弹窗会排进默认窗的队列,
|
|
135
|
+
* 表象是「另一扇窗里冒出别人的框」外加「自己那个框永远不出现」。
|
|
136
|
+
* 解析器在场而窗口没登记时**如实报错**,不要悄悄回落到单槽——那正是串台的来源。
|
|
137
|
+
*/
|
|
138
|
+
private func requireHost(ipc: IpcContext): WebViewHost {
|
|
139
|
+
match (this.hostByWindow) {
|
|
140
|
+
case Some(resolve) => match (resolve(ipc.window)) {
|
|
141
|
+
case Some(h) => return h
|
|
142
|
+
// 解析器在场=多窗口正路:这里回落到单槽就是「弹到别人窗口里」,
|
|
143
|
+
// 所以宁可不弹,把窗口 label 原样报出去(少一个框好过串台)。
|
|
144
|
+
case None => throw CommandException(
|
|
145
|
+
"dialog: 窗口 \"${ipc.window}\" 没有登记宿主(addWindow(); 默认窗在 run() 之前也可用)")
|
|
146
|
+
}
|
|
147
|
+
case None => ()
|
|
148
|
+
}
|
|
101
149
|
match (this.host) {
|
|
102
150
|
case Some(h) => h
|
|
103
151
|
case None => throw CommandException(
|
|
@@ -127,7 +175,7 @@ class DialogCommands <: CommandHandler {
|
|
|
127
175
|
}
|
|
128
176
|
}
|
|
129
177
|
|
|
130
|
-
private func pickFile(args: JsonObject, save: Bool): String {
|
|
178
|
+
private func pickFile(args: JsonObject, save: Bool, ipc: IpcContext): String {
|
|
131
179
|
var title = this.optionalString(args, "title", "")
|
|
132
180
|
if (title.size == 0) {
|
|
133
181
|
if (save) {
|
|
@@ -137,16 +185,25 @@ class DialogCommands <: CommandHandler {
|
|
|
137
185
|
}
|
|
138
186
|
}
|
|
139
187
|
let filter = this.optionalString(args, "filter", "")
|
|
140
|
-
return this.requireHost().showFileDialog(save, title, filter)
|
|
188
|
+
return this.requireHost(ipc).showFileDialog(save, title, filter)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 选目录(宿主侧 kind=6):只有 title 一个可选参数;取消回空串,与文件框同语义 */
|
|
192
|
+
private func pickFolder(args: JsonObject, ipc: IpcContext): String {
|
|
193
|
+
var title = this.optionalString(args, "title", "")
|
|
194
|
+
if (title.size == 0) {
|
|
195
|
+
title = "选择文件夹"
|
|
196
|
+
}
|
|
197
|
+
return this.requireHost(ipc).showFolderDialog(title)
|
|
141
198
|
}
|
|
142
199
|
|
|
143
|
-
private func message(args: JsonObject): Bool {
|
|
200
|
+
private func message(args: JsonObject, ipc: IpcContext): Bool {
|
|
144
201
|
let text = this.requireString(args, "message")
|
|
145
202
|
let kind = this.optionalString(args, "kind", "info")
|
|
146
203
|
var title = this.optionalString(args, "title", "")
|
|
147
204
|
if (title.size == 0) {
|
|
148
205
|
title = "提示"
|
|
149
206
|
}
|
|
150
|
-
return this.requireHost().showMessageDialog(kind, title, text)
|
|
207
|
+
return this.requireHost(ipc).showMessageDialog(kind, title, text)
|
|
151
208
|
}
|
|
152
209
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
package cjTauri.plugins.fs
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import std.fs.*
|
|
7
|
+
import stdx.encoding.json.*
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 官方插件 fs:文件读写(纯仓颉,零 C 桥改动,RFC-001 §7.3)
|
|
11
|
+
*
|
|
12
|
+
* 命令:fs:readText / fs:writeText / fs:exists / fs:list / fs:remove
|
|
13
|
+
* - readText / writeText:整文件读写(writeText 是覆盖写:已存在会截断,不存在则创建)
|
|
14
|
+
* - exists:路径是否存在
|
|
15
|
+
* - list:**平铺一层**列目录,返回 [{ name, dir, size }];不递归,顺序就是文件系统给的顺序(展示排序交给前端)
|
|
16
|
+
* - remove:只删**单个文件**,目标是目录一律拒——std.fs 的 remove 能把**空目录**也删掉,
|
|
17
|
+
* 所以这层挡在调用前(不做递归删除);文件不存在同样报错:调用方本想删的东西没删掉,
|
|
18
|
+
* 静默回 true 等于把失败伪装成成功
|
|
19
|
+
* 权限:**不自动放行**,应用要在 capabilities 里显式声明,两种写法等价:
|
|
20
|
+
* "commands": ["fs:readText", "fs:exists"] // 明文逐条(旧写法,一直支持)
|
|
21
|
+
* "permissions": ["fs:readonly"] // 命名权限集(v1.1,一次放行一组)
|
|
22
|
+
* 路径:`scopeAware()` 为真——页面给的 path 一律经 IpcContext.resolveScopedPath 校验;
|
|
23
|
+
* 清单里写了 "scopes": { "fs": { "allow": [...], "deny": [...] } } 就按白名单判定(越界即拒),
|
|
24
|
+
* 没写就维持旧行为(全量放行,启动时打一行 stderr 提示)。见 docs/RFC-插件路线图.md 第 4 期。
|
|
25
|
+
* 前端:window.__CJ_TAURI__.fs.readText({ path: "notes.txt" })
|
|
26
|
+
*/
|
|
27
|
+
public class FsPlugin <: Plugin {
|
|
28
|
+
public init() {}
|
|
29
|
+
|
|
30
|
+
public func name(): String {
|
|
31
|
+
return "fs"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 本插件的路径参数吃 scope 校验(RFC-003 第 4 期):唯一带任意路径的官方插件 */
|
|
35
|
+
public func scopeAware(): Bool {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
40
|
+
let m = HashMap<String, CommandHandler>()
|
|
41
|
+
m.add("readText", FsCommands())
|
|
42
|
+
m.add("writeText", FsCommands())
|
|
43
|
+
m.add("exists", FsCommands())
|
|
44
|
+
m.add("list", FsCommands())
|
|
45
|
+
m.add("remove", FsCommands())
|
|
46
|
+
return m
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 命名权限集(v1.1,对标 Tauri v2 的 fs:default / fs:allow-read-text-file)。
|
|
51
|
+
*
|
|
52
|
+
* readonly 只含读相关两条:只想读文件的应用引用它即可,**写能力不会被顺带打开**;
|
|
53
|
+
* 要写文件必须显式引用 default(或把 fs:writeText 写成明文)——最小权限不打折。
|
|
54
|
+
* 集内写短名(readText),框架自动补成 fs:readText。
|
|
55
|
+
*/
|
|
56
|
+
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
57
|
+
let m = HashMap<String, ArrayList<String>>()
|
|
58
|
+
let ro = ArrayList<String>()
|
|
59
|
+
ro.add("readText")
|
|
60
|
+
ro.add("exists")
|
|
61
|
+
ro.add("list") // 列目录也是读操作:只读应用(如文件浏览)要能拿它,但拿不到 remove
|
|
62
|
+
m.add("readonly", ro)
|
|
63
|
+
let all = ArrayList<String>()
|
|
64
|
+
all.add("readText")
|
|
65
|
+
all.add("writeText")
|
|
66
|
+
all.add("exists")
|
|
67
|
+
all.add("list")
|
|
68
|
+
all.add("remove")
|
|
69
|
+
m.add("default", all)
|
|
70
|
+
return m
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 前端 shim:往桥对象上挂一个 fs 命名空间。
|
|
75
|
+
* 只动 __CJ_TAURI__ 的一个命名空间,不碰 invoke / listen / emit,也不注册全局变量。
|
|
76
|
+
*/
|
|
77
|
+
public func jsShim(): String {
|
|
78
|
+
return "(function () { var t = window.__CJ_TAURI__ || (window.__CJ_TAURI__ = {});" +
|
|
79
|
+
" t.fs = {" +
|
|
80
|
+
" readText: function (args) { return window.__CJ_TAURI__.invoke('fs:readText', args); }," +
|
|
81
|
+
" writeText: function (args) { return window.__CJ_TAURI__.invoke('fs:writeText', args); }," +
|
|
82
|
+
" exists: function (args) { return window.__CJ_TAURI__.invoke('fs:exists', args); }," +
|
|
83
|
+
" list: function (args) { return window.__CJ_TAURI__.invoke('fs:list', args); }," +
|
|
84
|
+
" remove: function (args) { return window.__CJ_TAURI__.invoke('fs:remove', args); }" +
|
|
85
|
+
" }; })();\n"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* fs 插件的命令处理器(三个命令共用一份,按全名分派,与内置 SystemCommands 同形)
|
|
91
|
+
*/
|
|
92
|
+
class FsCommands <: CommandHandler {
|
|
93
|
+
public init() {}
|
|
94
|
+
|
|
95
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
96
|
+
match (cmd) {
|
|
97
|
+
case "fs:readText" => this.readText(args, ipc)
|
|
98
|
+
case "fs:writeText" => this.writeText(args, ipc)
|
|
99
|
+
case "fs:exists" => this.exists(args, ipc)
|
|
100
|
+
case "fs:list" => this.listEntries(args, ipc)
|
|
101
|
+
case "fs:remove" => this.removeFile(args, ipc)
|
|
102
|
+
case _ => throw CommandException("unknown fs command: ${cmd}")
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 取路径参数并过 scope 校验:越界在**这里**就抛(前端拿到可读错误),
|
|
108
|
+
* 命令体拿到的一定是已经规范化、且落在白名单里的路径。
|
|
109
|
+
*/
|
|
110
|
+
private func scopedPath(args: JsonObject, ipc: IpcContext): String {
|
|
111
|
+
return ipc.resolveScopedPath("fs", this.requireString(args, "path"))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 取必需的字符串参数;缺失或类型不对时抛业务错误,前端拿到的是可读原文 */
|
|
115
|
+
private func requireString(args: JsonObject, key: String): String {
|
|
116
|
+
match (args.get(key)) {
|
|
117
|
+
case Some(v) => match (v.kind()) {
|
|
118
|
+
case JsonKind.JsString => return v.asString().getValue()
|
|
119
|
+
case _ => throw CommandException("fs: argument \"${key}\" must be a string")
|
|
120
|
+
}
|
|
121
|
+
case None => throw CommandException("fs: missing argument \"${key}\"")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func readText(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
126
|
+
let path = this.scopedPath(args, ipc)
|
|
127
|
+
try {
|
|
128
|
+
return JsonString(String.fromUtf8(File.readFrom(path)))
|
|
129
|
+
} catch (e: Exception) {
|
|
130
|
+
throw CommandException("fs:readText failed: ${e}")
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func writeText(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
135
|
+
let path = this.scopedPath(args, ipc)
|
|
136
|
+
let contents = this.requireString(args, "contents")
|
|
137
|
+
try {
|
|
138
|
+
// File.writeTo 是覆盖写:文件已存在会截断,不存在则创建——不需要先删(与 File.create 不同)
|
|
139
|
+
File.writeTo(path, contents.toArray())
|
|
140
|
+
return JsonBool(true)
|
|
141
|
+
} catch (e: Exception) {
|
|
142
|
+
throw CommandException("fs:writeText failed: ${e}")
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private func exists(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
147
|
+
let path = this.scopedPath(args, ipc)
|
|
148
|
+
return JsonBool(exists(path))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 列目录:**平铺一层**、不递归——递归遍历是应用层的事,插件契约越薄越不容易被绕过。
|
|
153
|
+
* name 直接取 FileInfo.name(等价于 path.fileName),目录项也照报(dir=true),显不显示由页面定。
|
|
154
|
+
* 顺序不做保证:仓颉侧不排序(`ArrayList.sort` 已废弃),要稳定顺序在前端排。
|
|
155
|
+
*/
|
|
156
|
+
private func listEntries(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
157
|
+
let path = this.scopedPath(args, ipc)
|
|
158
|
+
var entries: Array<FileInfo>
|
|
159
|
+
try {
|
|
160
|
+
entries = Directory.readFrom(path)
|
|
161
|
+
} catch (e: Exception) {
|
|
162
|
+
throw CommandException("fs:list failed: ${e}")
|
|
163
|
+
}
|
|
164
|
+
let items = ArrayList<JsonValue>()
|
|
165
|
+
for (info in entries) {
|
|
166
|
+
let obj = JsonObject()
|
|
167
|
+
obj.put("name", JsonString(info.name))
|
|
168
|
+
obj.put("dir", JsonBool(info.isDirectory()))
|
|
169
|
+
obj.put("size", JsonInt(info.size))
|
|
170
|
+
items.add(obj)
|
|
171
|
+
}
|
|
172
|
+
return JsonArray(items)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 删单个文件。判定顺序不能反:**先确认存在、再挡住目录、最后才 remove**。
|
|
177
|
+
* 漏掉中间那步,一个空目录会被 std.fs 的 remove 静默删掉(它同时管文件和目录)。
|
|
178
|
+
*/
|
|
179
|
+
private func removeFile(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
180
|
+
let path = this.scopedPath(args, ipc)
|
|
181
|
+
if (!exists(path)) {
|
|
182
|
+
throw CommandException("fs:remove failed: no such file: ${path}")
|
|
183
|
+
}
|
|
184
|
+
if (FileInfo(path).isDirectory()) {
|
|
185
|
+
throw CommandException("fs:remove refused: is a directory: ${path}")
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
remove(path)
|
|
189
|
+
return JsonBool(true)
|
|
190
|
+
} catch (e: Exception) {
|
|
191
|
+
throw CommandException("fs:remove failed: ${e}")
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|