1use derive_more::{Display, Error};
2use rand::{rngs::OsRng, RngCore};
3use std::io::{self, SeekFrom};
4use std::path::{Path, PathBuf};
5use tokio::{
6 fs::{self, File, OpenOptions},
7 io::{AsyncReadExt, AsyncWriteExt},
8};
9
10#[derive(Debug, Display, Error)]
11pub enum LocalFileError {
12 SigMismatch,
13 IoError(io::Error),
14}
15
16pub type Result<T> = std::result::Result<T, LocalFileError>;
18
19#[derive(Copy, Clone, Debug, PartialEq, Eq)]
20pub struct LocalFileHandle {
21 pub id: u32,
22 pub sig: u32,
23}
24
25impl Into<u32> for LocalFileHandle {
27 fn into(self) -> u32 {
28 self.id
29 }
30}
31
32#[derive(Copy, Clone, Debug, PartialEq, Eq)]
33pub struct LocalFilePermissions {
34 pub write: bool,
35 pub read: bool,
36}
37
38#[derive(Debug)]
39pub struct LocalFile {
40 pub(super) id: u32,
42
43 pub(super) sig: u32,
47
48 file: File,
51
52 permissions: LocalFilePermissions,
54
55 path: PathBuf,
58}
59
60impl LocalFile {
61 pub(crate) fn new(
62 file: File,
63 permissions: LocalFilePermissions,
64 path: impl AsRef<Path>,
65 ) -> Self {
66 let id = OsRng.next_u32();
67 let sig = OsRng.next_u32();
68
69 Self {
70 id,
71 sig,
72 file,
73 permissions,
74 path: path.as_ref().to_path_buf(),
75 }
76 }
77
78 pub async fn open(
87 path: impl AsRef<Path>,
88 create: bool,
89 write: bool,
90 read: bool,
91 ) -> io::Result<Self> {
92 match OpenOptions::new()
93 .create(create)
94 .write(write)
95 .read(read)
96 .open(&path)
97 .await
98 {
99 Ok(file) => {
100 let cpath = fs::canonicalize(path).await?;
101 let permissions = LocalFilePermissions { write, read };
102 Ok(Self::new(file, permissions, cpath))
103 }
104 Err(x) => Err(x),
105 }
106 }
107
108 pub fn id(&self) -> u32 {
109 self.id
110 }
111
112 pub fn sig(&self) -> u32 {
113 self.sig
114 }
115
116 pub fn handle(&self) -> LocalFileHandle {
117 LocalFileHandle {
118 id: self.id,
119 sig: self.sig,
120 }
121 }
122
123 pub fn permissions(&self) -> LocalFilePermissions {
124 self.permissions
125 }
126
127 pub fn path(&self) -> &Path {
128 self.path.as_path()
129 }
130
131 pub async fn rename(
133 &mut self,
134 sig: u32,
135 to: impl AsRef<Path>,
136 ) -> Result<u32> {
137 if self.sig != sig {
138 return Err(LocalFileError::SigMismatch);
139 }
140
141 rename(self.path.as_path(), to.as_ref())
142 .await
143 .map_err(LocalFileError::IoError)?;
144
145 self.sig = OsRng.next_u32();
148 self.path = to.as_ref().to_path_buf();
149
150 Ok(self.sig)
151 }
152
153 pub async fn remove(&mut self, sig: u32) -> Result<()> {
158 if self.sig != sig {
159 return Err(LocalFileError::SigMismatch);
160 }
161
162 remove(self.path.as_path())
163 .await
164 .map_err(LocalFileError::IoError)?;
165
166 self.sig = OsRng.next_u32();
168
169 Ok(())
170 }
171
172 pub async fn read_all(&mut self, sig: u32) -> Result<Vec<u8>> {
174 if self.sig != sig {
175 return Err(LocalFileError::SigMismatch);
176 }
177
178 let mut buf = Vec::new();
179
180 self.file
181 .seek(SeekFrom::Start(0))
182 .await
183 .map_err(LocalFileError::IoError)?;
184
185 self.file
186 .read_to_end(&mut buf)
187 .await
188 .map_err(LocalFileError::IoError)?;
189
190 Ok(buf)
191 }
192
193 pub async fn write_all(&mut self, sig: u32, buf: &[u8]) -> Result<()> {
195 if self.sig != sig {
196 return Err(LocalFileError::SigMismatch);
197 }
198
199 self.file
200 .seek(SeekFrom::Start(0))
201 .await
202 .map_err(LocalFileError::IoError)?;
203
204 self.file
205 .set_len(0)
206 .await
207 .map_err(LocalFileError::IoError)?;
208
209 self.sig = OsRng.next_u32();
212
213 self.file
214 .write_all(buf)
215 .await
216 .map_err(LocalFileError::IoError)?;
217
218 self.file.flush().await.map_err(LocalFileError::IoError)
219 }
220}
221
222pub async fn rename(
223 from: impl AsRef<Path>,
224 to: impl AsRef<Path>,
225) -> io::Result<()> {
226 let metadata = fs::metadata(from.as_ref()).await?;
227
228 if metadata.is_file() {
229 fs::rename(from.as_ref(), to.as_ref()).await
230 } else {
231 Err(io::Error::new(io::ErrorKind::Other, "Not a file"))
232 }
233}
234
235pub async fn remove(path: impl AsRef<Path>) -> io::Result<()> {
236 fs::remove_file(path.as_ref()).await
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use std::io::{Read, Seek, SeekFrom, Write};
243
244 fn create_test_local_file(
245 file: std::fs::File,
246 path: impl AsRef<Path>,
247 ) -> LocalFile {
248 LocalFile::new(
249 File::from_std(file),
250 LocalFilePermissions {
251 read: true,
252 write: true,
253 },
254 path,
255 )
256 }
257
258 #[tokio::test]
259 async fn open_should_yield_error_if_file_missing_and_create_false() {
260 match LocalFile::open("missingfile", false, true, true).await {
261 Err(x) => assert_eq!(x.kind(), io::ErrorKind::NotFound),
262 Ok(f) => panic!("Unexpectedly opened missing file: {:?}", f.path()),
263 }
264 }
265
266 #[tokio::test]
267 async fn open_should_return_new_local_file_with_canonical_path() {
268 let (path, result) = async {
269 let f = tempfile::NamedTempFile::new().unwrap();
270 let path = f.path();
271 let result = LocalFile::open(path, false, true, true).await;
272
273 let f_path = fs::canonicalize(path.to_owned()).await.unwrap();
278 (f_path, result)
279 }
280 .await;
281
282 match result {
283 Ok(f) => assert_eq!(f.path(), path),
284 Err(x) => panic!("Failed to open file: {}", x),
285 }
286 }
287
288 #[tokio::test]
289 async fn id_should_return_associated_id() {
290 let lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
291
292 assert_eq!(lf.id, lf.id());
293 }
294
295 #[tokio::test]
296 async fn sig_should_return_associated_sig() {
297 let lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
298
299 assert_eq!(lf.sig, lf.sig());
300 }
301
302 #[tokio::test]
303 async fn handle_should_return_associated_handle_with_id_and_sig() {
304 let lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
305 let LocalFileHandle { id, sig } = lf.handle();
306
307 assert_eq!(id, lf.id());
308 assert_eq!(sig, lf.sig());
309 }
310
311 #[tokio::test]
312 async fn path_should_return_associated_path() {
313 let path_str = "test_cheeseburger";
314 let lf =
315 create_test_local_file(tempfile::tempfile().unwrap(), path_str);
316
317 assert_eq!(Path::new(path_str), lf.path());
318 }
319
320 #[tokio::test]
321 async fn read_all_should_yield_error_if_provided_sig_is_different() {
322 let mut lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
323
324 let sig = lf.sig();
325 match lf.read_all(sig + 1).await {
326 Err(LocalFileError::SigMismatch) => {
327 assert_eq!(lf.sig(), sig, "Signature changed after error");
328 }
329 Err(x) => panic!("Unexpected error: {}", x),
330 Ok(_) => panic!("Unexpectedly read file with bad sig"),
331 }
332 }
333
334 #[tokio::test]
335 async fn read_all_should_yield_error_if_file_not_readable() {
336 let result = async {
337 let f = tempfile::NamedTempFile::new().unwrap();
338 let path = f.path();
339 LocalFile::open(path, false, true, false).await
340 }
341 .await;
342
343 let mut lf = result.expect("Failed to open file");
344 let sig = lf.sig();
345
346 match lf.read_all(sig).await {
347 Err(LocalFileError::IoError(x))
348 if x.kind() == io::ErrorKind::Other =>
349 {
350 assert_eq!(
351 sig,
352 lf.sig(),
353 "Signature was changed when no modification happened"
354 );
355 }
356 Err(x) => panic!("Unexpected error: {}", x),
357 Ok(_) => panic!("Read succeeded unexpectedly"),
358 }
359 }
360
361 #[tokio::test]
362 async fn read_all_should_return_empty_if_file_empty() {
363 let mut lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
364 let sig = lf.sig();
365
366 match lf.read_all(sig).await {
367 Ok(contents) => {
368 assert!(
369 contents.is_empty(),
370 "Got non-empty contents from empty file"
371 );
372 assert_eq!(
373 sig,
374 lf.sig(),
375 "Signature was changed when no modification happened"
376 );
377 }
378 Err(x) => panic!("Unexpected error: {}", x),
379 }
380 }
381
382 #[tokio::test]
383 async fn read_all_should_return_all_file_content_from_start() {
384 let contents = b"some contents";
385
386 let mut f = tempfile::tempfile().unwrap();
387 f.write_all(contents).unwrap();
388
389 let mut lf = create_test_local_file(f, "");
390 let sig = lf.sig();
391
392 match lf.read_all(sig).await {
393 Ok(read_contents) => {
394 assert_eq!(
395 read_contents, contents,
396 "Read contents was different than expected: {:?}",
397 read_contents
398 );
399 assert_eq!(
400 sig,
401 lf.sig(),
402 "Signature was changed when no modification happened"
403 );
404 }
405 Err(x) => panic!("Unexpected error: {}", x),
406 }
407 }
408
409 #[tokio::test]
410 async fn write_all_should_yield_error_if_provided_sig_is_different() {
411 let mut lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
412
413 let sig = lf.sig();
414 match lf.write_all(sig + 1, b"some contents").await {
415 Err(LocalFileError::SigMismatch) => {
416 assert_eq!(lf.sig(), sig, "Signature changed after error");
417 }
418 Err(x) => panic!("Unexpected error: {}", x),
419 Ok(_) => panic!("Unexpectedly removed file with bad sig"),
420 }
421 }
422
423 #[tokio::test]
424 async fn write_all_should_yield_error_if_file_not_writeable() {
425 let result = async {
426 let f = tempfile::NamedTempFile::new().unwrap();
427 let path = f.path();
428 LocalFile::open(path, false, false, true).await
429 }
430 .await;
431
432 let mut lf = result.expect("Failed to open file");
433 let sig = lf.sig();
434
435 match lf.write_all(sig, b"some content").await {
436 Err(LocalFileError::IoError(x))
437 if x.kind() == io::ErrorKind::InvalidInput =>
438 {
439 assert_eq!(
440 sig,
441 lf.sig(),
442 "Signature was changed when no modification happened"
443 );
444 }
445 Err(x) => panic!("Unexpected error: {}", x),
446 Ok(_) => panic!("Write succeeded unexpectedly"),
447 }
448 }
449
450 #[tokio::test]
451 async fn write_all_should_overwrite_file_with_new_contents() {
452 let mut f = tempfile::tempfile().unwrap();
453 let mut buf = Vec::new();
454
455 let mut lf = create_test_local_file(f.try_clone().unwrap(), "");
457 let data = vec![1, 2, 3];
458
459 f.write_all(b"some existing data").unwrap();
461
462 let sig = lf.sig();
464 lf.write_all(sig, &data).await.unwrap();
465
466 f.seek(SeekFrom::Start(0)).unwrap();
468 f.read_to_end(&mut buf).unwrap();
469 assert_ne!(sig, lf.sig(), "Sig was not updated after write");
470 assert_eq!(buf, data);
471
472 let sig = lf.sig();
474 lf.write_all(sig, &data).await.unwrap();
475
476 f.seek(SeekFrom::Start(0)).unwrap();
478 buf.clear();
479 f.read_to_end(&mut buf).unwrap();
480 assert_ne!(sig, lf.sig(), "Sig was not updated after write");
481 assert_eq!(buf, data);
482 }
483
484 #[tokio::test]
485 async fn rename_should_yield_error_if_provided_sig_is_different() {
486 let mut lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
487
488 let sig = lf.sig();
489 match lf.rename(sig + 1, "something_else").await {
490 Err(LocalFileError::SigMismatch) => {
491 assert_eq!(lf.sig(), sig, "Signature changed after error");
492 }
493 Err(x) => panic!("Unexpected error: {}", x),
494 Ok(_) => panic!("Unexpectedly renamed file with bad sig"),
495 }
496 }
497
498 #[tokio::test]
499 async fn rename_should_yield_error_if_underlying_path_is_missing() {
500 let mut lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
501
502 let sig = lf.sig();
503 match lf.rename(sig, "something_else").await {
504 Err(LocalFileError::IoError(x))
505 if x.kind() == io::ErrorKind::NotFound =>
506 {
507 assert_eq!(lf.sig(), sig, "Signature changed after error")
508 }
509 Err(x) => panic!("Unexpected error: {}", x),
510 Ok(_) => panic!("Unexpectedly renamed file with bad path"),
511 }
512 }
513
514 #[tokio::test]
518 #[ignore]
519 async fn rename_should_yield_error_if_new_name_on_different_mount_point() {
520 let f = tempfile::NamedTempFile::new().unwrap();
521 let path = f.path();
522
523 let mut lf =
524 create_test_local_file(f.as_file().try_clone().unwrap(), path);
525
526 let sig = lf.sig();
529 match lf.rename(sig, "renamed_file").await {
530 Err(_) => {
531 assert_eq!(lf.sig(), sig, "Signature changed after error")
532 }
533 Ok(_) => panic!("Unexpectedly suceeded in rename: {:?}", lf.path()),
534 }
535 }
536
537 #[tokio::test]
538 async fn rename_should_move_file_to_another_location_by_path() {
539 let mut lf = LocalFile::open("file_to_rename", true, true, true)
540 .await
541 .expect("Failed to open");
542 let sig = lf.sig();
543
544 assert!(
546 fs::read("renamed_file").await.is_err(),
547 "File already exists at rename path"
548 );
549 let new_sig = lf
550 .rename(sig, "renamed_file")
551 .await
552 .expect("Failed to rename");
553 assert!(
554 fs::read("renamed_file").await.is_ok(),
555 "File did not get renamed to new path"
556 );
557 fs::remove_file("renamed_file")
558 .await
559 .expect("Failed to clean up file");
560
561 assert_ne!(new_sig, sig);
563 }
564
565 #[tokio::test]
566 async fn remove_should_yield_error_if_provided_sig_is_different() {
567 let mut lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
568
569 let sig = lf.sig();
570 match lf.remove(sig + 1).await {
571 Err(LocalFileError::SigMismatch) => {
572 assert_eq!(lf.sig(), sig, "Signature changed after error");
573 }
574 Err(x) => panic!("Unexpected error: {}", x),
575 Ok(_) => panic!("Unexpectedly removed file with bad sig"),
576 }
577 }
578
579 #[tokio::test]
580 async fn remove_should_yield_error_if_underlying_path_is_missing() {
581 let mut lf = create_test_local_file(tempfile::tempfile().unwrap(), "");
582
583 let sig = lf.sig();
584 match lf.remove(sig).await {
585 Err(LocalFileError::IoError(x))
586 if x.kind() == io::ErrorKind::NotFound =>
587 {
588 assert_eq!(lf.sig(), sig, "Signature changed after error");
589 }
590 Err(x) => panic!("Unexpected error: {}", x),
591 Ok(_) => panic!("Unexpectedly removed file with bad path"),
592 }
593 }
594
595 #[tokio::test]
596 async fn remove_should_remove_the_underlying_file_by_path() {
597 let f = tempfile::NamedTempFile::new().unwrap();
598 let path = f.path();
599
600 let mut lf =
601 create_test_local_file(f.as_file().try_clone().unwrap(), path);
602
603 let sig = lf.sig();
604
605 assert!(fs::read(path).await.is_ok(), "File already missing at path");
607 lf.remove(sig).await.expect("Failed to remove file");
608 assert!(fs::read(path).await.is_err(), "File still exists at path");
609 assert_ne!(sig, lf.sig(), "Signature was not updated");
610 }
611}