Mã nguồn đã công bố
Tải mã nguồn dạng ZIP
CH-J Tools
Duyệt thư mục và tệp của một bản phát hành ứng dụng cụ thể.
CH-J / Server Manager
1704 tệp
0.1.2-beta
Ngày thêm: · Ngày dựng:
1
//! Public Module API commands (Plugin API 1.7.0 compatible). Every command checks the2
//! calling module window and its manifest permission before touching a Core service.4
use super::{module_call, module_result, text, CommandResult};5
use crate::core::app::CoreSlot;6
use crate::core::error::{CoreError, CoreResult};7
use crate::core::logging::{normalize_limit, read_tail};8
use serde_json::{json, Value};9
use tauri::{Manager, State, Webview};11
macro_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
};23
}25
/// Native dialog texts of the Hash & Checksum module come from its own catalogs (as in 0.1.0).26
fn 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.i18n29
.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())38
}40
/// `payload.options || {}`.41
fn options(payload: &Value) -> Value {42
payload.get("options").filter(|v| v.is_object()).cloned().unwrap_or_else(|| json!({}))43
}45
/// JavaScript truthiness for flags passed as `Boolean(value)`.46
fn 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
}54
}56
/// 0.1.0 `ids(payload)`.57
fn 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
}63
}65
/// Explicit file/folder choice; both native pickers support multiple selections on every OS.66
async 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
}).await89
}91
async 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.await99
} else if choice == MessageDialogResult::No || choice == MessageDialogResult::Custom(folders.to_string()) {100
pick_folders.await101
} else { Vec::new() }102
}104
// --- window, language, sessions --------------------------------------------------------------106
module_cmd!(module_get_info, "plugin:getInfo", None, |core, ctx, p| Ok(ctx.manifest.clone()));107
module_cmd!(module_ui_get_language, "plugin:ui:getLanguage", None, |core, ctx, p| Ok(json!(core.language())));108
module_cmd!(module_close, "plugin:close", None, |core, ctx, p| {109
core.services.modules.close_label(&core, &ctx.label);110
Ok(json!({ "ok": true }))111
});112
module_cmd!(module_host_state, "plugin:host:state", None, |core, ctx, p| Ok(core.services.modules.host_state(&ctx.label)));113
module_cmd!(module_host_detach, "plugin:host:detach", None, |core, ctx, p| core.services.modules.detach(&core, &ctx.label));114
module_cmd!(module_host_attach, "plugin:host:attach", None, |core, ctx, p| core.services.modules.attach(&core, &ctx.label));115
module_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
});119
module_cmd!(module_sessions_list, "plugin:sessions:list", Some("session.read"), |core, ctx, p| Ok(Value::Array(core.services.sessions.list())));121
// --- system monitor ---------------------------------------------------------------------------123
module_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")).await125
});126
module_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 })).await128
});129
module_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 -------------------------------------------------------------------------------------133
module_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);134
module_cmd!(module_disks_smart, "plugin:disks:smart", Some("disks.read"), |core, ctx, p| core.services.disks.smart(text(&p, "sessionId"), &p).await);135
module_cmd!(module_disks_jobs, "plugin:disks:jobs", Some("disks.read"), |core, ctx, p| core.services.disks.jobs(text(&p, "sessionId"), &p).await);136
module_cmd!(module_disks_job_status, "plugin:disks:jobStatus", Some("disks.read"), |core, ctx, p| core.services.disks.job_status(text(&p, "sessionId"), &p).await);137
module_cmd!(module_disks_plan, "plugin:disks:plan", Some("disks.manage"), |core, ctx, p| core.services.disks.plan(text(&p, "sessionId"), &p).await);138
module_cmd!(module_disks_auto_expand, "plugin:disks:autoExpand", Some("disks.manage"), |core, ctx, p| core.services.disks.auto_expand(text(&p, "sessionId"), &p).await);139
module_cmd!(module_disks_cancel_plan, "plugin:disks:cancelPlan", Some("disks.manage"), |core, ctx, p| Ok(core.services.disks.cancel_plan(&p["planId"])));140
module_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 --------------------------------------------------------------------------------144
module_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);145
module_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
});156
module_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));157
module_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
});163
module_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
});172
module_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."));}entry183
};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 concurrent205
// 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
});212
module_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
});222
module_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
});230
module_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 and247
// fingerprint leave Core; private material and OpenPGP identities stay in the vault.248
module_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
});258
module_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 --------------------------------------------------------------------------------------265
module_cmd!(module_users_list, "plugin:users:list", Some("users.read"), |core, ctx, p| core.services.users.list(text(&p, "sessionId")).await);266
module_cmd!(module_users_privileges, "plugin:users:privileges", Some("users.read"), |core, ctx, p| core.services.users.privileges(text(&p, "sessionId"), p["refresh"] == true).await);267
module_cmd!(module_users_inventory, "plugin:users:inventory", Some("users.read"), |core, ctx, p| core.services.users.inventory(text(&p, "sessionId"), &p).await);268
module_cmd!(module_users_detail, "plugin:users:detail", Some("users.read"), |core, ctx, p| core.services.users.detail(text(&p, "sessionId"), &p).await);269
module_cmd!(module_users_manage, "plugin:users:manage", Some("users.manage"), |core, ctx, p| core.services.users.manage(text(&p, "sessionId"), &p).await);270
module_cmd!(module_users_keys, "plugin:users:keys", Some("users.manage"), |core, ctx, p| core.services.users.keys(text(&p, "sessionId"), &p).await);271
module_cmd!(module_users_ssh_policy, "plugin:users:sshPolicy", Some("users.manage"), |core, ctx, p| core.services.users.ssh_policy(text(&p, "sessionId"), &p).await);272
module_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 ---------------------------------------------------------------------------------------276
use crate::core::ssh::nginx;277
module_cmd!(module_nginx_inspect, "plugin:nginx:inspect", Some("nginx.read"), |core, ctx, p| nginx::inspect(&core.services.privileged, text(&p, "sessionId")).await);278
module_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);279
module_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);280
module_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);281
module_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);282
module_cmd!(module_nginx_reload, "plugin:nginx:reload", Some("nginx.manage"), |core, ctx, p| nginx::reload(&core.services.privileged, text(&p, "sessionId"), &p).await);283
module_cmd!(module_nginx_sites, "plugin:nginx:sites", Some("nginx.read"), |core, ctx, p| nginx::sites(&core.services.privileged, text(&p, "sessionId"), &p).await);284
module_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);285
module_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);286
module_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);287
module_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);288
module_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);289
module_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);290
module_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);291
module_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);292
module_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);293
module_cmd!(module_nginx_backups, "plugin:nginx:backups", Some("nginx.read"), |core, ctx, p| nginx::backups(&core.services.privileged, text(&p, "sessionId"), &p).await);294
module_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);295
module_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);296
module_cmd!(module_nginx_certificates, "plugin:nginx:certificates", Some("nginx.read"), |core, ctx, p| nginx::certificates(&core.services.privileged, text(&p, "sessionId"), &p).await);297
module_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);298
module_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);299
module_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);300
module_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 --------------------------------------------------------------------------------------304
use crate::core::ssh::apache;305
module_cmd!(module_apache_inspect, "plugin:apache:inspect", Some("apache.read"), |core, ctx, p| apache::inspect(&core.services.privileged, text(&p, "sessionId")).await);306
module_cmd!(module_apache_inventory, "plugin:apache:inventory", Some("apache.read"), |core, ctx, p| apache::inventory(&core.services.privileged, text(&p, "sessionId"), &p).await);307
module_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);308
module_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);309
module_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);310
module_cmd!(module_apache_backups, "plugin:apache:backups", Some("apache.read"), |core, ctx, p| apache::backups(&core.services.privileged, text(&p, "sessionId"), &p).await);311
module_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);312
module_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);313
module_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);314
module_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);315
module_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);316
module_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);317
module_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);318
module_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);319
module_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);320
module_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);321
module_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);322
module_cmd!(module_apache_reload, "plugin:apache:reload", Some("apache.manage"), |core, ctx, p| apache::reload(&core.services.privileged, text(&p, "sessionId"), &p).await);323
module_cmd!(module_network_inspect, "plugin:network:inspect", Some("network.read"), |core, ctx, p| core.services.network.inspect(text(&p, "sessionId"), &p).await);324
module_cmd!(module_network_pending, "plugin:network:pending", Some("network.read"), |core, ctx, p| core.services.network.pending(text(&p, "sessionId")).await);325
module_cmd!(module_network_diagnose, "plugin:network:diagnose", Some("network.read"), |core, ctx, p| core.services.network.diagnose(text(&p, "sessionId"), &p).await);326
module_cmd!(module_network_plan_interface, "plugin:network:planInterface", Some("network.manage"), |core, ctx, p| core.services.network.plan_interface(text(&p, "sessionId"), &p).await);327
module_cmd!(module_network_plan_firewall, "plugin:network:planFirewall", Some("network.manage"), |core, ctx, p| core.services.network.plan_firewall(text(&p, "sessionId"), &p).await);328
module_cmd!(module_network_apply, "plugin:network:apply", Some("network.manage"), |core, ctx, p| core.services.network.apply(&p).await);329
module_cmd!(module_network_verify, "plugin:network:verify", Some("network.manage"), |core, ctx, p| core.services.network.verify(&p).await);330
module_cmd!(module_network_confirm, "plugin:network:confirm", Some("network.manage"), |core, ctx, p| core.services.network.confirm(&p).await);331
module_cmd!(module_network_rollback, "plugin:network:rollback", Some("network.manage"), |core, ctx, p| core.services.network.rollback(&p).await);332
module_cmd!(module_network_extend, "plugin:network:extend", Some("network.manage"), |core, ctx, p| core.services.network.extend(&p).await);333
module_cmd!(module_network_transaction_log, "plugin:network:transactionLog", Some("network.manage"), |core, ctx, p| core.services.network.transaction_log(&p).await);334
module_cmd!(module_rsync_ssh_profiles, "plugin:rsync:sshProfiles", Some("rsync.read"), |core, ctx, p| core.services.rsync.ssh_profiles());335
module_cmd!(module_rsync_local_probe, "plugin:rsync:localProbe", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.local_probe().await));336
module_cmd!(module_rsync_remote_probe, "plugin:rsync:remoteProbe", Some("rsync.read"), |core, ctx, p| core.services.rsync.remote_probe(text(&p, "sessionId")).await);337
module_cmd!(module_rsync_inspect, "plugin:rsync:inspect", Some("rsync.read"), |core, ctx, p| core.services.rsync.inspect(&p).await);338
module_cmd!(module_rsync_jobs, "plugin:rsync:jobs", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.jobs()));339
module_cmd!(module_rsync_job, "plugin:rsync:job", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.job(text(&p, "id"))));340
module_cmd!(module_rsync_describe, "plugin:rsync:describe", Some("rsync.read"), |core, ctx, p| core.services.rsync.describe(&p));341
module_cmd!(module_rsync_versions, "plugin:rsync:versions", Some("rsync.read"), |core, ctx, p| core.services.rsync.versions(&p).await);342
module_cmd!(module_rsync_profiles, "plugin:rsync:profiles", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.profiles()));343
module_cmd!(module_rsync_history, "plugin:rsync:history", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.history()));344
module_cmd!(module_rsync_schedule_list, "plugin:rsync:scheduleList", Some("rsync.read"), |core, ctx, p| core.services.rsync.schedule_list(text(&p, "sessionId"), &p).await);345
module_cmd!(module_rsync_discover, "plugin:rsync:discover", Some("rsync.read"), |core, ctx, p| core.services.rsync.discover(text(&p, "sessionId"), &p).await);346
module_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);347
module_cmd!(module_rsync_take_handoff, "plugin:rsync:takeHandoff", Some("rsync.read"), |core, ctx, p| Ok(core.services.rsync.take_handoff()));348
module_cmd!(module_rsync_preview, "plugin:rsync:preview", Some("rsync.run"), |core, ctx, p| core.services.rsync.preview(&ctx.id, &p).await);349
module_cmd!(module_rsync_run, "plugin:rsync:run", Some("rsync.run"), |core, ctx, p| core.services.rsync.run(&ctx.id, &p));350
module_cmd!(module_rsync_cancel, "plugin:rsync:cancel", Some("rsync.run"), |core, ctx, p| core.services.rsync.cancel(text(&p, "id")).await);351
module_cmd!(module_rsync_install, "plugin:rsync:install", Some("rsync.run"), |core, ctx, p| core.services.rsync.install(text(&p, "sessionId"), &p).await);352
module_cmd!(module_rsync_connect, "plugin:rsync:connect", Some("rsync.run"), |core, ctx, p| core.services.rsync.connect(&p).await);353
module_cmd!(module_rsync_trust_host, "plugin:rsync:trustHost", Some("rsync.run"), |core, ctx, p| core.services.rsync.trust_host(&p));354
module_cmd!(module_rsync_disconnect, "plugin:rsync:disconnect", Some("rsync.run"), |core, ctx, p| core.services.rsync.disconnect(text(&p, "sessionId")).await);355
module_cmd!(module_rsync_save_profile, "plugin:rsync:saveProfile", Some("rsync.run"), |core, ctx, p| core.services.rsync.save_profile(&p));356
module_cmd!(module_rsync_delete_profile, "plugin:rsync:deleteProfile", Some("rsync.run"), |core, ctx, p| core.services.rsync.delete_profile(text(&p, "id")));357
module_cmd!(module_rsync_clear_history, "plugin:rsync:clearHistory", Some("rsync.run"), |core, ctx, p| core.services.rsync.clear_history());358
module_cmd!(module_rsync_prune_versions, "plugin:rsync:pruneVersions", Some("rsync.run"), |core, ctx, p| core.services.rsync.prune_versions(&p).await);359
module_cmd!(module_rsync_schedule_install, "plugin:rsync:scheduleInstall", Some("rsync.run"), |core, ctx, p| core.services.rsync.schedule_install(&p).await);360
module_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);361
module_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);362
module_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
});366
module_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);367
module_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 ---------------------------------------------------------------------------372
module_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);373
module_cmd!(module_files_privileges, "plugin:files:privileges", Some("files.read"), |core, ctx, p| core.services.files.privileges(text(&p, "sessionId")).await);374
module_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);375
module_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);376
module_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));377
module_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);378
module_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);379
module_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);380
module_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)));381
module_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);382
module_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);383
module_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);384
module_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);385
module_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);386
module_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).await389
});390
module_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
});400
module_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
});409
module_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.428
fn 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())433
}434
module_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
});438
module_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
});443
module_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
});447
module_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
});451
module_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
});455
module_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`.461
module_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).await474
}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).await486
}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
});490
module_cmd!(module_transfers_list, "plugin:files:transfers:list", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.list(&ctx.id)));491
module_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).await495
});496
module_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, &[]).await503
})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)).await511
})512
});513
core.services.transfers.plan_download(&ctx.id, text(&p, "sessionId"), &p["paths"], &options(&p), pick_file, pick_directory).await514
});515
module_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);516
module_cmd!(module_transfers_pause, "plugin:files:transfers:pause", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.pause(&ctx.id, &transfer_ids(&p))));517
module_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))));518
module_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))));519
module_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));520
module_cmd!(module_transfers_remove, "plugin:files:transfers:remove", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.remove(&ctx.id, &transfer_ids(&p))));521
module_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 ---------------------------------------------------------------------------------------525
module_cmd!(module_hashing_algorithms, "plugin:hashing:algorithms", Some("local.hash"), |core, ctx, p| Ok(core.services.hashing.algorithms()));526
module_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
});537
module_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
});543
module_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
});551
module_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
});558
module_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
});565
module_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);566
module_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"))));567
module_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")));568
module_cmd!(module_hashing_start, "plugin:hashing:start", Some("local.hash"), |core, ctx, p| core.services.hashing.start(&ctx.id, &p));569
module_cmd!(module_hashing_verify, "plugin:hashing:verify", Some("local.hash"), |core, ctx, p| core.services.hashing.verify(&ctx.id, &p));570
module_cmd!(module_hashing_compare, "plugin:hashing:compare", Some("local.hash"), |core, ctx, p| core.services.hashing.compare(&ctx.id, &p));571
module_cmd!(module_hashing_generate_manifest, "plugin:hashing:generateManifest", Some("local.hash"), |core, ctx, p| core.services.hashing.generate_manifest(&ctx.id, &p));572
module_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
});577
module_cmd!(module_hashing_export_results, "plugin:hashing:exportResults", Some("local.hash"), |core, ctx, p| core.services.hashing.export_results(&ctx.id, &p));578
module_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
});584
module_cmd!(module_hashing_status, "plugin:hashing:status", Some("local.hash"), |core, ctx, p| core.services.hashing.status(&ctx.id, &p["jobId"]));585
module_cmd!(module_hashing_cancel, "plugin:hashing:cancel", Some("local.hash"), |core, ctx, p| core.services.hashing.cancel(&ctx.id, &p["jobId"]));587
pub fn _keep(app: &tauri::AppHandle) {588
let _ = app.app_handle();589
}591
#[cfg(test)]592
mod 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
}609
}SHA-256
dcd044c8d01ed6f54635d2d4149684bd2894194930e54eed4ed9b6baea21f62aCH-J Proprietary Software License 1.14 · Mã nguồn được cung cấp theo CH-J Proprietary Software License 1.14. Việc công khai mã nguồn không thay đổi các điều khoản giấy phép và không cấp thêm quyền nào.
SHA-256 của tệp nén
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f