1pub mod actions;
3
4pub use actions::NfsClientProtocol;
5
6use anyhow::{Context, Result};
7use std::collections::HashMap;
8use std::net::SocketAddr;
9use std::sync::Arc;
10use tokio::sync::{mpsc, Mutex};
11use tracing::{error, info, trace, warn};
12
13use crate::client::llm_budget::call_llm_for_client;
14use crate::llm::actions::client_trait::{Client as ClientTrait, ClientActionResult};
15use crate::llm::ollama_client::OllamaClient;
16use crate::logging::emit::Log;
17use crate::protocol::Event;
18use crate::state::app_state::AppState;
19use crate::state::{ClientId, ClientStatus};
20use serde_json::json;
21
22#[cfg(unix)]
23use nfs3_client::{tokio::TokioConnector, Nfs3ConnectionBuilder};
24#[cfg(unix)]
25use nfs3_types::nfs3::*;
26
27use crate::client::nfs::actions::{NFS_CLIENT_CONNECTED_EVENT, NFS_CLIENT_OPERATION_RESULT_EVENT};
28
29#[cfg(unix)]
31type NfsConn = nfs3_client::Nfs3Connection<nfs3_client::tokio::TokioIo<tokio::net::TcpStream>>;
32
33pub struct NfsClient;
35
36impl NfsClient {
37 #[cfg(unix)]
39 pub async fn connect_with_llm_actions(
40 remote_addr: String,
41 llm_client: OllamaClient,
42 app_state: Arc<AppState>,
43 status_tx: mpsc::UnboundedSender<String>,
44 client_id: ClientId,
45 startup_params: Option<crate::protocol::StartupParams>,
46 ) -> Result<SocketAddr> {
47 let (server_addr, export_path) = Self::parse_nfs_address(&remote_addr)?;
50
51 Log::new(Some(&status_tx)).info(format!(
52 "NFS client {} attempting to connect to {} for export {}",
53 client_id, server_addr, export_path
54 ));
55
56 let server = server_addr.split(':').next().unwrap_or(&server_addr);
58
59 let params = startup_params.as_ref();
66 let port_param = |key: &str| -> Result<Option<u16>> {
67 let raw = params
68 .map(|p| p.get_optional_u64(key))
69 .transpose()?
70 .flatten();
71 match raw {
72 None => Ok(None),
73 Some(v) if (1..=65535).contains(&v) => Ok(Some(v as u16)),
74 Some(v) => Err(anyhow::anyhow!(
75 "NFS client '{key}' must be between 1 and 65535, got {v}"
76 )),
77 }
78 };
79 let portmapper_port = port_param("portmapper_port")?;
80 let mount_port = port_param("mount_port")?;
81 let nfs_port = port_param("nfs_port")?;
82 let privileged_source_port = params
83 .map(|p| p.get_optional_bool("privileged_source_port"))
84 .transpose()?
85 .flatten()
86 .unwrap_or(true);
87
88 let mut builder = Nfs3ConnectionBuilder::new(TokioConnector, server, &export_path)
90 .connect_from_privileged_port(privileged_source_port);
91 if let Some(port) = portmapper_port {
92 builder = builder.portmapper_port(port);
93 }
94 if let Some(port) = mount_port {
95 builder = builder.mount_port(port);
96 }
97 if let Some(port) = nfs_port {
98 builder = builder.nfs3_port(port);
99 }
100 let connection = builder
101 .mount()
102 .await
103 .context("Failed to mount NFS export")?;
104
105 Log::new(Some(&status_tx)).info(format!(
106 "NFS client {} successfully mounted export {}",
107 client_id, export_path
108 ));
109
110 let root_fh = connection.root_nfs_fh3();
112 let root_fh_hex = hex::encode(&root_fh.data.0);
113
114 app_state
116 .update_client_status(client_id, ClientStatus::Connected)
117 .await;
118 let _ = status_tx.send("__UPDATE_UI__".to_string());
119
120 let fh_cache = Arc::new(Mutex::new(HashMap::new()));
122 fh_cache
123 .lock()
124 .await
125 .insert("/".to_string(), root_fh.clone());
126
127 let connection_arc = Arc::new(Mutex::new(connection));
129
130 let command_rx =
136 crate::client::command_support::register_command_channel(&app_state, client_id).await;
137 let command_task = tokio::spawn(Self::command_loop(
138 connection_arc.clone(),
139 fh_cache.clone(),
140 command_rx,
141 client_id,
142 llm_client.clone(),
143 app_state.clone(),
144 status_tx.clone(),
145 ));
146 app_state
147 .register_client_task(client_id, command_task)
148 .await;
149
150 let app_state_clone = app_state.clone();
151 let llm_client_clone = llm_client.clone();
152 let status_tx_clone = status_tx.clone();
153 let fh_cache_clone = fh_cache.clone();
154
155 let task_registrar = app_state.clone();
158 let task_handle = tokio::spawn(async move {
159 if let Err(e) = Self::handle_nfs_operations(
160 connection_arc,
161 fh_cache_clone,
162 client_id,
163 export_path,
164 root_fh_hex,
165 llm_client_clone,
166 app_state_clone,
167 status_tx_clone,
168 )
169 .await
170 {
171 error!("NFS client {} handler error: {}", client_id, e);
172 }
173 });
174 task_registrar
175 .register_client_task(client_id, task_handle)
176 .await;
177
178 let shown_port = nfs_port.unwrap_or(2049);
193 let shown_addr = server
194 .parse::<std::net::IpAddr>()
195 .map(|ip| SocketAddr::new(ip, shown_port))
196 .unwrap_or_else(|_| {
197 SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)
198 });
199 Ok(shown_addr)
200 }
201
202 #[cfg(unix)]
204 async fn handle_nfs_operations(
205 connection: Arc<Mutex<NfsConn>>,
206 fh_cache: Arc<Mutex<HashMap<String, nfs_fh3>>>,
207 client_id: ClientId,
208 export_path: String,
209 root_fh_hex: String,
210 llm_client: OllamaClient,
211 app_state: Arc<AppState>,
212 status_tx: mpsc::UnboundedSender<String>,
213 ) -> Result<()> {
214 let instruction = app_state
216 .get_instruction_for_client(client_id)
217 .await
218 .unwrap_or_default();
219
220 let protocol = Arc::new(NfsClientProtocol::new());
221
222 let connected_event = Event::new(
224 &NFS_CLIENT_CONNECTED_EVENT,
225 json!({
226 "export_path": export_path,
227 "root_fh": root_fh_hex
228 }),
229 );
230
231 let memory = app_state
232 .get_memory_for_client(client_id)
233 .await
234 .unwrap_or_default();
235
236 let result = call_llm_for_client(
237 &llm_client,
238 &app_state,
239 client_id.to_string(),
240 &instruction,
241 &memory,
242 Some(&connected_event),
243 protocol.as_ref(),
244 &status_tx,
245 )
246 .await?;
247
248 if let Some(new_memory) = result.memory_updates {
250 app_state.set_memory_for_client(client_id, new_memory).await;
251 }
252
253 for action in result.actions {
255 if let Err(e) = Self::execute_action(
256 &connection,
257 &fh_cache,
258 action,
259 client_id,
260 &llm_client,
261 &app_state,
262 &status_tx,
263 protocol.as_ref(),
264 )
265 .await
266 {
267 Log::new(Some(&status_tx)).error(format!("NFS operation failed: {}", e));
268 }
269 }
270
271 loop {
273 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
274
275 if app_state.get_client(client_id).await.is_none() {
276 info!("NFS client {} stopped", client_id);
277 break;
278 }
279 }
280
281 Ok(())
282 }
283
284 #[cfg(unix)]
286 async fn execute_action(
287 connection: &Arc<Mutex<NfsConn>>,
288 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
289 action: serde_json::Value,
290 client_id: ClientId,
291 llm_client: &OllamaClient,
292 app_state: &Arc<AppState>,
293 status_tx: &mpsc::UnboundedSender<String>,
294 protocol: &dyn ClientTrait,
295 ) -> Result<()> {
296 let action_result = protocol.execute_action(action.clone())?;
297
298 match action_result {
299 ClientActionResult::Disconnect => {
300 info!("NFS client {} disconnecting", client_id);
301 app_state.remove_client_handle(client_id).await;
303 app_state
304 .update_client_status(client_id, ClientStatus::Disconnected)
305 .await;
306 return Ok(());
307 }
308 ClientActionResult::WaitForMore => {
309 trace!("NFS client {} waiting", client_id);
310 return Ok(());
311 }
312 ClientActionResult::Custom { name, data } => {
313 let result_data = match Self::perform_op(connection, fh_cache, &name, data).await {
321 Ok(result_data) => {
322 info!("NFS client {} completed operation: {}", client_id, name);
323 result_data
324 }
325 Err(e) => {
326 warn!("NFS client {} operation {} failed: {}", client_id, name, e);
327 serde_json::json!({
328 "success": false,
329 "error": e.to_string(),
330 })
331 }
332 };
333
334 Box::pin(Self::report_operation(
335 connection,
336 fh_cache,
337 &name,
338 result_data,
339 client_id,
340 llm_client,
341 app_state,
342 status_tx,
343 ))
344 .await?;
345 }
346 _ => {}
347 }
348
349 Ok(())
350 }
351
352 #[cfg(unix)]
359 #[allow(clippy::too_many_arguments)]
360 async fn report_operation(
361 connection: &Arc<Mutex<NfsConn>>,
362 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
363 name: &str,
364 result_data: serde_json::Value,
365 client_id: ClientId,
366 llm_client: &OllamaClient,
367 app_state: &Arc<AppState>,
368 status_tx: &mpsc::UnboundedSender<String>,
369 ) -> Result<()> {
370 let protocol = NfsClientProtocol::new();
371
372 let result_event = Event::new(
373 &NFS_CLIENT_OPERATION_RESULT_EVENT,
374 json!({
375 "operation": name,
376 "result": result_data
377 }),
378 );
379
380 let instruction = app_state
381 .get_instruction_for_client(client_id)
382 .await
383 .unwrap_or_default();
384 let memory = app_state
385 .get_memory_for_client(client_id)
386 .await
387 .unwrap_or_default();
388
389 let llm_result = call_llm_for_client(
390 llm_client,
391 app_state,
392 client_id.to_string(),
393 &instruction,
394 &memory,
395 Some(&result_event),
396 &protocol,
397 status_tx,
398 )
399 .await?;
400
401 if let Some(new_memory) = llm_result.memory_updates {
402 app_state.set_memory_for_client(client_id, new_memory).await;
403 }
404
405 for next_action in llm_result.actions {
406 Box::pin(Self::execute_action(
407 connection,
408 fh_cache,
409 next_action,
410 client_id,
411 llm_client,
412 app_state,
413 status_tx,
414 &protocol,
415 ))
416 .await?;
417 }
418 Ok(())
419 }
420
421 #[cfg(unix)]
426 async fn perform_op(
427 connection: &Arc<Mutex<NfsConn>>,
428 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
429 name: &str,
430 data: serde_json::Value,
431 ) -> Result<serde_json::Value> {
432 match name {
433 "nfs_lookup" => Self::op_lookup(connection, fh_cache, data).await,
434 "nfs_read_file" => Self::op_read_file(connection, fh_cache, data).await,
435 "nfs_write_file" => Self::op_write_file(connection, fh_cache, data).await,
436 "nfs_list_dir" => Self::op_list_dir(connection, fh_cache, data).await,
437 "nfs_get_attr" => Self::op_get_attr(connection, fh_cache, data).await,
438 "nfs_create_file" => Self::op_create_file(connection, fh_cache, data).await,
439 "nfs_mkdir" => Self::op_mkdir(connection, fh_cache, data).await,
440 "nfs_remove" => Self::op_remove(connection, fh_cache, data).await,
441 "nfs_rmdir" => Self::op_rmdir(connection, fh_cache, data).await,
442 other => Err(anyhow::anyhow!("Unknown NFS operation: {}", other)),
443 }
444 }
445
446 #[cfg(unix)]
457 #[allow(clippy::too_many_arguments)]
458 async fn command_loop(
459 connection: Arc<Mutex<NfsConn>>,
460 fh_cache: Arc<Mutex<HashMap<String, nfs_fh3>>>,
461 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
462 client_id: ClientId,
463 llm_client: OllamaClient,
464 app_state: Arc<AppState>,
465 status_tx: mpsc::UnboundedSender<String>,
466 ) {
467 use crate::llm::actions::protocol_trait::Protocol;
468 use crate::state::client_handles::ClientSendOutcome;
469 use crate::state::AccessLogOwner;
470
471 let protocol = Arc::new(NfsClientProtocol::new());
472
473 while let Some(command) = command_rx.recv().await {
474 let action = command.action.clone();
475 let mut follow_up: Option<(String, serde_json::Value)> = None;
476 let mut disconnect = false;
477
478 let outcome: Result<ClientSendOutcome> = match protocol.execute_action(action.clone()) {
479 Err(e) => Ok(ClientSendOutcome::Rejected {
480 error: e.to_string(),
481 }),
482 Ok(ClientActionResult::Disconnect) => {
483 disconnect = true;
484 Ok(ClientSendOutcome::Disconnected)
485 }
486 Ok(ClientActionResult::WaitForMore) => Ok(ClientSendOutcome::Executed {
487 detail: "wait_for_more: nothing was asked of the export".to_string(),
488 }),
489 Ok(ClientActionResult::NoAction) => Ok(ClientSendOutcome::Executed {
490 detail: "no_action".to_string(),
491 }),
492 Ok(ClientActionResult::Custom { name, data }) => {
493 match Self::perform_op(&connection, &fh_cache, &name, data).await {
494 Ok(result) => {
495 let detail = format!("{name} completed: {result}");
496 follow_up = Some((name, result));
497 Ok(ClientSendOutcome::Executed { detail })
498 }
499 Err(e) => Err(e.context(format!("injected NFS action '{name}'"))),
500 }
501 }
502 Ok(other) => Ok(ClientSendOutcome::Executed {
503 detail: format!("unsupported action result {other:?}"),
504 }),
505 };
506
507 let outcome_json = match &outcome {
508 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
509 Err(e) => serde_json::json!({"error": e.to_string()}),
510 };
511 app_state
512 .record_access_log(
513 AccessLogOwner::Client(client_id.as_u32()),
514 protocol.protocol_name(),
515 None,
516 "injected_action",
517 action,
518 vec![outcome_json],
519 )
520 .await;
521
522 if let Err(e) = &outcome {
523 error!("NFS client {} injected action failed: {}", client_id, e);
524 let _ = status_tx.send(format!(
525 "[WARN] Client {} injected action failed: {}",
526 client_id, e
527 ));
528 }
529 let _ = status_tx.send("__UPDATE_UI__".to_string());
530 crate::client::command_support::reply(command, outcome);
531
532 if disconnect {
533 app_state
534 .update_client_status(client_id, ClientStatus::Disconnected)
535 .await;
536 break;
537 }
538
539 if let Some((name, result)) = follow_up {
542 let connection = connection.clone();
543 let fh_cache = fh_cache.clone();
544 let llm_client = llm_client.clone();
545 let state = app_state.clone();
546 let tx = status_tx.clone();
547 let handle = tokio::spawn(async move {
548 if let Err(e) = Self::report_operation(
549 &connection,
550 &fh_cache,
551 &name,
552 result,
553 client_id,
554 &llm_client,
555 &state,
556 &tx,
557 )
558 .await
559 {
560 error!("NFS client {} follow-up failed: {}", client_id, e);
561 }
562 });
563 app_state.register_client_task(client_id, handle).await;
564 }
565 }
566
567 app_state.remove_client_handle(client_id).await;
570 let _ = status_tx.send("__UPDATE_UI__".to_string());
571 }
572
573 #[cfg(unix)]
575 async fn resolve_path(
576 connection: &Arc<Mutex<NfsConn>>,
577 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
578 path: &str,
579 ) -> Result<nfs_fh3> {
580 {
582 let cache = fh_cache.lock().await;
583 if let Some(fh) = cache.get(path) {
584 return Ok(fh.clone());
585 }
586 }
587
588 let components: Vec<&str> = path
590 .trim_start_matches('/')
591 .split('/')
592 .filter(|s| !s.is_empty())
593 .collect();
594
595 if components.is_empty() {
596 let conn = connection.lock().await;
597 return Ok(conn.root_nfs_fh3());
598 }
599
600 let mut current_fh = {
602 let conn = connection.lock().await;
603 conn.root_nfs_fh3()
604 };
605 let mut current_path = String::from("/");
606
607 for component in components {
608 let lookup_args = LOOKUP3args {
609 what: diropargs3 {
610 dir: current_fh.clone(),
611 name: component.as_bytes().into(),
612 },
613 };
614
615 let lookup_res = connection.lock().await.lookup(&lookup_args).await?;
616
617 match lookup_res {
618 LOOKUP3res::Ok(ok_res) => {
619 current_fh = ok_res.object;
620 current_path = if current_path == "/" {
621 format!("/{}", component)
622 } else {
623 format!("{}/{}", current_path, component)
624 };
625 fh_cache
626 .lock()
627 .await
628 .insert(current_path.clone(), current_fh.clone());
629 }
630 LOOKUP3res::Err((stat, _)) => {
631 return Err(anyhow::anyhow!(
632 "Lookup failed for {}: {:?}",
633 component,
634 stat
635 ));
636 }
637 }
638 }
639
640 Ok(current_fh)
641 }
642
643 #[cfg(unix)]
645 async fn op_lookup(
646 connection: &Arc<Mutex<NfsConn>>,
647 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
648 data: serde_json::Value,
649 ) -> Result<serde_json::Value> {
650 let path = data["path"].as_str().context("Missing 'path' parameter")?;
651 let fh = Self::resolve_path(connection, fh_cache, path).await?;
652
653 Ok(json!({
654 "path": path,
655 "file_handle": hex::encode(&fh.data.0),
656 "success": true
657 }))
658 }
659
660 #[cfg(unix)]
662 async fn op_read_file(
663 connection: &Arc<Mutex<NfsConn>>,
664 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
665 data: serde_json::Value,
666 ) -> Result<serde_json::Value> {
667 let path = data["path"].as_str().context("Missing 'path' parameter")?;
668 let offset = data["offset"].as_u64().unwrap_or(0);
669 let count = crate::client::wire_values::number::<u32>(&data, "count", 4096)?;
670
671 let fh = Self::resolve_path(connection, fh_cache, path).await?;
672
673 let read_args = READ3args {
674 file: fh,
675 offset,
676 count,
677 };
678
679 let read_res = connection.lock().await.read(&read_args).await?;
680
681 match read_res {
682 READ3res::Ok(ok_res) => {
683 let data_str = String::from_utf8_lossy(&ok_res.data.0).to_string();
684 Ok(json!({
685 "path": path,
686 "data": data_str,
687 "bytes_read": ok_res.count,
688 "eof": ok_res.eof,
689 "success": true
690 }))
691 }
692 READ3res::Err((stat, _)) => Err(anyhow::anyhow!("Read failed: {:?}", stat)),
693 }
694 }
695
696 #[cfg(unix)]
698 async fn op_write_file(
699 connection: &Arc<Mutex<NfsConn>>,
700 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
701 data: serde_json::Value,
702 ) -> Result<serde_json::Value> {
703 let path = data["path"].as_str().context("Missing 'path' parameter")?;
704 let write_data = data["data"].as_str().context("Missing 'data' parameter")?;
705 let offset = data["offset"].as_u64().unwrap_or(0);
706
707 let fh = Self::resolve_path(connection, fh_cache, path).await?;
708
709 let write_args = WRITE3args {
710 file: fh,
711 offset,
712 count: u32::try_from(write_data.len()).context("NFS write exceeds u32 length")?,
713 stable: stable_how::FILE_SYNC,
714 data: write_data.as_bytes().into(),
715 };
716
717 let write_res = connection.lock().await.write(&write_args).await?;
718
719 match write_res {
720 WRITE3res::Ok(ok_res) => Ok(json!({
721 "path": path,
722 "bytes_written": ok_res.count,
723 "success": true
724 })),
725 WRITE3res::Err((stat, _)) => Err(anyhow::anyhow!("Write failed: {:?}", stat)),
726 }
727 }
728
729 #[cfg(unix)]
731 async fn op_list_dir(
732 connection: &Arc<Mutex<NfsConn>>,
733 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
734 data: serde_json::Value,
735 ) -> Result<serde_json::Value> {
736 let path = data["path"].as_str().unwrap_or("/");
737 let fh = Self::resolve_path(connection, fh_cache, path).await?;
738
739 let readdir_args = READDIR3args {
740 dir: fh,
741 cookie: 0,
742 cookieverf: cookieverf3([0; 8]),
743 count: 4096,
744 };
745
746 let readdir_res = connection.lock().await.readdir(&readdir_args).await?;
747
748 match readdir_res {
749 READDIR3res::Ok(ok_res) => {
750 let mut entries = Vec::new();
751
752 for entry in &ok_res.reply.entries.0 {
754 entries.push(json!({
755 "name": String::from_utf8_lossy(entry.name.as_ref()),
756 "fileid": entry.fileid
757 }));
758 }
759
760 Ok(json!({
761 "path": path,
762 "entries": entries,
763 "success": true
764 }))
765 }
766 READDIR3res::Err((stat, _)) => Err(anyhow::anyhow!("Readdir failed: {:?}", stat)),
767 }
768 }
769
770 #[cfg(unix)]
772 async fn op_get_attr(
773 connection: &Arc<Mutex<NfsConn>>,
774 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
775 data: serde_json::Value,
776 ) -> Result<serde_json::Value> {
777 let path = data["path"].as_str().context("Missing 'path' parameter")?;
778 let fh = Self::resolve_path(connection, fh_cache, path).await?;
779
780 let getattr_args = GETATTR3args { object: fh };
781
782 let getattr_res = connection.lock().await.getattr(&getattr_args).await?;
783
784 match getattr_res {
785 GETATTR3res::Ok(ok_res) => Ok(json!({
786 "path": path,
787 "type": format!("{:?}", ok_res.obj_attributes.type_),
788 "size": ok_res.obj_attributes.size,
789 "mode": ok_res.obj_attributes.mode,
790 "success": true
791 })),
792 GETATTR3res::Err(stat) => Err(anyhow::anyhow!("Getattr failed: {:?}", stat)),
793 }
794 }
795
796 #[cfg(unix)]
798 async fn op_create_file(
799 connection: &Arc<Mutex<NfsConn>>,
800 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
801 data: serde_json::Value,
802 ) -> Result<serde_json::Value> {
803 let path = data["path"].as_str().context("Missing 'path' parameter")?;
804 let mode = crate::client::wire_values::number::<u32>(&data, "mode", 0o644)?;
805
806 let (dir_path, filename) = path.rsplit_once('/').unwrap_or(("/", path));
807 let dir_fh = Self::resolve_path(connection, fh_cache, dir_path).await?;
808
809 let create_args = CREATE3args {
810 where_: diropargs3 {
811 dir: dir_fh,
812 name: filename.as_bytes().into(),
813 },
814 how: createhow3::UNCHECKED(sattr3 {
815 mode: Nfs3Option::Some(mode),
816 uid: Nfs3Option::None,
817 gid: Nfs3Option::None,
818 size: Nfs3Option::None,
819 atime: set_atime::DONT_CHANGE,
820 mtime: set_mtime::DONT_CHANGE,
821 }),
822 };
823
824 let create_res = connection.lock().await.create(&create_args).await?;
825
826 match create_res {
827 CREATE3res::Ok(ok_res) => {
828 if let Nfs3Option::Some(obj) = ok_res.obj {
829 fh_cache.lock().await.insert(path.to_string(), obj);
830 }
831 Ok(json!({
832 "path": path,
833 "created": true,
834 "success": true
835 }))
836 }
837 CREATE3res::Err((stat, _)) => Err(anyhow::anyhow!("Create failed: {:?}", stat)),
838 }
839 }
840
841 #[cfg(unix)]
843 async fn op_mkdir(
844 connection: &Arc<Mutex<NfsConn>>,
845 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
846 data: serde_json::Value,
847 ) -> Result<serde_json::Value> {
848 let path = data["path"].as_str().context("Missing 'path' parameter")?;
849 let mode = crate::client::wire_values::number::<u32>(&data, "mode", 0o755)?;
850
851 let (dir_path, dirname) = path.rsplit_once('/').unwrap_or(("/", path));
852 let dir_fh = Self::resolve_path(connection, fh_cache, dir_path).await?;
853
854 let mkdir_args = MKDIR3args {
855 where_: diropargs3 {
856 dir: dir_fh,
857 name: dirname.as_bytes().into(),
858 },
859 attributes: sattr3 {
860 mode: Nfs3Option::Some(mode),
861 uid: Nfs3Option::None,
862 gid: Nfs3Option::None,
863 size: Nfs3Option::None,
864 atime: set_atime::DONT_CHANGE,
865 mtime: set_mtime::DONT_CHANGE,
866 },
867 };
868
869 let mkdir_res = connection.lock().await.mkdir(&mkdir_args).await?;
870
871 match mkdir_res {
872 MKDIR3res::Ok(ok_res) => {
873 if let Nfs3Option::Some(obj) = ok_res.obj {
874 fh_cache.lock().await.insert(path.to_string(), obj);
875 }
876 Ok(json!({
877 "path": path,
878 "created": true,
879 "success": true
880 }))
881 }
882 MKDIR3res::Err((stat, _)) => Err(anyhow::anyhow!("Mkdir failed: {:?}", stat)),
883 }
884 }
885
886 #[cfg(unix)]
888 async fn op_remove(
889 connection: &Arc<Mutex<NfsConn>>,
890 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
891 data: serde_json::Value,
892 ) -> Result<serde_json::Value> {
893 let path = data["path"].as_str().context("Missing 'path' parameter")?;
894
895 let (dir_path, filename) = path.rsplit_once('/').unwrap_or(("/", path));
896 let dir_fh = Self::resolve_path(connection, fh_cache, dir_path).await?;
897
898 let remove_args = REMOVE3args {
899 object: diropargs3 {
900 dir: dir_fh,
901 name: filename.as_bytes().into(),
902 },
903 };
904
905 let remove_res = connection.lock().await.remove(&remove_args).await?;
906
907 match remove_res {
908 REMOVE3res::Ok(_) => {
909 fh_cache.lock().await.remove(path);
910 Ok(json!({
911 "path": path,
912 "removed": true,
913 "success": true
914 }))
915 }
916 REMOVE3res::Err((stat, _)) => Err(anyhow::anyhow!("Remove failed: {:?}", stat)),
917 }
918 }
919
920 #[cfg(unix)]
922 async fn op_rmdir(
923 connection: &Arc<Mutex<NfsConn>>,
924 fh_cache: &Arc<Mutex<HashMap<String, nfs_fh3>>>,
925 data: serde_json::Value,
926 ) -> Result<serde_json::Value> {
927 let path = data["path"].as_str().context("Missing 'path' parameter")?;
928
929 let (dir_path, dirname) = path.rsplit_once('/').unwrap_or(("/", path));
930 let dir_fh = Self::resolve_path(connection, fh_cache, dir_path).await?;
931
932 let rmdir_args = RMDIR3args {
933 object: diropargs3 {
934 dir: dir_fh,
935 name: dirname.as_bytes().into(),
936 },
937 };
938
939 let rmdir_res = connection.lock().await.rmdir(&rmdir_args).await?;
940
941 match rmdir_res {
942 RMDIR3res::Ok(_) => {
943 fh_cache.lock().await.remove(path);
944 Ok(json!({
945 "path": path,
946 "removed": true,
947 "success": true
948 }))
949 }
950 RMDIR3res::Err((stat, _)) => Err(anyhow::anyhow!("Rmdir failed: {:?}", stat)),
951 }
952 }
953
954 fn parse_nfs_address(addr: &str) -> Result<(String, String)> {
957 if let Some(pos) = addr.rfind(':') {
959 let (server_part, path_part) = addr.split_at(pos);
960 let path = path_part.trim_start_matches(':');
961
962 if path.starts_with('/') {
964 let server = if !server_part.contains(':') {
965 format!("{}:2049", server_part)
966 } else {
967 server_part.to_string()
968 };
969 return Ok((server, path.to_string()));
970 }
971 }
972
973 Err(anyhow::anyhow!(
975 "Invalid NFS address format. Expected: server:port:/export/path or server:/export/path"
976 ))
977 }
978
979 #[cfg(not(unix))]
981 pub async fn connect_with_llm_actions(
982 _remote_addr: String,
983 _llm_client: OllamaClient,
984 _app_state: Arc<AppState>,
985 status_tx: mpsc::UnboundedSender<String>,
986 _client_id: ClientId,
987 _startup_params: Option<crate::protocol::StartupParams>,
988 ) -> Result<SocketAddr> {
989 Log::new(Some(&status_tx)).error("NFS feature not enabled at compile time".to_string());
990 Err(anyhow::anyhow!("NFS feature not enabled"))
991 }
992}