1use std::io::Cursor;
13use std::time::Duration;
14
15use nfs::v4::blocking::Client;
16use nfs::v4::{
17 ACCESS4_DELETE, ACCESS4_EXTEND, ACCESS4_LOOKUP, ACCESS4_MODIFY, ACCESS4_READ, BasicAttributes,
18 DirPageCursor, FATTR4_MODE, FATTR4_OWNER, FATTR4_OWNER_GROUP, FATTR4_SIZE,
19};
20use nfs::{AuthSys, RetryPolicy};
21
22const OBJECT: &[u8] = b"nfs-rs v4 cookbook object\nline two\nline three\n";
23const STREAMED: &[u8] = b"streamed through NFSv4 open/write/close\n";
24
25fn main() -> nfs::Result<()> {
26 let config = Config::from_env();
27 let mut client = connect(&config.host)?;
28
29 println!("connected to NFSv4 host {}", config.host);
30 if let Some(info) = client.root_fsinfo() {
31 println!(
32 "root fsinfo: max_read={:?} max_write={:?} lease={:?}",
33 info.max_read, info.max_write, info.lease_time_seconds
34 );
35 }
36
37 let work_dir = remote_path(
38 &config.remote_dir,
39 &format!("nfs-rs-v4-cookbook-{}", std::process::id()),
40 );
41 client.remove_all_if_exists(&work_dir)?;
42 client.create_dir_all(&work_dir, 0o755)?;
43
44 let flow_result = run_filesystem_flow(&mut client, &work_dir);
45 let cleanup_result = client.remove_all_if_exists(&work_dir);
46 let shutdown_result = client.shutdown();
47 finish(flow_result, cleanup_result, shutdown_result)
48}
49
50fn connect(host: &str) -> nfs::Result<Client> {
51 let process_id = std::process::id();
52 Client::builder(host.to_owned())
53 .auth_sys(AuthSys::current())
54 .timeout(Some(Duration::from_secs(10)))
55 .port(2049)
56 .owner_id(format!("nfs-rs:v4-cookbook:{host}:{process_id}").into_bytes())
57 .open_owner(format!("nfs-rs:v4-open-owner:{process_id}").into_bytes())
58 .io_size(128 * 1024)
59 .dir_size(64 * 1024)
60 .max_dir_entries(4096)
61 .retry_policy(RetryPolicy::new(
62 4,
63 Duration::from_millis(50),
64 Duration::from_secs(2),
65 ))
66 .connect()
67}
68
69fn run_filesystem_flow(client: &mut Client, work_dir: &str) -> nfs::Result<()> {
70 let object = remote_path(work_dir, "object.txt");
71 let streamed = remote_path(work_dir, "streamed.txt");
72 let created = remote_path(work_dir, "created.txt");
73 let copied = remote_path(work_dir, "copied.txt");
74 let renamed = remote_path(work_dir, "renamed.txt");
75 let nested_dir = remote_path(work_dir, "nested/a/b");
76 let nested_file = remote_path(&nested_dir, "payload.bin");
77
78 println!("workspace: {work_dir}");
79
80 client.write_atomic_with_mode(&object, OBJECT, 0o640)?;
81 print_v4_metadata("object after atomic write", &client.metadata(&object)?);
82
83 let exists = client.exists(&object)?;
84 println!("exists({object}) = {exists}");
85
86 let access = client.access(
87 &object,
88 ACCESS4_READ | ACCESS4_LOOKUP | ACCESS4_MODIFY | ACCESS4_EXTEND | ACCESS4_DELETE,
89 )?;
90 println!(
91 "access mask: supported=0x{:x} granted=0x{:x}",
92 access.supported, access.access
93 );
94
95 let first_five = client.read_exact_at(&object, 0, 5)?;
96 println!(
97 "first five bytes: {:?}",
98 String::from_utf8_lossy(&first_five)
99 );
100
101 let middle = client.read_range(&object, 7, 14)?;
102 println!("range read: {:?}", String::from_utf8_lossy(&middle));
103
104 let mut downloaded = Vec::new();
105 let downloaded_len = client.read_to_writer(&object, &mut downloaded)?;
106 println!(
107 "streamed object into local Vec: {downloaded_len} bytes, {}",
108 String::from_utf8_lossy(&downloaded)
109 );
110
111 let mut upload = Cursor::new(STREAMED);
112 let uploaded_len = client.write_atomic_from_reader_with_mode(&streamed, &mut upload, 0o644)?;
113 println!("streamed upload wrote {uploaded_len} bytes");
114
115 client.append(&streamed, b"appended\n")?;
116 client.write_at(&streamed, 0, b"STREAMED")?;
117 client.truncate(&streamed, 24)?;
118 let commit = client.commit(&streamed, 0, 0)?;
119 println!("commit verifier: {:02x?}", commit.verifier);
120
121 client.create_new_with_mode(&created, 0o600)?;
122 client.write_at(&created, 0, b"created with create_new_with_mode\n")?;
123 let _ = optional(
124 "create_new_with_mode existing file",
125 client.create_new_with_mode(&created, 0o600),
126 );
127
128 let copied_len = client.copy_atomic(&streamed, &copied)?;
129 println!("copied {copied_len} bytes from {streamed} to {copied}");
130
131 client.rename(&copied, &renamed)?;
132 let _ = optional("touch", client.touch(&renamed));
133 let _ = optional("set_mode", client.set_mode(&renamed, 0o644));
134
135 client.create_dir_all(&nested_dir, 0o755)?;
136 client.write_atomic(&nested_file, b"nested file\n")?;
137
138 demonstrate_optional_links(client, &object, work_dir)?;
139 demonstrate_v4_sparse_file_ops(client, &object);
140 demonstrate_directory_reads(client, work_dir)?;
141 demonstrate_filesystem_queries(client, work_dir)?;
142 demonstrate_error_handling(client, work_dir);
143 demonstrate_reconnect(client, work_dir)?;
144
145 client.renew()?;
146 Ok(())
147}
148
149fn demonstrate_optional_links(
150 client: &mut Client,
151 object: &str,
152 work_dir: &str,
153) -> nfs::Result<()> {
154 let symlink = remote_path(work_dir, "object.symlink");
155 if optional("symlink", client.symlink(&symlink, "object.txt")).is_some() {
156 let target = client.read_link(&symlink)?;
157 println!("symlink target: {target}");
158 }
159
160 let hard_link = remote_path(work_dir, "object.hardlink");
161 if optional("hard_link", client.hard_link(object, &hard_link)).is_some() {
162 println!("created hard link: {hard_link}");
163 }
164
165 Ok(())
166}
167
168fn demonstrate_v4_sparse_file_ops(client: &mut Client, object: &str) {
169 if let Some(offset) = optional("seek_data", client.seek_data(object, 0)) {
170 println!("first data offset: {offset:?}");
171 }
172 if let Some(offset) = optional("seek_hole", client.seek_hole(object, 0)) {
173 println!("first hole offset: {offset:?}");
174 }
175 if optional("allocate", client.allocate(object, 0, 4096)).is_some() {
176 let _ = optional("deallocate", client.deallocate(object, 2048, 1024));
177 }
178}
179
180fn demonstrate_directory_reads(client: &mut Client, work_dir: &str) -> nfs::Result<()> {
181 println!("limited directory listing:");
182 for entry in client.read_dir_limited(work_dir, 16)? {
183 let attrs = entry.basic_attributes()?;
184 println!(
185 " {} type={:?} size={:?}",
186 entry.name, attrs.file_type, attrs.size
187 );
188 }
189
190 println!("paged directory listing:");
191 let mut cursor: Option<DirPageCursor> = None;
192 loop {
193 let page = client.read_dir_page_limited(work_dir, cursor, 4)?;
194 for entry in &page.entries {
195 let attrs = entry.basic_attributes()?;
196 println!(" page entry: {} type={:?}", entry.name, attrs.file_type);
197 }
198 if page.is_eof() {
199 break;
200 }
201 cursor = page.next_cursor;
202 }
203
204 Ok(())
205}
206
207fn demonstrate_filesystem_queries(client: &mut Client, work_dir: &str) -> nfs::Result<()> {
208 let supported = client.supported_attrs(work_dir)?;
209 println!(
210 "supported attrs: size={} mode={} owner={} owner_group={}",
211 supported.contains(FATTR4_SIZE),
212 supported.contains(FATTR4_MODE),
213 supported.contains(FATTR4_OWNER),
214 supported.contains(FATTR4_OWNER_GROUP)
215 );
216
217 let fsstat = client.fsstat(work_dir)?;
218 println!(
219 "fsstat: total={:?} free={:?} available={:?}",
220 fsstat.total_bytes, fsstat.free_bytes, fsstat.available_bytes
221 );
222
223 let fsinfo = client.fsinfo(work_dir)?;
224 println!(
225 "fsinfo: link_support={:?} symlink_support={:?} max_file_size={:?}",
226 fsinfo.link_support, fsinfo.symlink_support, fsinfo.max_file_size
227 );
228
229 let pathconf = client.pathconf(work_dir)?;
230 println!(
231 "pathconf: name_max={:?} link_max={:?} case_preserving={:?}",
232 pathconf.name_max, pathconf.link_max, pathconf.case_preserving
233 );
234
235 Ok(())
236}
237
238fn demonstrate_error_handling(client: &mut Client, work_dir: &str) {
239 let missing = remote_path(work_dir, "does-not-exist.txt");
240 match client.read(&missing) {
241 Ok(_) => println!("unexpectedly read missing file"),
242 Err(err) if err.is_not_found() => println!("missing file classified as not found"),
243 Err(err) if err.is_session_recoverable() => println!("recoverable session error: {err}"),
244 Err(err) if err.is_lost_state() => println!("lost NFSv4 state: {err}"),
245 Err(err) if err.is_retryable() => println!("retryable error: {err}"),
246 Err(err) if err.is_permission_denied() => println!("permission error: {err}"),
247 Err(err) => println!("other read error: {err}"),
248 }
249}
250
251fn demonstrate_reconnect(client: &mut Client, work_dir: &str) -> nfs::Result<()> {
252 client.reconnect()?;
253 println!(
254 "reconnected; workspace still exists = {}",
255 client.exists(work_dir)?
256 );
257 Ok(())
258}
259
260fn print_v4_metadata(label: &str, metadata: &BasicAttributes) {
261 println!(
262 "{label}: type={:?} size={:?} mode={:?} owner={:?} group={:?}",
263 metadata.file_type,
264 metadata.size,
265 metadata.mode.map(|mode| mode & 0o7777),
266 metadata.owner,
267 metadata.owner_group
268 );
269}
270
271fn optional<T>(operation: &str, result: nfs::Result<T>) -> Option<T> {
272 match result {
273 Ok(value) => Some(value),
274 Err(err) => {
275 println!(
276 "optional operation {operation} skipped: {err} ({})",
277 classify_error(&err)
278 );
279 None
280 }
281 }
282}
283
284fn classify_error(err: &nfs::Error) -> &'static str {
285 if err.is_not_found() {
286 "not found"
287 } else if err.is_permission_denied() {
288 "permission denied"
289 } else if err.is_session_recoverable() {
290 "session recoverable"
291 } else if err.is_lost_state() {
292 "lost state"
293 } else if err.is_retryable() {
294 "retryable"
295 } else if err.is_no_space() {
296 "no space"
297 } else if err.is_read_only() {
298 "read-only filesystem"
299 } else if err.is_stale_handle() {
300 "stale handle"
301 } else if err.is_already_exists() {
302 "already exists"
303 } else {
304 "unclassified"
305 }
306}
307
308fn finish(
309 flow_result: nfs::Result<()>,
310 cleanup_result: nfs::Result<bool>,
311 shutdown_result: nfs::Result<()>,
312) -> nfs::Result<()> {
313 match flow_result {
314 Ok(()) => {
315 cleanup_result?;
316 shutdown_result?;
317 Ok(())
318 }
319 Err(err) => {
320 if let Err(cleanup_err) = cleanup_result {
321 println!("cleanup after failure also failed: {cleanup_err}");
322 }
323 if let Err(shutdown_err) = shutdown_result {
324 println!("shutdown after failure also failed: {shutdown_err}");
325 }
326 Err(err)
327 }
328 }
329}
330
331fn remote_path(parent: &str, name: &str) -> String {
332 if parent == "/" {
333 format!("/{name}")
334 } else {
335 format!("{}/{name}", parent.trim_end_matches('/'))
336 }
337}
338
339struct Config {
340 host: String,
341 remote_dir: String,
342}
343
344impl Config {
345 fn from_env() -> Self {
346 let mut args = std::env::args().skip(1);
347 Self {
348 host: args.next().unwrap_or_else(|| "127.0.0.1".to_owned()),
349 remote_dir: args.next().unwrap_or_else(|| "/".to_owned()),
350 }
351 }
352}