Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J Tools
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / Server Manager
872 Dateien
core-20261030T220507Z-61e4558d889f
Hinzugefügt: · Erstellt:
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 = core.app.get_webview_window(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_window_minimize, "plugin:window:minimize", None, |core, ctx, p| {113
core.services.modules.minimize_label(&core, &ctx.label, true);114
Ok(json!({ "ok": true }))115
});116
module_cmd!(module_sessions_list, "plugin:sessions:list", Some("session.read"), |core, ctx, p| Ok(Value::Array(core.services.sessions.list())));118
// --- system monitor ---------------------------------------------------------------------------120
module_cmd!(module_system_metrics, "plugin:system:metrics", Some("system.metrics.read"), |core, ctx, p| {121
crate::core::ssh::metrics::read_system_metrics(&core.services.sessions, text(&p, "sessionId")).await122
});123
module_cmd!(module_system_virtual_memory, "plugin:system:virtualMemory", Some("system.metrics.read"), |core, ctx, p| {124
core.services.virtual_memory.read(text(&p, "sessionId"), &json!({ "sudoPassword": p["sudoPassword"], "privileged": p["privileged"] == true })).await125
});126
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);128
// --- disks -------------------------------------------------------------------------------------130
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);131
module_cmd!(module_disks_smart, "plugin:disks:smart", Some("disks.read"), |core, ctx, p| core.services.disks.smart(text(&p, "sessionId"), &p).await);132
module_cmd!(module_disks_jobs, "plugin:disks:jobs", Some("disks.read"), |core, ctx, p| core.services.disks.jobs(text(&p, "sessionId"), &p).await);133
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);134
module_cmd!(module_disks_plan, "plugin:disks:plan", Some("disks.manage"), |core, ctx, p| core.services.disks.plan(text(&p, "sessionId"), &p).await);135
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);136
module_cmd!(module_disks_cancel_plan, "plugin:disks:cancelPlan", Some("disks.manage"), |core, ctx, p| Ok(core.services.disks.cancel_plan(&p["planId"])));137
module_cmd!(module_disks_apply, "plugin:disks:apply", Some("disks.manage"), |core, ctx, p| core.services.disks.apply(text(&p, "sessionId"), &p).await);139
// --- keys, logs --------------------------------------------------------------------------------141
module_cmd!(module_keys_generate, "plugin:keys:generate", Some("keys.generate"), |core, ctx, p| core.services.keys.generate(&p).await);142
module_cmd!(module_keys_save, "plugin:keys:save", Some("keys.generate"), |core, ctx, p| {143
let generated = core.services.keys.generated(text(&p, "generationId"))?;144
let title = core.i18n.t(&core.language(), "native.keys.saveTitle");145
let parent = core.app.get_webview_window(&ctx.label);146
let suggested = generated["suggestedName"].as_str().unwrap_or("id_key").to_string();147
match crate::dialogs::save_file(&core.app, parent.as_ref(), Some(&title), &suggested, &[]).await {148
Some(path) => crate::core::keys::save_pair(&path, &generated),149
None => Ok(json!({ "canceled": true })),150
}151
});152
module_cmd!(module_keys_password, "plugin:keys:password", Some("keys.generate"), |core, ctx, p| core.services.keys.generate_password(&p));153
module_cmd!(module_keys_copy_password, "plugin:keys:copyPassword", Some("keys.generate"), |core, ctx, p| {154
use tauri_plugin_clipboard_manager::ClipboardExt;155
let password = core.services.keys.password(text(&p, "passwordId"))?;156
core.app.clipboard().write_text(password.as_str()).map_err(|_| CoreError::plain("Clipboard is unavailable."))?;157
Ok(json!({ "copied": true }))158
});159
module_cmd!(module_logs_read, "plugin:logs:read", Some("logs.read"), |core, ctx, p| {160
let tail = read_tail(core.logger.path(), normalize_limit(p.get("maxBytes")))?;161
Ok(serde_json::to_value(tail)?)162
});164
// --- users --------------------------------------------------------------------------------------166
module_cmd!(module_users_list, "plugin:users:list", Some("users.read"), |core, ctx, p| core.services.users.list(text(&p, "sessionId")).await);167
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);168
module_cmd!(module_users_inventory, "plugin:users:inventory", Some("users.read"), |core, ctx, p| core.services.users.inventory(text(&p, "sessionId"), &p).await);169
module_cmd!(module_users_detail, "plugin:users:detail", Some("users.read"), |core, ctx, p| core.services.users.detail(text(&p, "sessionId"), &p).await);170
module_cmd!(module_users_manage, "plugin:users:manage", Some("users.manage"), |core, ctx, p| core.services.users.manage(text(&p, "sessionId"), &p).await);171
module_cmd!(module_users_keys, "plugin:users:keys", Some("users.manage"), |core, ctx, p| core.services.users.keys(text(&p, "sessionId"), &p).await);172
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);174
// --- nginx ---------------------------------------------------------------------------------------176
use crate::core::ssh::nginx;177
module_cmd!(module_nginx_inspect, "plugin:nginx:inspect", Some("nginx.read"), |core, ctx, p| nginx::inspect(&core.services.sessions, text(&p, "sessionId")).await);178
module_cmd!(module_nginx_read_config, "plugin:nginx:readConfig", Some("nginx.read"), |core, ctx, p| nginx::read_config(&core.services.sessions, text(&p, "sessionId"), &p).await);179
module_cmd!(module_nginx_dump_config, "plugin:nginx:dumpConfig", Some("nginx.read"), |core, ctx, p| nginx::dump_config(&core.services.sessions, text(&p, "sessionId"), &p).await);180
module_cmd!(module_nginx_test_config, "plugin:nginx:testConfig", Some("nginx.read"), |core, ctx, p| nginx::test_config(&core.services.sessions, text(&p, "sessionId"), &p).await);181
module_cmd!(module_nginx_save_config, "plugin:nginx:saveConfig", Some("nginx.manage"), |core, ctx, p| nginx::save_config(&core.services.sessions, text(&p, "sessionId"), &p).await);182
module_cmd!(module_nginx_reload, "plugin:nginx:reload", Some("nginx.manage"), |core, ctx, p| nginx::reload(&core.services.sessions, text(&p, "sessionId"), &p).await);184
// --- files / transfers ---------------------------------------------------------------------------186
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);187
module_cmd!(module_files_privileges, "plugin:files:privileges", Some("files.read"), |core, ctx, p| core.services.files.privileges(text(&p, "sessionId")).await);188
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);189
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);190
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));191
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);192
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);193
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);194
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)));195
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);196
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);197
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);198
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);199
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);200
module_cmd!(module_files_upload, "plugin:files:upload", Some("files.transfer"), |core, ctx, p| {201
let picked = pick_upload_paths(&core, &ctx.label).await;202
core.services.files.upload(text(&p, "sessionId"), &p["directory"], picked).await203
});204
module_cmd!(module_files_download, "plugin:files:download", Some("files.transfer"), |core, ctx, p| {205
let session = text(&p, "sessionId");206
let (target, size) = core.services.files.download_check(session, &p["path"]).await?;207
let title = core.t("native.files.saveDownload");208
let parent = core.app.get_webview_window(&ctx.label);209
match crate::dialogs::save_file(&core.app, parent.as_ref(), Some(&title), &crate::core::files::posix::basename(&target), &[]).await {210
Some(local) => core.services.files.download_to(session, &target, size, &local).await,211
None => Ok(json!({ "canceled": true })),212
}213
});214
module_cmd!(module_files_download_many, "plugin:files:downloadMany", Some("files.transfer"), |core, ctx, p| {215
let targets = crate::core::files::service::RemoteFileService::normalize_selection(&p["paths"])?;216
let title = core.t("native.files.selectDownloadDirectory");217
let parent = core.app.get_webview_window(&ctx.label);218
match crate::dialogs::pick_folder(&core.app, parent.as_ref(), Some(&title)).await {219
Some(directory) => core.services.files.download_many(text(&p, "sessionId"), &targets, &directory).await,220
None => Ok(json!({ "canceled": true, "items": [] })),221
}222
});223
module_cmd!(module_files_download_archive, "plugin:files:downloadArchive", Some("files.transfer"), |core, ctx, p| {224
use crate::core::files::service::{safe_entry_name, RemoteFileService};225
let targets = RemoteFileService::normalize_selection(&p["paths"])?;226
if targets.iter().any(|t| t == "/") {227
return Err(CoreError::plain("Archiving the entire remote root is not allowed."));228
}229
let format = RemoteFileService::archive_format(&p["format"])?;230
let base = if targets.len() == 1 { safe_entry_name(&crate::core::files::posix::basename(&targets[0]))? } else { "remote-selection".to_string() };231
let title = core.t("native.files.saveArchive");232
let parent = core.app.get_webview_window(&ctx.label);233
let extensions: &[&str] = match format.as_str() { "tar.gz" => &["tar.gz", "tgz"], "tar" => &["tar"], _ => &["zip"] };234
let upper = format.to_uppercase();235
let filters = [crate::dialogs::Filter { name: &upper, extensions }];236
match crate::dialogs::save_file(&core.app, parent.as_ref(), Some(&title), &format!("{base}.{format}"), &filters).await {237
Some(destination) => core.services.files.download_archive(text(&p, "sessionId"), &targets, &format, &destination).await,238
None => Ok(json!({ "canceled": true })),239
}240
});241
// The local disk belongs to the first-party File Manager only, whatever another module's manifest asks for.242
fn local_data_dir(core: &crate::core::app::Core, ctx: &crate::security::caller::ModuleContext) -> CoreResult<std::path::PathBuf> {243
if ctx.id != "chj.file-manager" {244
return Err(CoreError::new("MODULE_PERMISSION_DENIED", "Only the File Manager can use the local disk."));245
}246
Ok(core.paths.data.clone())247
}248
module_cmd!(module_local_places, "plugin:files:local:places", Some("files.local"), |core, ctx, p| {249
local_data_dir(&core, &ctx)?;250
Ok(crate::core::files::local::places())251
});252
module_cmd!(module_local_list, "plugin:files:local:list", Some("files.local"), |core, ctx, p| {253
let data = local_data_dir(&core, &ctx)?;254
let path = text(&p, "path").to_string();255
tokio::task::spawn_blocking(move || crate::core::files::local::list(&path, &data)).await.map_err(|_| CoreError::plain("Local folder read failed."))?256
});257
module_cmd!(module_local_mkdir, "plugin:files:local:mkdir", Some("files.local"), |core, ctx, p| {258
let data = local_data_dir(&core, &ctx)?;259
crate::core::files::local::mkdir(text(&p, "parent"), text(&p, "name"), &data)260
});261
module_cmd!(module_local_create_file, "plugin:files:local:createFile", Some("files.local"), |core, ctx, p| {262
let data = local_data_dir(&core, &ctx)?;263
crate::core::files::local::create_file(text(&p, "parent"), text(&p, "name"), &data)264
});265
module_cmd!(module_local_rename, "plugin:files:local:rename", Some("files.local"), |core, ctx, p| {266
let data = local_data_dir(&core, &ctx)?;267
crate::core::files::local::rename(text(&p, "path"), text(&p, "name"), &data)268
});269
module_cmd!(module_local_remove, "plugin:files:local:remove", Some("files.local"), |core, ctx, p| {270
let data = local_data_dir(&core, &ctx)?;271
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();272
tokio::task::spawn_blocking(move || crate::core::files::local::remove(&paths, &data)).await.map_err(|_| CoreError::plain("Local delete failed."))?273
});274
// Copy between panels: no picker, the destination comes from the panel. Local ends need `files.local`.275
module_cmd!(module_transfers_plan_copy, "plugin:files:transfers:planCopy", Some("files.transfer"), |core, ctx, p| {276
let (from, to) = (&p["source"], &p["destination"]);277
match (text(from, "kind"), text(to, "kind")) {278
("remote", "remote") => core.services.transfers.plan_relay(&ctx.id, text(from, "sessionId"), &from["paths"], text(to, "sessionId"), &to["directory"]).await,279
("local", "remote") => {280
ctx.require("files.local")?;281
let data = local_data_dir(&core, &ctx)?;282
let mut paths = Vec::new();283
for value in from["paths"].as_array().cloned().unwrap_or_default() {284
paths.push(crate::core::files::local::checked_path(value.as_str().unwrap_or(""), &data)?);285
}286
let pick: crate::core::files::transfers::PickPaths = Box::new(move || Box::pin(async move { Some(paths) }));287
core.services.transfers.plan_upload(&ctx.id, text(to, "sessionId"), &to["directory"], pick).await288
}289
("remote", "local") => {290
ctx.require("files.local")?;291
let data = local_data_dir(&core, &ctx)?;292
let directory = crate::core::files::local::checked_path(text(to, "directory"), &data)?;293
let file_directory = directory.clone();294
let pick_file: crate::core::files::transfers::PickSavePath = Box::new(move |name: String| {295
Box::pin(async move { crate::core::files::service::safe_entry_name(&name).ok().map(|name| file_directory.join(name)) })296
});297
let pick_directory: crate::core::files::transfers::PickDirectory = Box::new(move || Box::pin(async move { Some(directory) }));298
core.services.transfers.plan_download(&ctx.id, text(from, "sessionId"), &from["paths"], &json!({ "noSaveDialog": true }), pick_file, pick_directory).await299
}300
_ => Err(crate::core::files::service::file_error("TRANSFER_INVALID_SELECTION", "Copy between two different panels; a local copy is not a transfer.")),301
}302
});303
module_cmd!(module_transfers_list, "plugin:files:transfers:list", Some("files.transfer"), |core, ctx, p| Ok(core.services.transfers.list(&ctx.id)));304
module_cmd!(module_transfers_plan_upload, "plugin:files:transfers:planUpload", Some("files.transfer"), |core, ctx, p| {305
let (dialog_core, label) = (core.clone(), ctx.label.clone());306
let pick: crate::core::files::transfers::PickPaths = Box::new(move || Box::pin(async move { Some(pick_upload_paths(&dialog_core, &label).await) }));307
core.services.transfers.plan_upload(&ctx.id, text(&p, "sessionId"), &p["directory"], pick).await308
});309
module_cmd!(module_transfers_plan_download, "plugin:files:transfers:planDownload", Some("files.transfer"), |core, ctx, p| {310
let (file_core, file_label) = (core.clone(), ctx.label.clone());311
let pick_file: crate::core::files::transfers::PickSavePath = Box::new(move |name: String| {312
Box::pin(async move {313
let title = file_core.t("native.files.saveDownload");314
let parent = file_core.app.get_webview_window(&file_label);315
crate::dialogs::save_file(&file_core.app, parent.as_ref(), Some(&title), &name, &[]).await316
})317
});318
let (dir_core, dir_label) = (core.clone(), ctx.label.clone());319
let pick_directory: crate::core::files::transfers::PickDirectory = Box::new(move || {320
Box::pin(async move {321
let title = dir_core.t("native.files.selectDownloadDirectory");322
let parent = dir_core.app.get_webview_window(&dir_label);323
crate::dialogs::pick_folder(&dir_core.app, parent.as_ref(), Some(&title)).await324
})325
});326
core.services.transfers.plan_download(&ctx.id, text(&p, "sessionId"), &p["paths"], &options(&p), pick_file, pick_directory).await327
});328
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);329
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))));330
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))));331
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))));332
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));333
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))));334
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!({}))));336
// --- hashing ---------------------------------------------------------------------------------------338
module_cmd!(module_hashing_algorithms, "plugin:hashing:algorithms", Some("local.hash"), |core, ctx, p| Ok(core.services.hashing.algorithms()));339
module_cmd!(module_hashing_select_files, "plugin:hashing:selectFiles", Some("local.hash"), |core, ctx, p| {340
let multiple = p["multiple"] == true;341
let title = hash_text(&core, if multiple { "Select local files to hash" } else { "Select a local file to hash" });342
let parent = core.app.get_webview_window(&ctx.label);343
let picked = if multiple {344
Some(crate::dialogs::pick_files(&core.app, parent.as_ref(), Some(&title), &[]).await).filter(|v| !v.is_empty())345
} else {346
crate::dialogs::pick_file(&core.app, parent.as_ref(), Some(&title), &[]).await.map(|p| vec![p])347
};348
core.services.hashing.select_files(&ctx.id, multiple, picked)349
});350
module_cmd!(module_hashing_select_directory, "plugin:hashing:selectDirectory", Some("local.hash"), |core, ctx, p| {351
let title = hash_text(&core, "Select a local directory");352
let parent = core.app.get_webview_window(&ctx.label);353
let picked = crate::dialogs::pick_folder(&core.app, parent.as_ref(), Some(&title)).await;354
core.services.hashing.select_directory(&ctx.id, picked)355
});356
module_cmd!(module_hashing_select_manifest, "plugin:hashing:selectManifest", Some("local.hash"), |core, ctx, p| {357
let title = hash_text(&core, "Select a checksum manifest");358
let name = hash_text(&core, "Checksum manifests");359
let parent = core.app.get_webview_window(&ctx.label);360
let filters = [crate::dialogs::Filter { name: &name, extensions: &["sha224", "sha256", "sha384", "sha512", "sha3", "blake3", "md5", "sha1", "sfv", "checksums", "txt"] }];361
let picked = crate::dialogs::pick_file(&core.app, parent.as_ref(), Some(&title), &filters).await;362
core.services.hashing.select_manifest(&ctx.id, picked)363
});364
module_cmd!(module_hashing_select_manifest_destination, "plugin:hashing:selectManifestDestination", Some("local.hash"), |core, ctx, p| {365
let suggested = crate::core::hashing::service::LocalHashService::suggested_manifest_name(&p);366
let title = hash_text(&core, "Save checksum manifest");367
let parent = core.app.get_webview_window(&ctx.label);368
let picked = crate::dialogs::save_file(&core.app, parent.as_ref(), Some(&title), &suggested, &[]).await;369
core.services.hashing.select_manifest_destination(&ctx.id, picked)370
});371
module_cmd!(module_hashing_start, "plugin:hashing:start", Some("local.hash"), |core, ctx, p| core.services.hashing.start(&ctx.id, &p));372
module_cmd!(module_hashing_verify, "plugin:hashing:verify", Some("local.hash"), |core, ctx, p| core.services.hashing.verify(&ctx.id, &p));373
module_cmd!(module_hashing_compare, "plugin:hashing:compare", Some("local.hash"), |core, ctx, p| core.services.hashing.compare(&ctx.id, &p));374
module_cmd!(module_hashing_generate_manifest, "plugin:hashing:generateManifest", Some("local.hash"), |core, ctx, p| core.services.hashing.generate_manifest(&ctx.id, &p));375
module_cmd!(module_hashing_verify_manifest, "plugin:hashing:verifyManifest", Some("local.hash"), |core, ctx, p| {376
let (service, id) = (core.services.hashing.clone(), ctx.id.clone());377
// Manifest parsing and path checks touch the disk: keep them off the async workers.378
tokio::task::spawn_blocking(move || service.verify_manifest(&id, &p)).await.map_err(|_| CoreError::new("HASH_INTERNAL_ERROR", "An internal hashing error occurred."))?379
});380
module_cmd!(module_hashing_export_results, "plugin:hashing:exportResults", Some("local.hash"), |core, ctx, p| core.services.hashing.export_results(&ctx.id, &p));381
module_cmd!(module_hashing_copy_result, "plugin:hashing:copyResult", Some("local.hash"), |core, ctx, p| {382
use tauri_plugin_clipboard_manager::ClipboardExt;383
let (text, count) = core.services.hashing.copy_text(&ctx.id, &p)?;384
core.app.clipboard().write_text(text).map_err(|_| CoreError::plain("Clipboard is unavailable."))?;385
Ok(json!({ "ok": true, "count": count }))386
});387
module_cmd!(module_hashing_status, "plugin:hashing:status", Some("local.hash"), |core, ctx, p| core.services.hashing.status(&ctx.id, &p["jobId"]));388
module_cmd!(module_hashing_cancel, "plugin:hashing:cancel", Some("local.hash"), |core, ctx, p| core.services.hashing.cancel(&ctx.id, &p["jobId"]));390
pub fn _keep(app: &tauri::AppHandle) {391
let _ = app.app_handle();392
}394
#[cfg(test)]395
mod upload_tests {396
use super::*;397
use tauri_plugin_dialog::MessageDialogResult;399
#[tokio::test]400
async fn upload_choice_preserves_multiselection_and_cancel_opens_no_picker() {401
for choice in [MessageDialogResult::Yes, MessageDialogResult::Custom("Soubory".into())] {402
let paths = choose_upload_paths(choice, "Soubory", "Složky", async { vec!["a.txt".into(), "b.txt".into()] }, async { panic!("folder picker opened") }).await;403
assert_eq!(paths, vec![std::path::PathBuf::from("a.txt"), "b.txt".into()]);404
}405
for choice in [MessageDialogResult::No, MessageDialogResult::Custom("Složky".into())] {406
let paths = choose_upload_paths(choice, "Soubory", "Složky", async { panic!("file picker opened") }, async { vec!["dir-a".into(), "dir-b".into()] }).await;407
assert_eq!(paths, vec![std::path::PathBuf::from("dir-a"), "dir-b".into()]);408
}409
let paths = choose_upload_paths(MessageDialogResult::Cancel, "Soubory", "Složky", async { panic!("file picker opened") }, async { panic!("folder picker opened") }).await;410
assert!(paths.is_empty());411
}412
}SHA-256
cdde761f12c836722b135bf5fe71aae7cea8cf91906f703f73a03fd033db0ffdCH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
77380d4805485ffa4027fbe7858b5836f197a4aa9d52358a43e2d12d018744df