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