CH-J Toolsإدارة الخوادم عبر SSH
القائمة
المصدر المنشور

CH-J Tools

تصفّح الأدلة والملفات الخاصة بإصدار محدد من التطبيق.

تنزيل الشيفرة المصدرية بصيغة ZIP
CH-J / Server Manager 1704 ملفات
0.1.2-beta تاريخ الإضافة: · تاريخ البناء:
module_api.rs تاريخ الإضافة: لم يُسجَّل التاريخ ٥٢٫٢ KB · 609 أسطر
الشيفرة تنزيل الملف
1//! Public Module API commands (Plugin API 1.7.0 compatible). Every command checks the
2//! calling module window and its manifest permission before touching a Core service.
4use super::{module_call, module_result, text, CommandResult};
5use crate::core::app::CoreSlot;
6use crate::core::error::{CoreError, CoreResult};
7use crate::core::logging::{normalize_limit, read_tail};
8use serde_json::{json, Value};
9use tauri::{Manager, State, Webview};
11macro_rules! module_cmd {
12 ($name:ident, $channel:expr, $perm:expr, |$core:ident, $ctx:ident, $p:ident| $body:expr) => {
13 #[tauri::command]
14 pub async fn $name(webview: Webview, slot: State<'_, CoreSlot>, payload: Option<Value>) -> CommandResult {
15 #[allow(unused_variables)]
16 let ($core, $ctx) = module_call(&webview, &slot, $perm)?;
17 #[allow(unused_variables)]
18 let $p = super::payload(payload);
19 let result: CoreResult<Value> = async { $body }.await;
20 module_result($channel, result)
21 }
22 };
25/// Native dialog texts of the Hash & Checksum module come from its own catalogs (as in 0.1.0).
26fn hash_text(core: &crate::core::app::Core, key: &str) -> String {
27 let dir = core.paths.bundled_modules().join("chj.hash-checksum").join("0.0.1").join("ui").join("i18n");
28 core.i18n
29 .chain(&core.language())
30 .iter()
31 .find_map(|code| {
32 std::fs::read(dir.join(format!("{code}.json")))
33 .ok()
34 .and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok())
35 .and_then(|catalog| catalog.get(key).and_then(Value::as_str).map(str::to_string))
36 })
37 .unwrap_or_else(|| key.to_string())
40/// `payload.options || {}`.
41fn options(payload: &Value) -> Value {
42 payload.get("options").filter(|v| v.is_object()).cloned().unwrap_or_else(|| json!({}))
45/// JavaScript truthiness for flags passed as `Boolean(value)`.
46fn truthy(value: &Value) -> bool {
47 match value {
48 Value::Null => false,
49 Value::Bool(b) => *b,
50 Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0 && !f.is_nan()),
51 Value::String(s) => !s.is_empty(),
52 _ => true,
53 }
56/// 0.1.0 `ids(payload)`.
57fn transfer_ids(payload: &Value) -> Value {
58 match &payload["ids"] {
59 Value::String(s) if s == "all" => json!("all"),
60 Value::Array(list) => Value::Array(list.iter().take(20000).map(|v| json!(v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string()))).collect()),
61 other => json!([other.as_str().map(str::to_string).unwrap_or_else(|| other.to_string())]),
62 }
65/// Explicit file/folder choice; both native pickers support multiple selections on every OS.
66async fn pick_upload_paths(core: &crate::core::app::Core, label: &str) -> Vec<std::path::PathBuf> {
67 use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
68 let title = core.t("native.files.selectUpload");
69 let parent = crate::dialogs::host_window(&core.app, label);
70 let files = core.t("chrome.files");
71 let folders = core.t("chrome.folders");
72 let (tx, rx) = tokio::sync::oneshot::channel();
73 let mut dialog = core.app.dialog().message(&title).title(&title)
74 .buttons(MessageDialogButtons::YesNoCancelCustom(files.clone(), folders.clone(), core.t("chrome.cancel")));
75 if let Some(window) = &parent { dialog = dialog.parent(window); }
76 dialog.show_with_result(move |result| { let _ = tx.send(result); });
77 let Ok(choice) = rx.await else { return Vec::new() };
78 choose_upload_paths(choice, &files, &folders,
79 crate::dialogs::pick_files(&core.app, parent.as_ref(), Some(&title), &[]),
80 async {
81 let (tx, rx) = tokio::sync::oneshot::channel();
82 let mut dialog = core.app.dialog().file().set_title(&title).set_can_create_directories(true);
83 if let Some(window) = &parent { dialog = dialog.set_parent(window); }
84 dialog.pick_folders(move |paths| {
85 let _ = tx.send(paths.unwrap_or_default().into_iter().filter_map(|p| p.into_path().ok()).collect());
86 });
87 rx.await.unwrap_or_default()
88 }).await
91async fn choose_upload_paths(
92 choice: tauri_plugin_dialog::MessageDialogResult, files: &str, folders: &str,
93 pick_files: impl std::future::Future<Output = Vec<std::path::PathBuf>>,
94 pick_folders: impl std::future::Future<Output = Vec<std::path::PathBuf>>,
95) -> Vec<std::path::PathBuf> {
96 use tauri_plugin_dialog::MessageDialogResult;
97 if choice == MessageDialogResult::Yes || choice == MessageDialogResult::Custom(files.to_string()) {
98 pick_files.await
99 } else if choice == MessageDialogResult::No || choice == MessageDialogResult::Custom(folders.to_string()) {
100 pick_folders.await
101 } else { Vec::new() }
104// --- window, language, sessions --------------------------------------------------------------
106module_cmd!(module_get_info, "plugin:getInfo", None, |core, ctx, p| Ok(ctx.manifest.clone()));
107module_cmd!(module_ui_get_language, "plugin:ui:getLanguage", None, |core, ctx, p| Ok(json!(core.language())));
108module_cmd!(module_close, "plugin:close", None, |core, ctx, p| {
109 core.services.modules.close_label(&core, &ctx.label);
110 Ok(json!({ "ok": true }))
111});
112module_cmd!(module_host_state, "plugin:host:state", None, |core, ctx, p| Ok(core.services.modules.host_state(&ctx.label)));
113module_cmd!(module_host_detach, "plugin:host:detach", None, |core, ctx, p| core.services.modules.detach(&core, &ctx.label));
114module_cmd!(module_host_attach, "plugin:host:attach", None, |core, ctx, p| core.services.modules.attach(&core, &ctx.label));
115module_cmd!(module_window_minimize, "plugin:window:minimize", None, |core, ctx, p| {
116 core.services.modules.minimize_label(&core, &ctx.label, true);
117 Ok(json!({ "ok": true }))
118});
119module_cmd!(module_sessions_list, "plugin:sessions:list", Some("session.read"), |core, ctx, p| Ok(Value::Array(core.services.sessions.list())));
121// --- system monitor ---------------------------------------------------------------------------
123module_cmd!(module_system_metrics, "plugin:system:metrics", Some("system.metrics.read"), |core, ctx, p| {
124 crate::core::ssh::metrics::read_system_metrics(&core.services.sessions, text(&p, "sessionId")).await
125});
126module_cmd!(module_system_virtual_memory, "plugin:system:virtualMemory", Some("system.metrics.read"), |core, ctx, p| {
127 core.services.virtual_memory.read(text(&p, "sessionId"), &json!({ "sudoPassword": p["sudoPassword"], "privileged": p["privileged"] == true })).await
128});
129module_cmd!(module_system_manage_virtual_memory, "plugin:system:manageVirtualMemory", Some("system.memory.manage"), |core, ctx, p| core.services.virtual_memory.manage(text(&p, "sessionId"), &p).await);
131// --- disks -------------------------------------------------------------------------------------
133module_cmd!(module_disks_inspect, "plugin:disks:inspect", Some("disks.read"), |core, ctx, p| core.services.disks.inspect(text(&p, "sessionId"), &json!({ "privileged": p["privileged"] == true, "sudoPassword": p["sudoPassword"], "rememberSudo": p["rememberSudo"] == true })).await);
134module_cmd!(module_disks_smart, "plugin:disks:smart", Some("disks.read"), |core, ctx, p| core.services.disks.smart(text(&p, "sessionId"), &p).await);
135module_cmd!(module_disks_jobs, "plugin:disks:jobs", Some("disks.read"), |core, ctx, p| core.services.disks.jobs(text(&p, "sessionId"), &p).await);
136module_cmd!(module_disks_job_status, "plugin:disks:jobStatus", Some("disks.read"), |core, ctx, p| core.services.disks.job_status(text(&p, "sessionId"), &p).await);
137module_cmd!(module_disks_plan, "plugin:disks:plan", Some("disks.manage"), |core, ctx, p| core.services.disks.plan(text(&p, "sessionId"), &p).await);
138module_cmd!(module_disks_auto_expand, "plugin:disks:autoExpand", Some("disks.manage"), |core, ctx, p| core.services.disks.auto_expand(text(&p, "sessionId"), &p).await);
139module_cmd!(module_disks_cancel_plan, "plugin:disks:cancelPlan", Some("disks.manage"), |core, ctx, p| Ok(core.services.disks.cancel_plan(&p["planId"])));
140module_cmd!(module_disks_apply, "plugin:disks:apply", Some("disks.manage"), |core, ctx, p| core.services.disks.apply(text(&p, "sessionId"), &p).await);
142// --- keys, logs --------------------------------------------------------------------------------
144module_cmd!(module_keys_generate, "plugin:keys:generate", Some("keys.generate"), |core, ctx, p| core.services.keys.generate_owned(&format!("{}:{}",ctx.id,ctx.label), &p).await);
145module_cmd!(module_keys_save, "plugin:keys:save", Some("keys.generate"), |core, ctx, p| {
146 let generated = core.services.keys.generated_owned(&format!("{}:{}",ctx.id,ctx.label), text(&p, "generationId"))?;
147 if generated["family"] != "ssh" { return Err(CoreError::plain("Use the OpenPGP generated-file export.")); }
148 let title = core.i18n.t(&core.language(), "native.keys.saveTitle");
149 let parent = crate::dialogs::host_window(&core.app, &ctx.label);
150 let suggested = generated["suggestedName"].as_str().unwrap_or("id_key").to_string();
151 match crate::dialogs::save_file(&core.app, parent.as_ref(), Some(&title), &suggested, &[]).await {
152 Some(path) => crate::core::keys::save_pair(&path, &generated),
153 None => Ok(json!({ "canceled": true })),
154 }
155});
156module_cmd!(module_keys_password, "plugin:keys:password", Some("keys.generate"), |core, ctx, p| core.services.keys.generate_password_owned(&format!("{}:{}",ctx.id,ctx.label), &p));
157module_cmd!(module_keys_copy_password, "plugin:keys:copyPassword", Some("keys.generate"), |core, ctx, p| {
158 use tauri_plugin_clipboard_manager::ClipboardExt;
159 let password = core.services.keys.password_owned(&format!("{}:{}",ctx.id,ctx.label), text(&p, "passwordId"))?;
160 core.app.clipboard().write_text(password.as_str()).map_err(|_| CoreError::plain("Clipboard is unavailable."))?;
161 Ok(json!({ "copied": true }))
162});
163module_cmd!(module_keys_state, "plugin:keys:state", Some("keys.generate"), |core, ctx, p| {
164 let owner=format!("{}:{}",ctx.id,ctx.label);
165 let mut state = core.services.keys.state_owned(&owner); state["vaultUnlocked"] = json!(core.vault.is_unlocked());
166 if let Some(token)=state["editContext"]["token"].as_str() {
167 let valid=ctx.id=="chj.key-generator" && core.services.keys.edit_context(&owner,token).is_ok_and(|context| context["vaultGeneration"].as_u64().is_some_and(|g|core.vault.assert_generation(g).is_ok()));
168 if !valid {state["editContext"]=Value::Null;}
169 }
170 Ok(state)
171});
172module_cmd!(module_keys_edit, "plugin:keys:edit", Some("keys.generate"), |core, ctx, p| {
173 if ctx.id!="chj.key-generator" {return Err(CoreError::new("PLUGIN_PERMISSION_DENIED","Only the Key Generator can edit its selected identity."));}
174 let owner=format!("{}:{}",ctx.id,ctx.label);
175 if ["signText","verifyText","encryptText","decryptText"].contains(&text(&p,"operation")) {
176 let generated=p["generationId"].as_str();
177 let context=if generated.is_none(){Some(core.services.keys.edit_context(&owner,text(&p,"editToken"))?)}else{None};
178 let before=if let Some(id)=generated{core.services.keys.identity_owned(&owner,id)?}else{
179 let context=context.as_ref().unwrap();let generation=context["vaultGeneration"].as_u64().ok_or_else(||CoreError::plain("Open the identity again."))?;
180 core.services.biometrics.require_sensitive_action().await?;
181 let entry=crate::core::keychain::get_at_generation(&core.vault,text(context,"id"),generation)?;
182 if entry["public"]!=context["public"]{return Err(CoreError::plain("Key changed. Open it again."));}entry
183 };
184 let job=core.services.keys.begin_operation(&owner,&p)?;let entry=before.clone();let payload=p.clone();
185 let result=tokio::task::spawn_blocking(move||{job.step("prepare")?;let result=crate::core::openpgp_data::perform_text(&entry,&payload)?;job.step("verify")?;Ok::<_,CoreError>(result)}).await.map_err(|_|CoreError::plain("OpenPGP operation failed."))??;
186 if let Some(id)=generated {if core.services.keys.identity_owned(&owner,id)?!=before{return Err(CoreError::plain("Generated identity changed."));}return Ok(result)}
187 let context=context.unwrap();let generation=context["vaultGeneration"].as_u64().unwrap();
188 return core.services.keys.with_edit_context(&owner,text(&p,"editToken"),&context,||{
189 crate::core::keychain::with_revision(&core.vault,generation,&before,||Ok(result))
190 });
191 }
192 if p["generationId"].is_string() {return core.services.keys.edit_generated(&owner,&p).await;}
193 let token=text(&p,"editToken");
194 let context=core.services.keys.edit_context(&owner,token)?;
195 if text(&p,"operation")=="closeEditor" {core.services.keys.clear_edit(&owner);return Ok(json!({"closed":true}));}
196 let generation=context["vaultGeneration"].as_u64().ok_or_else(||CoreError::plain("Open the identity again from Keychain."))?;
197 core.vault.assert_generation(generation)?;
198 core.services.biometrics.require_sensitive_action().await?;
199 let before=crate::core::keychain::get_at_generation(&core.vault,text(&context,"id"),generation)?;
200 if before["public"]!=context["public"] {return Err(CoreError::plain("Key changed. Open it again from Keychain."));}
201 let job=core.services.keys.begin_operation(&owner,&p)?;
202 let entry=before.clone();let payload=p.clone();
203 let next=tokio::task::spawn_blocking(move ||crate::core::openpgp::edit(&entry,&payload,&|phase|job.step(phase))).await.map_err(|_|CoreError::plain("Key editing failed."))??;
204 // A closed module, changed selection, expired authorization, lock or concurrent
205 // certificate update must never commit an old result to the vault.
206 let updated=core.services.keys.with_edit_context(&owner,token,&context,||
207 crate::core::keychain::commit_revision(&core.vault,generation,&before,next.clone()))?;
208 core.services.keys.refresh_edit(&owner,token,&next);
209 core.emit_main("keychain:changed",json!({}));
210 Ok(json!({"stored":true,"identity":updated}))
211});
212module_cmd!(module_keys_save_artifact, "plugin:keys:saveArtifact", Some("keys.generate"), |core, ctx, p| {
213 let owner = format!("{}:{}",ctx.id,ctx.label);
214 let generated = core.services.keys.generated_owned(&owner, text(&p,"generationId"))?;
215 let index = p["index"].as_u64().ok_or_else(|| CoreError::plain("Invalid generated file index."))? as usize;
216 let file = generated["files"].as_array().and_then(|files| files.get(index)).ok_or_else(|| CoreError::plain("Generated file was not found."))?;
217 let parent = crate::dialogs::host_window(&core.app,&ctx.label);
218 let Some(path) = crate::dialogs::save_file(&core.app,parent.as_ref(),None,file["name"].as_str().unwrap_or("key.asc"),&[]).await else { return Ok(json!({"canceled":true})); };
219 let generated = core.services.keys.generated_owned(&owner,text(&p,"generationId"))?;
220 crate::core::keys::save_artifact(&path,&generated,index)
221});
222module_cmd!(module_keys_save_bundle, "plugin:keys:saveBundle", Some("keys.generate"), |core, ctx, p| {
223 let owner = format!("{}:{}",ctx.id,ctx.label);
224 core.services.keys.generated_owned(&owner,text(&p,"generationId"))?;
225 let parent = crate::dialogs::host_window(&core.app,&ctx.label);
226 let Some(path) = crate::dialogs::pick_folder(&core.app,parent.as_ref(),None).await else { return Ok(json!({"canceled":true})); };
227 let generated = core.services.keys.generated_owned(&owner,text(&p,"generationId"))?;
228 crate::core::keys::save_bundle(&path,&generated)
229});
230module_cmd!(module_keys_store_keychain, "plugin:keys:storeKeychain", Some("keychain.storeGenerated"), |core, ctx, p| {
231 if ctx.id != "chj.key-generator" { return Err(CoreError::new("PLUGIN_PERMISSION_DENIED","Only the Key Generator can store its generated identities.")); }
232 let owner = format!("{}:{}",ctx.id,ctx.label);
233 core.services.keys.identity_owned(&owner,text(&p,"generationId"))?;
234 if !core.vault.is_unlocked() {
235 core.emit_main("generator:unlockRequested",json!({}));
236 if let Some(window) = crate::dialogs::main_window(&core.app) { let _ = window.show(); let _ = window.set_focus(); }
237 return Ok(json!({"needsUnlock":true}));
238 }
239 let generation = core.vault.unlocked_generation()?;
240 core.services.biometrics.require_sensitive_action().await?;
241 let entry = core.services.keys.identity_owned(&owner,text(&p,"generationId"))?;
242 let saved = crate::core::keychain::store_at_generation(&core.vault,entry,generation)?;
243 core.emit_main("keychain:changed",json!({}));
244 Ok(json!({"stored":true,"identity":saved}))
245});
246// Public SSH keys from the Keychain for authorized_keys in Users. Only public lines, label and
247// fingerprint leave Core; private material and OpenPGP identities stay in the vault.
248module_cmd!(module_keychain_public_ssh, "plugin:keychain:publicSsh", Some("keychain.readPublic"), |core, ctx, p| {
249 if ctx.id != "chj.users" { return Err(CoreError::new("PLUGIN_PERMISSION_DENIED","Only the Users module can read public Keychain keys.")); }
250 let generation = core.vault.unlocked_generation().map_err(|_| CoreError::new("VAULT_LOCKED","Unlock the vault in the main window to use Keychain keys."))?;
251 core.vault.with_data_at_generation(generation, |data| {
252 Ok(Value::Array(data.pointer("/secrets/keychain").and_then(Value::as_array).map(|items| items.iter()
253 .filter(|item| item["kind"] == "ssh" && item["public"].is_string())
254 .map(|item| json!({"id":item["id"],"label":item["label"],"fingerprint":item["fingerprint"],"publicKey":item["public"]}))
255 .collect()).unwrap_or_default()))
256 })
257});
258module_cmd!(module_logs_read, "plugin:logs:read", Some("logs.read"), |core, ctx, p| {
259 let tail = read_tail(core.logger.path(), normalize_limit(p.get("maxBytes")))?;
260 Ok(serde_json::to_value(tail)?)
261});
263// --- users --------------------------------------------------------------------------------------
265module_cmd!(module_users_list, "plugin:users:list", Some("users.read"), |core, ctx, p| core.services.users.list(text(&p, "sessionId")).await);
266module_cmd!(module_users_privileges, "plugin:users:privileges", Some("users.read"), |core, ctx, p| core.services.users.privileges(text(&p, "sessionId"), p["refresh"] == true).await);
267module_cmd!(module_users_inventory, "plugin:users:inventory", Some("users.read"), |core, ctx, p| core.services.users.inventory(text(&p, "sessionId"), &p).await);
268module_cmd!(module_users_detail, "plugin:users:detail", Some("users.read"), |core, ctx, p| core.services.users.detail(text(&p, "sessionId"), &p).await);
269module_cmd!(module_users_manage, "plugin:users:manage", Some("users.manage"), |core, ctx, p| core.services.users.manage(text(&p, "sessionId"), &p).await);
270module_cmd!(module_users_keys, "plugin:users:keys", Some("users.manage"), |core, ctx, p| core.services.users.keys(text(&p, "sessionId"), &p).await);
271module_cmd!(module_users_ssh_policy, "plugin:users:sshPolicy", Some("users.manage"), |core, ctx, p| core.services.users.ssh_policy(text(&p, "sessionId"), &p).await);
272module_cmd!(module_users_ssh_password_off_all, "plugin:users:sshPasswordOffAll", Some("users.manage"), |core, ctx, p| core.services.users.ssh_password_off_all(text(&p, "sessionId"), &p).await);
274// --- nginx ---------------------------------------------------------------------------------------
276use crate::core::ssh::nginx;
277module_cmd!(module_nginx_inspect, "plugin:nginx:inspect", Some("nginx.read"), |core, ctx, p| nginx::inspect(&core.services.privileged, text(&p, "sessionId")).await);
278module_cmd!(module_nginx_read_config, "plugin:nginx:readConfig", Some("nginx.read"), |core, ctx, p| nginx::read_config(&core.services.privileged, text(&p, "sessionId"), &p).await);
279module_cmd!(module_nginx_dump_config, "plugin:nginx:dumpConfig", Some("nginx.read"), |core, ctx, p| nginx::dump_config(&core.services.privileged, text(&p, "sessionId"), &p).await);
280module_cmd!(module_nginx_test_config, "plugin:nginx:testConfig", Some("nginx.read"), |core, ctx, p| nginx::test_config(&core.services.privileged, text(&p, "sessionId"), &p).await);
281module_cmd!(module_nginx_save_config, "plugin:nginx:saveConfig", Some("nginx.manage"), |core, ctx, p| nginx::save_config(&core.services.privileged, text(&p, "sessionId"), &p).await);
282module_cmd!(module_nginx_reload, "plugin:nginx:reload", Some("nginx.manage"), |core, ctx, p| nginx::reload(&core.services.privileged, text(&p, "sessionId"), &p).await);
283module_cmd!(module_nginx_sites, "plugin:nginx:sites", Some("nginx.read"), |core, ctx, p| nginx::sites(&core.services.privileged, text(&p, "sessionId"), &p).await);
284module_cmd!(module_nginx_plan_site, "plugin:nginx:planSite", Some("nginx.read"), |core, ctx, p| nginx::plan_site(&core.services.privileged, text(&p, "sessionId"), &p).await);
285module_cmd!(module_nginx_create_site, "plugin:nginx:createSite", Some("nginx.manage"), |core, ctx, p| nginx::create_site(&core.services.privileged, text(&p, "sessionId"), &p).await);
286module_cmd!(module_nginx_set_site_enabled, "plugin:nginx:setSiteEnabled", Some("nginx.manage"), |core, ctx, p| nginx::set_site_enabled(&core.services.privileged, text(&p, "sessionId"), &p).await);
287module_cmd!(module_nginx_adopt_site, "plugin:nginx:adoptSite", Some("nginx.manage"), |core, ctx, p| nginx::adopt_site(&core.services.privileged, text(&p, "sessionId"), &p).await);
288module_cmd!(module_nginx_drop_broken_link, "plugin:nginx:dropBrokenLink", Some("nginx.manage"), |core, ctx, p| nginx::drop_broken_link(&core.services.privileged, text(&p, "sessionId"), &p).await);
289module_cmd!(module_nginx_plan_route, "plugin:nginx:planRoute", Some("nginx.read"), |core, ctx, p| nginx::plan_route(&core.services.privileged, text(&p, "sessionId"), &p).await);
290module_cmd!(module_nginx_apply_route, "plugin:nginx:applyRoute", Some("nginx.manage"), |core, ctx, p| nginx::apply_route(&core.services.privileged, text(&p, "sessionId"), &p).await);
291module_cmd!(module_nginx_diagnose_domain, "plugin:nginx:diagnoseDomain", Some("nginx.read"), |core, ctx, p| nginx::diagnose_domain(&core.services.privileged, text(&p, "sessionId"), &p).await);
292module_cmd!(module_nginx_plan_edit, "plugin:nginx:planEdit", Some("nginx.read"), |core, ctx, p| nginx::plan_edit(&core.services.privileged, text(&p, "sessionId"), &p).await);
293module_cmd!(module_nginx_backups, "plugin:nginx:backups", Some("nginx.read"), |core, ctx, p| nginx::backups(&core.services.privileged, text(&p, "sessionId"), &p).await);
294module_cmd!(module_nginx_read_backup, "plugin:nginx:readBackup", Some("nginx.read"), |core, ctx, p| nginx::read_backup(&core.services.privileged, text(&p, "sessionId"), &p).await);
295module_cmd!(module_nginx_restore_backup, "plugin:nginx:restoreBackup", Some("nginx.manage"), |core, ctx, p| nginx::restore_backup(&core.services.privileged, text(&p, "sessionId"), &p).await);
296module_cmd!(module_nginx_certificates, "plugin:nginx:certificates", Some("nginx.read"), |core, ctx, p| nginx::certificates(&core.services.privileged, text(&p, "sessionId"), &p).await);
297module_cmd!(module_nginx_install_certbot, "plugin:nginx:installCertbot", Some("nginx.manage"), |core, ctx, p| nginx::install_certbot(&core.services.privileged, text(&p, "sessionId"), &p).await);
298module_cmd!(module_nginx_issue_certificate, "plugin:nginx:issueCertificate", Some("nginx.manage"), |core, ctx, p| nginx::issue_certificate(&core.services.privileged, text(&p, "sessionId"), &p).await);
299module_cmd!(module_nginx_read_log, "plugin:nginx:readLog", Some("nginx.read"), |core, ctx, p| nginx::read_log(&core.services.privileged, text(&p, "sessionId"), &p).await);
300module_cmd!(module_nginx_probe_backends, "plugin:nginx:probeBackends", Some("nginx.read"), |core, ctx, p| nginx::probe_backends(&core.services.privileged, text(&p, "sessionId"), &p).await);
302// --- apache --------------------------------------------------------------------------------------
304use crate::core::ssh::apache;
305module_cmd!(module_apache_inspect, "plugin:apache:inspect", Some("apache.read"), |core, ctx, p| apache::inspect(&core.services.privileged, text(&p, "sessionId")).await);
306module_cmd!(module_apache_inventory, "plugin:apache:inventory", Some("apache.read"), |core, ctx, p| apache::inventory(&core.services.privileged, text(&p, "sessionId"), &p).await);
307module_cmd!(module_apache_read_log, "plugin:apache:readLog", Some("apache.read"), |core, ctx, p| apache::read_log(&core.services.privileged, text(&p, "sessionId"), &p).await);
308module_cmd!(module_apache_probe_backends, "plugin:apache:probeBackends", Some("apache.read"), |core, ctx, p| apache::probe_backends(&core.services.privileged, text(&p, "sessionId"), &p).await);
309module_cmd!(module_apache_read_file, "plugin:apache:readFile", Some("apache.read"), |core, ctx, p| apache::read_file(&core.services.privileged, text(&p, "sessionId"), &p).await);
310module_cmd!(module_apache_backups, "plugin:apache:backups", Some("apache.read"), |core, ctx, p| apache::backups(&core.services.privileged, text(&p, "sessionId"), &p).await);
311module_cmd!(module_apache_read_backup, "plugin:apache:readBackup", Some("apache.read"), |core, ctx, p| apache::read_backup(&core.services.privileged, text(&p, "sessionId"), &p).await);
312module_cmd!(module_apache_plan_site, "plugin:apache:planSite", Some("apache.read"), |core, ctx, p| apache::plan_site(&core.services.privileged, text(&p, "sessionId"), &p).await);
313module_cmd!(module_apache_plan_edit, "plugin:apache:planEdit", Some("apache.read"), |core, ctx, p| apache::plan_edit(&core.services.privileged, text(&p, "sessionId"), &p).await);
314module_cmd!(module_apache_plan_fix, "plugin:apache:planFix", Some("apache.read"), |core, ctx, p| apache::plan_fix(&core.services.privileged, text(&p, "sessionId"), &p).await);
315module_cmd!(module_apache_save_file, "plugin:apache:saveFile", Some("apache.manage"), |core, ctx, p| apache::save_file(&core.services.privileged, text(&p, "sessionId"), &p).await);
316module_cmd!(module_apache_restore_backup, "plugin:apache:restoreBackup", Some("apache.manage"), |core, ctx, p| apache::restore_backup(&core.services.privileged, text(&p, "sessionId"), &p).await);
317module_cmd!(module_apache_create_site, "plugin:apache:createSite", Some("apache.manage"), |core, ctx, p| apache::create_site(&core.services.privileged, text(&p, "sessionId"), &p).await);
318module_cmd!(module_apache_set_site_enabled, "plugin:apache:setSiteEnabled", Some("apache.manage"), |core, ctx, p| apache::set_site_enabled(&core.services.privileged, text(&p, "sessionId"), &p).await);
319module_cmd!(module_apache_adopt_site, "plugin:apache:adoptSite", Some("apache.manage"), |core, ctx, p| apache::adopt_site(&core.services.privileged, text(&p, "sessionId"), &p).await);
320module_cmd!(module_apache_drop_broken_link, "plugin:apache:dropBrokenLink", Some("apache.manage"), |core, ctx, p| apache::drop_broken_link(&core.services.privileged, text(&p, "sessionId"), &p).await);
321module_cmd!(module_apache_set_module, "plugin:apache:setModule", Some("apache.manage"), |core, ctx, p| apache::set_module(&core.services.privileged, text(&p, "sessionId"), &p).await);
322module_cmd!(module_apache_reload, "plugin:apache:reload", Some("apache.manage"), |core, ctx, p| apache::reload(&core.services.privileged, text(&p, "sessionId"), &p).await);
323module_cmd!(module_network_inspect, "plugin:network:inspect", Some("network.read"), |core, ctx, p| core.services.network.inspect(text(&p, "sessionId"), &p).await);
324module_cmd!(module_network_pending, "plugin:network:pending", Some("network.read"), |core, ctx, p| core.services.network.pending(text(&p, "sessionId")).await);
325module_cmd!(module_network_diagnose, "plugin:network:diagnose", Some("network.read"), |core, ctx, p| core.services.network.diagnose(text(&p, "sessionId"), &p).await);
326module_cmd!(module_network_plan_interface, "plugin:network:planInterface", Some("network.manage"), |core, ctx, p| core.services.network.plan_interface(text(&p, "sessionId"), &p).await);
327module_cmd!(module_network_plan_firewall, "plugin:network:planFirewall", Some("network.manage"), |core, ctx, p| core.services.network.plan_firewall(text(&p, "sessionId"), &p).await);
328module_cmd!(module_network_apply, "plugin:network:apply", Some("network.manage"), |core, ctx, p| core.services.network.apply(&p).await);
329module_cmd!(module_network_verify, "plugin:network:verify", Some("network.manage"), |core, ctx, p| core.services.network.verify(&p).await);
330module_cmd!(module_network_confirm, "plugin:network:confirm", Some("network.manage"), |core, ctx, p| core.services.network.confirm(&p).await);
331module_cmd!(module_network_rollback, "plugin:network:rollback", Some("network.manage"), |core, ctx, p| core.services.network.rollback(&p).await);
332module_cmd!(module_network_extend, "plugin:network:extend", Some("network.manage"), |core, ctx, p| core.services.network.extend(&p).await);
333module_cmd!(module_network_transaction_log, "plugin:network:transactionLog", Some("network.manage"), |core, ctx, p| core.services.network.transaction_log(&p).await);
334module_cmd!(module_rsync_ssh_profiles, "plugin:rsync:sshProfiles", Some("rsync.read"), |core, ctx, p| core.services.rsync.ssh_profiles());
335module_cmd!(module_rsync_local_probe, "plugin:rsync:localProbe", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.local_probe().await));
336module_cmd!(module_rsync_remote_probe, "plugin:rsync:remoteProbe", Some("rsync.read"), |core, ctx, p| core.services.rsync.remote_probe(text(&p, "sessionId")).await);
337module_cmd!(module_rsync_inspect, "plugin:rsync:inspect", Some("rsync.read"), |core, ctx, p| core.services.rsync.inspect(&p).await);
338module_cmd!(module_rsync_jobs, "plugin:rsync:jobs", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.jobs()));
339module_cmd!(module_rsync_job, "plugin:rsync:job", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.job(text(&p, "id"))));
340module_cmd!(module_rsync_describe, "plugin:rsync:describe", Some("rsync.read"), |core, ctx, p| core.services.rsync.describe(&p));
341module_cmd!(module_rsync_versions, "plugin:rsync:versions", Some("rsync.read"), |core, ctx, p| core.services.rsync.versions(&p).await);
342module_cmd!(module_rsync_profiles, "plugin:rsync:profiles", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.profiles()));
343module_cmd!(module_rsync_history, "plugin:rsync:history", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.history()));
344module_cmd!(module_rsync_schedule_list, "plugin:rsync:scheduleList", Some("rsync.read"), |core, ctx, p| core.services.rsync.schedule_list(text(&p, "sessionId"), &p).await);
345module_cmd!(module_rsync_discover, "plugin:rsync:discover", Some("rsync.read"), |core, ctx, p| core.services.rsync.discover(text(&p, "sessionId"), &p).await);
346module_cmd!(module_rsync_schedule_log, "plugin:rsync:scheduleLog", Some("rsync.read"), |core, ctx, p| core.services.rsync.schedule_action(text(&p, "sessionId"), "log", &p).await);
347module_cmd!(module_rsync_take_handoff, "plugin:rsync:takeHandoff", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.take_handoff()));
348module_cmd!(module_rsync_preview, "plugin:rsync:preview", Some("rsync.run"), |core, ctx, p| core.services.rsync.preview(&ctx.id, &p).await);
349module_cmd!(module_rsync_run, "plugin:rsync:run", Some("rsync.run"), |core, ctx, p| core.services.rsync.run(&ctx.id, &p));
350module_cmd!(module_rsync_cancel, "plugin:rsync:cancel", Some("rsync.run"), |core, ctx, p| core.services.rsync.cancel(text(&p, "id")).await);
351module_cmd!(module_rsync_install, "plugin:rsync:install", Some("rsync.run"), |core, ctx, p| core.services.rsync.install(text(&p, "sessionId"), &p).await);
352module_cmd!(module_rsync_connect, "plugin:rsync:connect", Some("rsync.run"), |core, ctx, p| core.services.rsync.connect(&p).await);
353module_cmd!(module_rsync_trust_host, "plugin:rsync:trustHost", Some("rsync.run"), |core, ctx, p| core.services.rsync.trust_host(&p));
354module_cmd!(module_rsync_disconnect, "plugin:rsync:disconnect", Some("rsync.run"), |core, ctx, p| core.services.rsync.disconnect(text(&p, "sessionId")).await);
355module_cmd!(module_rsync_save_profile, "plugin:rsync:saveProfile", Some("rsync.run"), |core, ctx, p| core.services.rsync.save_profile(&p));
356module_cmd!(module_rsync_delete_profile, "plugin:rsync:deleteProfile", Some("rsync.run"), |core, ctx, p| core.services.rsync.delete_profile(text(&p, "id")));
357module_cmd!(module_rsync_clear_history, "plugin:rsync:clearHistory", Some("rsync.run"), |core, ctx, p| core.services.rsync.clear_history());
358module_cmd!(module_rsync_prune_versions, "plugin:rsync:pruneVersions", Some("rsync.run"), |core, ctx, p| core.services.rsync.prune_versions(&p).await);
359module_cmd!(module_rsync_schedule_install, "plugin:rsync:scheduleInstall", Some("rsync.run"), |core, ctx, p| core.services.rsync.schedule_install(&p).await);
360module_cmd!(module_rsync_schedule_start, "plugin:rsync:scheduleStart", Some("rsync.run"), |core, ctx, p| core.services.rsync.schedule_action(text(&p, "sessionId"), "start", &p).await);
361module_cmd!(module_rsync_schedule_remove, "plugin:rsync:scheduleRemove", Some("rsync.run"), |core, ctx, p| core.services.rsync.schedule_action(text(&p, "sessionId"), "remove", &p).await);
362module_cmd!(module_rsync_open_manager, "plugin:rsync:openManager", Some("rsync.run"), |core, ctx, p| {
363 core.services.rsync.put_handoff(p["spec"].clone());
364 core.services.modules.open(&core, "chj.rsync-manager")
365});
366module_cmd!(module_apache_install_certbot, "plugin:apache:installCertbot", Some("apache.manage"), |core, ctx, p| apache::install_certbot(&core.services.privileged, text(&p, "sessionId"), &p).await);
367module_cmd!(module_apache_issue_certificate, "plugin:apache:issueCertificate", Some("apache.manage"), |core, ctx, p| apache::issue_certificate(&core.services.privileged, text(&p, "sessionId"), &p).await);
370// --- files / transfers ---------------------------------------------------------------------------
372module_cmd!(module_files_list, "plugin:files:list", Some("files.read"), |core, ctx, p| core.services.files.list(text(&p, "sessionId"), &p["path"], &options(&p)).await);
373module_cmd!(module_files_privileges, "plugin:files:privileges", Some("files.read"), |core, ctx, p| core.services.files.privileges(text(&p, "sessionId")).await);
374module_cmd!(module_files_read_text, "plugin:files:readText", Some("files.read"), |core, ctx, p| core.services.files.read_text(text(&p, "sessionId"), &p["path"], &options(&p), &ctx.id).await);
375module_cmd!(module_files_write_text, "plugin:files:writeText", Some("files.write"), |core, ctx, p| core.services.files.write_text(text(&p, "sessionId"), &p["path"], &p["text"], &options(&p), &ctx.id).await);
376module_cmd!(module_files_save_text, "plugin:files:saveText", Some("files.write"), |core, ctx, p| Ok(core.services.files.save_text(text(&p, "sessionId"), &p["path"], &p["text"], &options(&p), &ctx.id).await));
377module_cmd!(module_files_list_recovery, "plugin:files:listRecovery", Some("files.read"), |core, ctx, p| core.services.files.list_recovery(text(&p, "sessionId"), &options(&p)).await);
378module_cmd!(module_files_read_recovery, "plugin:files:readRecovery", Some("files.read"), |core, ctx, p| core.services.files.read_recovery(text(&p, "sessionId"), &p["recoveryId"], &options(&p)).await);
379module_cmd!(module_files_cleanup_recovery, "plugin:files:cleanupRecovery", Some("files.write"), |core, ctx, p| core.services.files.cleanup_recovery(text(&p, "sessionId"), &p["recoveryId"], &options(&p)).await);
380module_cmd!(module_files_close_text, "plugin:files:closeText", Some("files.read"), |core, ctx, p| Ok(core.services.files.close_text(text(&p, "editId"), &ctx.id)));
381module_cmd!(module_files_create_file, "plugin:files:createFile", Some("files.write"), |core, ctx, p| core.services.files.create_file(text(&p, "sessionId"), &p["path"], &options(&p)).await);
382module_cmd!(module_files_mkdir, "plugin:files:mkdir", Some("files.write"), |core, ctx, p| core.services.files.mkdir(text(&p, "sessionId"), &p["path"], &options(&p)).await);
383module_cmd!(module_files_rename, "plugin:files:rename", Some("files.write"), |core, ctx, p| core.services.files.rename(text(&p, "sessionId"), &p["from"], &p["to"], &options(&p)).await);
384module_cmd!(module_files_remove, "plugin:files:remove", Some("files.write"), |core, ctx, p| core.services.files.remove(text(&p, "sessionId"), &p["path"], &options(&p)).await);
385module_cmd!(module_files_remove_many, "plugin:files:removeMany", Some("files.write"), |core, ctx, p| core.services.files.remove_many(text(&p, "sessionId"), &p["paths"], p["recursive"].as_bool().unwrap_or(false) || truthy(&p["recursive"]), &options(&p)).await);
386module_cmd!(module_files_upload, "plugin:files:upload", Some("files.transfer"), |core, ctx, p| {
387 let picked = pick_upload_paths(&core, &ctx.label).await;
388 core.services.files.upload(text(&p, "sessionId"), &p["directory"], picked).await
389});
390module_cmd!(module_files_download, "plugin:files:download", Some("files.transfer"), |core, ctx, p| {
391 let session = text(&p, "sessionId");
392 let (target, size) = core.services.files.download_check(session, &p["path"]).await?;
393 let title = core.t("native.files.saveDownload");
394 let parent = crate::dialogs::host_window(&core.app, &ctx.label);
395 match crate::dialogs::save_file(&core.app, parent.as_ref(), Some(&title), &crate::core::files::posix::basename(&target), &[]).await {
396 Some(local) => core.services.files.download_to(session, &target, size, &local).await,
397 None => Ok(json!({ "canceled": true })),
398 }
399});
400module_cmd!(module_files_download_many, "plugin:files:downloadMany", Some("files.transfer"), |core, ctx, p| {
401 let targets = crate::core::files::service::RemoteFileService::normalize_selection(&p["paths"])?;
402 let title = core.t("native.files.selectDownloadDirectory");
403 let parent = crate::dialogs::host_window(&core.app, &ctx.label);
404 match crate::dialogs::pick_folder(&core.app, parent.as_ref(), Some(&title)).await {
405 Some(directory) => core.services.files.download_many(text(&p, "sessionId"), &targets, &directory).await,
406 None => Ok(json!({ "canceled": true, "items": [] })),
407 }
408});
409module_cmd!(module_files_download_archive, "plugin:files:downloadArchive", Some("files.transfer"), |core, ctx, p| {
410 use crate::core::files::service::{safe_entry_name, RemoteFileService};
411 let targets = RemoteFileService::normalize_selection(&p["paths"])?;
412 if targets.iter().any(|t| t == "/") {
413 return Err(CoreError::plain("Archiving the entire remote root is not allowed."));
414 }
415 let format = RemoteFileService::archive_format(&p["format"])?;
416 let base = if targets.len() == 1 { safe_entry_name(&crate::core::files::posix::basename(&targets[0]))? } else { "remote-selection".to_string() };
417 let title = core.t("native.files.saveArchive");
418 let parent = crate::dialogs::host_window(&core.app, &ctx.label);
419 let extensions: &[&str] = match format.as_str() { "tar.gz" => &["tar.gz", "tgz"], "tar" => &["tar"], _ => &["zip"] };
420 let upper = format.to_uppercase();
421 let filters = [crate::dialogs::Filter { name: &upper, extensions }];
422 match crate::dialogs::save_file(&core.app, parent.as_ref(), Some(&title), &format!("{base}.{format}"), &filters).await {
423 Some(destination) => core.services.files.download_archive(text(&p, "sessionId"), &targets, &format, &destination).await,
424 None => Ok(json!({ "canceled": true })),
425 }
426});
427// The local disk belongs to the first-party File Manager only, whatever another module's manifest asks for.
428fn local_data_dir(core: &crate::core::app::Core, ctx: &crate::security::caller::ModuleContext) -> CoreResult<std::path::PathBuf> {
429 if ctx.id != "chj.file-manager" {
430 return Err(CoreError::new("MODULE_PERMISSION_DENIED", "Only the File Manager can use the local disk."));
431 }
432 Ok(core.paths.data.clone())
434module_cmd!(module_local_places, "plugin:files:local:places", Some("files.local"), |core, ctx, p| {
435 local_data_dir(&core, &ctx)?;
436 Ok(crate::core::files::local::places())
437});
438module_cmd!(module_local_list, "plugin:files:local:list", Some("files.local"), |core, ctx, p| {
439 let data = local_data_dir(&core, &ctx)?;
440 let path = text(&p, "path").to_string();
441 tokio::task::spawn_blocking(move || crate::core::files::local::list(&path, &data)).await.map_err(|_| CoreError::plain("Local folder read failed."))?
442});
443module_cmd!(module_local_mkdir, "plugin:files:local:mkdir", Some("files.local"), |core, ctx, p| {
444 let data = local_data_dir(&core, &ctx)?;
445 crate::core::files::local::mkdir(text(&p, "parent"), text(&p, "name"), &data)
446});
447module_cmd!(module_local_create_file, "plugin:files:local:createFile", Some("files.local"), |core, ctx, p| {
448 let data = local_data_dir(&core, &ctx)?;
449 crate::core::files::local::create_file(text(&p, "parent"), text(&p, "name"), &data)
450});
451module_cmd!(module_local_rename, "plugin:files:local:rename", Some("files.local"), |core, ctx, p| {
452 let data = local_data_dir(&core, &ctx)?;
453 crate::core::files::local::rename(text(&p, "path"), text(&p, "name"), &data)
454});
455module_cmd!(module_local_remove, "plugin:files:local:remove", Some("files.local"), |core, ctx, p| {
456 let data = local_data_dir(&core, &ctx)?;
457 let paths: Vec<String> = p["paths"].as_array().map(|list| list.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default();
458 tokio::task::spawn_blocking(move || crate::core::files::local::remove(&paths, &data)).await.map_err(|_| CoreError::plain("Local delete failed."))?
459});
460// Copy between panels: no picker, the destination comes from the panel. Local ends need `files.local`.
461module_cmd!(module_transfers_plan_copy, "plugin:files:transfers:planCopy", Some("files.transfer"), |core, ctx, p| {
462 let (from, to) = (&p["source"], &p["destination"]);
463 match (text(from, "kind"), text(to, "kind")) {
464 ("remote", "remote") => core.services.transfers.plan_relay(&ctx.id, text(from, "sessionId"), &from["paths"], text(to, "sessionId"), &to["directory"], &p["options"]).await,
465 ("local", "remote") => {
466 ctx.require("files.local")?;
467 let data = local_data_dir(&core, &ctx)?;
468 let mut paths = Vec::new();
469 for value in from["paths"].as_array().cloned().unwrap_or_default() {
470 paths.push(crate::core::files::local::checked_path(value.as_str().unwrap_or(""), &data)?);
471 }
472 let pick: crate::core::files::transfers::PickPaths = Box::new(move || Box::pin(async move { Some(paths) }));
473 core.services.transfers.plan_upload(&ctx.id, text(to, "sessionId"), &to["directory"], pick).await
474 }
475 ("remote", "local") => {
476 ctx.require("files.local")?;
477 let data = local_data_dir(&core, &ctx)?;
478 let directory = crate::core::files::local::checked_path(text(to, "directory"), &data)?;
479 let file_directory = directory.clone();
480 let pick_file: crate::core::files::transfers::PickSavePath = Box::new(move |name: String| {
481 Box::pin(async move { crate::core::files::service::safe_entry_name(&name).ok().map(|name| file_directory.join(name)) })
482 });
483 let pick_directory: crate::core::files::transfers::PickDirectory = Box::new(move || Box::pin(async move { Some(directory) }));
484 let options = crate::core::files::transfers::copy_download_options(&p["options"]);
485 core.services.transfers.plan_download(&ctx.id, text(from, "sessionId"), &from["paths"], &options, pick_file, pick_directory).await
486 }
487 _ => Err(crate::core::files::service::file_error("TRANSFER_INVALID_SELECTION", "Copy between two different panels; a local copy is not a transfer.")),
488 }
489});
490module_cmd!(module_transfers_list, "plugin:files:transfers:list", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.list(&ctx.id)));
491module_cmd!(module_transfers_plan_upload, "plugin:files:transfers:planUpload", Some("files.transfer"), |core, ctx, p| {
492 let (dialog_core, label) = (core.clone(), ctx.label.clone());
493 let pick: crate::core::files::transfers::PickPaths = Box::new(move || Box::pin(async move { Some(pick_upload_paths(&dialog_core, &label).await) }));
494 core.services.transfers.plan_upload(&ctx.id, text(&p, "sessionId"), &p["directory"], pick).await
495});
496module_cmd!(module_transfers_plan_download, "plugin:files:transfers:planDownload", Some("files.transfer"), |core, ctx, p| {
497 let (file_core, file_label) = (core.clone(), ctx.label.clone());
498 let pick_file: crate::core::files::transfers::PickSavePath = Box::new(move |name: String| {
499 Box::pin(async move {
500 let title = file_core.t("native.files.saveDownload");
501 let parent = crate::dialogs::host_window(&file_core.app, &file_label);
502 crate::dialogs::save_file(&file_core.app, parent.as_ref(), Some(&title), &name, &[]).await
503 })
504 });
505 let (dir_core, dir_label) = (core.clone(), ctx.label.clone());
506 let pick_directory: crate::core::files::transfers::PickDirectory = Box::new(move || {
507 Box::pin(async move {
508 let title = dir_core.t("native.files.selectDownloadDirectory");
509 let parent = crate::dialogs::host_window(&dir_core.app, &dir_label);
510 crate::dialogs::pick_folder(&dir_core.app, parent.as_ref(), Some(&title)).await
511 })
512 });
513 core.services.transfers.plan_download(&ctx.id, text(&p, "sessionId"), &p["paths"], &options(&p), pick_file, pick_directory).await
514});
515module_cmd!(module_transfers_start, "plugin:files:transfers:start", Some("files.transfer"), |core, ctx, p| core.services.transfers.start(&ctx.id, &p["planId"], &options(&p)).await);
516module_cmd!(module_transfers_pause, "plugin:files:transfers:pause", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.pause(&ctx.id, &transfer_ids(&p))));
517module_cmd!(module_transfers_resume, "plugin:files:transfers:resume", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.resume(&ctx.id, &transfer_ids(&p), &options(&p))));
518module_cmd!(module_transfers_retry, "plugin:files:transfers:retry", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.retry(&ctx.id, &transfer_ids(&p), &options(&p))));
519module_cmd!(module_transfers_cancel, "plugin:files:transfers:cancel", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.cancel(&ctx.id, &transfer_ids(&p)).await));
520module_cmd!(module_transfers_remove, "plugin:files:transfers:remove", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.remove(&ctx.id, &transfer_ids(&p))));
521module_cmd!(module_transfers_settings, "plugin:files:transfers:settings", Some("files.transfer"), |core, ctx, p| core.services.transfers.update_settings(p.get("settings").filter(|v| v.is_object()).unwrap_or(&json!({}))));
523// --- hashing ---------------------------------------------------------------------------------------
525module_cmd!(module_hashing_algorithms, "plugin:hashing:algorithms", Some("local.hash"), |core, ctx, p| Ok(core.services.hashing.algorithms()));
526module_cmd!(module_hashing_select_files, "plugin:hashing:selectFiles", Some("local.hash"), |core, ctx, p| {
527 let multiple = p["multiple"] == true;
528 let title = hash_text(&core, if multiple { "Select local files to hash" } else { "Select a local file to hash" });
529 let parent = crate::dialogs::host_window(&core.app, &ctx.label);
530 let picked = if multiple {
531 Some(crate::dialogs::pick_files(&core.app, parent.as_ref(), Some(&title), &[]).await).filter(|v| !v.is_empty())
532 } else {
533 crate::dialogs::pick_file(&core.app, parent.as_ref(), Some(&title), &[]).await.map(|p| vec![p])
534 };
535 core.services.hashing.select_files(&ctx.id, multiple, picked)
536});
537module_cmd!(module_hashing_select_directory, "plugin:hashing:selectDirectory", Some("local.hash"), |core, ctx, p| {
538 let title = hash_text(&core, "Select a local directory");
539 let parent = crate::dialogs::host_window(&core.app, &ctx.label);
540 let picked = crate::dialogs::pick_folder(&core.app, parent.as_ref(), Some(&title)).await;
541 core.services.hashing.select_directory(&ctx.id, picked)
542});
543module_cmd!(module_hashing_select_manifest, "plugin:hashing:selectManifest", Some("local.hash"), |core, ctx, p| {
544 let title = hash_text(&core, "Select a checksum manifest");
545 let name = hash_text(&core, "Checksum manifests");
546 let parent = crate::dialogs::host_window(&core.app, &ctx.label);
547 let filters = [crate::dialogs::Filter { name: &name, extensions: &["sha224", "sha256", "sha384", "sha512", "sha3", "blake3", "md5", "sha1", "sfv", "checksums", "txt"] }];
548 let picked = crate::dialogs::pick_file(&core.app, parent.as_ref(), Some(&title), &filters).await;
549 core.services.hashing.select_manifest(&ctx.id, picked)
550});
551module_cmd!(module_hashing_select_manifest_destination, "plugin:hashing:selectManifestDestination", Some("local.hash"), |core, ctx, p| {
552 let suggested = crate::core::hashing::service::LocalHashService::suggested_manifest_name(&p);
553 let title = hash_text(&core, "Save checksum manifest");
554 let parent = crate::dialogs::host_window(&core.app, &ctx.label);
555 let picked = crate::dialogs::save_file(&core.app, parent.as_ref(), Some(&title), &suggested, &[]).await;
556 core.services.hashing.select_manifest_destination(&ctx.id, picked)
557});
558module_cmd!(module_hashing_select_pgp_file, "plugin:hashing:selectPgpFile", Some("local.hash"), |core, ctx, p| {
559 let kind = if text(&p, "kind") == "key" { "key" } else { "signature" };
560 let title = hash_text(&core, if kind == "key" { "Select the signer's public key" } else { "Select the signature file" });
561 let parent = crate::dialogs::host_window(&core.app, &ctx.label);
562 let picked = crate::dialogs::pick_file(&core.app, parent.as_ref(), Some(&title), &[]).await;
563 core.services.hashing.select_pgp_file(&ctx.id, kind, picked)
564});
565module_cmd!(module_hashing_pgp_verify, "plugin:hashing:pgpVerify", Some("local.hash"), |core, ctx, p| core.services.hashing.pgp_verify(&ctx.id, &p, core.paths.in_data("pgp-keys")).await);
566module_cmd!(module_hashing_pgp_keys, "plugin:hashing:pgpKeys", Some("local.hash"), |core, ctx, p| Ok(crate::core::hashing::pgp::list_keys(&core.paths.in_data("pgp-keys"))));
567module_cmd!(module_hashing_pgp_remove_key, "plugin:hashing:pgpRemoveKey", Some("local.hash"), |core, ctx, p| crate::core::hashing::pgp::remove_key(&core.paths.in_data("pgp-keys"), text(&p, "fingerprint")));
568module_cmd!(module_hashing_start, "plugin:hashing:start", Some("local.hash"), |core, ctx, p| core.services.hashing.start(&ctx.id, &p));
569module_cmd!(module_hashing_verify, "plugin:hashing:verify", Some("local.hash"), |core, ctx, p| core.services.hashing.verify(&ctx.id, &p));
570module_cmd!(module_hashing_compare, "plugin:hashing:compare", Some("local.hash"), |core, ctx, p| core.services.hashing.compare(&ctx.id, &p));
571module_cmd!(module_hashing_generate_manifest, "plugin:hashing:generateManifest", Some("local.hash"), |core, ctx, p| core.services.hashing.generate_manifest(&ctx.id, &p));
572module_cmd!(module_hashing_verify_manifest, "plugin:hashing:verifyManifest", Some("local.hash"), |core, ctx, p| {
573 let (service, id) = (core.services.hashing.clone(), ctx.id.clone());
574 // Manifest parsing and path checks touch the disk: keep them off the async workers.
575 tokio::task::spawn_blocking(move || service.verify_manifest(&id, &p)).await.map_err(|_| CoreError::new("HASH_INTERNAL_ERROR", "An internal hashing error occurred."))?
576});
577module_cmd!(module_hashing_export_results, "plugin:hashing:exportResults", Some("local.hash"), |core, ctx, p| core.services.hashing.export_results(&ctx.id, &p));
578module_cmd!(module_hashing_copy_result, "plugin:hashing:copyResult", Some("local.hash"), |core, ctx, p| {
579 use tauri_plugin_clipboard_manager::ClipboardExt;
580 let (text, count) = core.services.hashing.copy_text(&ctx.id, &p)?;
581 core.app.clipboard().write_text(text).map_err(|_| CoreError::plain("Clipboard is unavailable."))?;
582 Ok(json!({ "ok": true, "count": count }))
583});
584module_cmd!(module_hashing_status, "plugin:hashing:status", Some("local.hash"), |core, ctx, p| core.services.hashing.status(&ctx.id, &p["jobId"]));
585module_cmd!(module_hashing_cancel, "plugin:hashing:cancel", Some("local.hash"), |core, ctx, p| core.services.hashing.cancel(&ctx.id, &p["jobId"]));
587pub fn _keep(app: &tauri::AppHandle) {
588 let _ = app.app_handle();
591#[cfg(test)]
592mod upload_tests {
593 use super::*;
594 use tauri_plugin_dialog::MessageDialogResult;
596 #[tokio::test]
597 async fn upload_choice_preserves_multiselection_and_cancel_opens_no_picker() {
598 for choice in [MessageDialogResult::Yes, MessageDialogResult::Custom("Soubory".into())] {
599 let paths = choose_upload_paths(choice, "Soubory", "Složky", async { vec!["a.txt".into(), "b.txt".into()] }, async { panic!("folder picker opened") }).await;
600 assert_eq!(paths, vec![std::path::PathBuf::from("a.txt"), "b.txt".into()]);
601 }
602 for choice in [MessageDialogResult::No, MessageDialogResult::Custom("Složky".into())] {
603 let paths = choose_upload_paths(choice, "Soubory", "Složky", async { panic!("file picker opened") }, async { vec!["dir-a".into(), "dir-b".into()] }).await;
604 assert_eq!(paths, vec![std::path::PathBuf::from("dir-a"), "dir-b".into()]);
605 }
606 let paths = choose_upload_paths(MessageDialogResult::Cancel, "Soubory", "Složky", async { panic!("file picker opened") }, async { panic!("folder picker opened") }).await;
607 assert!(paths.is_empty());
608 }
SHA-256dcd044c8d01ed6f54635d2d4149684bd2894194930e54eed4ed9b6baea21f62a

CH-J Proprietary Software License 1.14 · تُتاح الشيفرة المصدرية بموجب CH-J Proprietary Software License 1.14. وإتاحتها لا تغيّر شروط الترخيص ولا تمنح حقوقًا إضافية.

SHA-256 للأرشيفac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
الملفات