Skip to main content

over_there/core/server/fs/
file.rs

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
16/// Represents a result from a local file operation
17pub 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
25/// Converts handle into its id
26impl 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    /// Represents a unique id with which to lookup the file
41    pub(super) id: u32,
42
43    /// Represents a unique signature that acts as a barrier to prevent
44    /// unexpected operations on the file from a client with an outdated
45    /// understanding of the file
46    pub(super) sig: u32,
47
48    /// Represents an underlying file descriptor with which we can read,
49    /// write, and perform other operations
50    file: File,
51
52    /// Represents the permissions associated with the file when it was opened
53    permissions: LocalFilePermissions,
54
55    /// Represents the absolute path to the file; any movement
56    /// of the file will result in changing the path
57    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    /// Opens up a file at `path`. Will create the file if `create is true,
79    /// otherwise will fail if missing.
80    ///
81    /// - Read permission is set by `read`.
82    /// - Write permission is set by `write`.
83    ///
84    /// Internally, the path will be canonicalized to a resolved, absolute
85    /// path that can be used as reference when examining the local file.
86    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    /// Renames a file (if possible) using its underlying path as the origin
132    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        // Update signature to reflect the change and update our internal
146        // path so that we can continue to do renames/removals properly
147        self.sig = OsRng.next_u32();
148        self.path = to.as_ref().to_path_buf();
149
150        Ok(self.sig)
151    }
152
153    /// Removes the file (if possible) using its underlying path
154    ///
155    /// NOTE: If successful, this makes the local file reference no longer
156    ///       usable for the majority of its functionality
157    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        // Update signature to reflect the change
167        self.sig = OsRng.next_u32();
168
169        Ok(())
170    }
171
172    /// Reads all contents of file from beginning to end
173    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    /// Overwrites contents of file with provided contents
194    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        // Update our sig after we first touch the file so we guarantee
210        // that any modification (even partial) is reflected as a change
211        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            // NOTE: Need to canonicalize the path below as can run into
274            //       cases such as on MacOS where temp path can be
275            //       /private/var/folders/... for result.unwrap().path() and
276            //       /var/folders/... for path
277            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        // Load the file as a LocalFile
456        let mut lf = create_test_local_file(f.try_clone().unwrap(), "");
457        let data = vec![1, 2, 3];
458
459        // Put some arbitrary data into the file
460        f.write_all(b"some existing data").unwrap();
461
462        // Overwrite the existing data
463        let sig = lf.sig();
464        lf.write_all(sig, &data).await.unwrap();
465
466        // Verify the data we just wrote
467        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        // Overwrite the existing data (again)
473        let sig = lf.sig();
474        lf.write_all(sig, &data).await.unwrap();
475
476        // Verify the data we just wrote
477        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    // NOTE: This works on some Linux and not others (in terms of how it's
515    //       being tested). For now, we'll ignore and come back later to
516    //       properly test this case.
517    #[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        // NOTE: Renaming when using temp file on Linux seems to trigger this,
527        //       so using it as a test case
528        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        // Do rename and verify that the file at the new path exists
545        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        // Verify signature changed
562        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        // Do remove and verify that the file at path is gone
606        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}