Skip to main content

headless_lms_utils/file_store/
local_file_store.rs

1use std::path::{Path, PathBuf};
2
3use async_trait::async_trait;
4use bytes::Bytes;
5use futures::{Stream, StreamExt};
6
7use tokio::{
8    fs::{self, OpenOptions},
9    io::{self, AsyncWriteExt, BufWriter},
10};
11use tokio_util::io::ReaderStream;
12
13use super::{FileStore, GenericPayload, generate_cache_folder_dir, path_to_str};
14use crate::prelude::*;
15
16#[derive(Debug, Clone)]
17pub struct LocalFileStore {
18    pub base_path: PathBuf,
19    pub base_url: String,
20    pub cache_files_path: PathBuf,
21}
22
23impl LocalFileStore {
24    pub fn new(base_path: PathBuf, base_url: String) -> UtilResult<Self> {
25        if base_path.exists() {
26            if !base_path.is_dir() {
27                return Err(UtilError::new(
28                    UtilErrorType::Other,
29                    "Base path should be a folder".to_string(),
30                    None,
31                ));
32            }
33        } else {
34            std::fs::create_dir_all(&base_path)?;
35        }
36        let cache_files_path = generate_cache_folder_dir()?;
37        Ok(Self {
38            base_path,
39            base_url,
40            cache_files_path,
41        })
42    }
43}
44#[async_trait(?Send)]
45impl FileStore for LocalFileStore {
46    async fn upload(&self, path: &Path, contents: Vec<u8>, _mime_type: &str) -> UtilResult<()> {
47        let full_path = self.base_path.join(path);
48        if let Some(parent) = full_path.parent() {
49            fs::create_dir_all(parent).await?;
50        }
51        fs::write(full_path, contents).await?;
52        Ok(())
53    }
54
55    async fn download(&self, path: &Path) -> UtilResult<Vec<u8>> {
56        let full_path = self.base_path.join(path);
57        Ok(fs::read(full_path).await?)
58    }
59
60    async fn delete(&self, path: &Path) -> UtilResult<()> {
61        let full_path = self.base_path.join(path);
62        fs::remove_file(full_path).await?;
63        Ok(())
64    }
65
66    async fn get_direct_download_url(&self, path: &Path) -> UtilResult<String> {
67        let full_path = self.base_path.join(path);
68        if !full_path.exists() {
69            return Err(UtilError::new(
70                UtilErrorType::Other,
71                "File does not exist.".to_string(),
72                None,
73            ));
74        }
75        let path_str = path_to_str(path)?;
76        if self.base_url.ends_with('/') {
77            return Ok(format!("{}{}", self.base_url, path_str));
78        }
79        Ok(format!("{}/{}", self.base_url, path_str))
80    }
81
82    async fn upload_stream(
83        &self,
84        path: &Path,
85        mut contents: GenericPayload,
86        _mime_type: &str,
87    ) -> UtilResult<()> {
88        let full_path = self.base_path.join(path);
89        let parent = full_path.parent().ok_or_else(|| {
90            UtilError::new(
91                UtilErrorType::Other,
92                "Media path did not have a parent folder".to_string(),
93                None,
94            )
95        })?;
96        if parent.exists() {
97            if !parent.is_dir() {
98                return Err(UtilError::new(
99                    UtilErrorType::Other,
100                    "Base path should be a folder".to_string(),
101                    None,
102                ));
103            }
104        } else {
105            fs::create_dir_all(&parent).await?;
106        }
107        let file = OpenOptions::new()
108            .truncate(true)
109            .create(true)
110            .write(true)
111            .open(full_path)
112            .await?;
113
114        let mut buf_writer = BufWriter::new(file);
115
116        while let Some(bytes_res) = contents.next().await {
117            let bytes =
118                bytes_res.map_err(|e| UtilError::new(UtilErrorType::Other, e.to_string(), None))?;
119            buf_writer.write_all(&bytes).await?;
120        }
121
122        buf_writer.flush().await?;
123
124        Ok(())
125    }
126
127    async fn download_stream(
128        &self,
129        path: &Path,
130    ) -> UtilResult<Box<dyn Stream<Item = std::io::Result<Bytes>>>> {
131        let full_path = self.base_path.join(path);
132        let file = fs::File::open(full_path).await?;
133        let reader = io::BufReader::new(file);
134        let stream = ReaderStream::new(reader);
135        Ok(Box::new(stream))
136    }
137
138    fn get_cache_files_folder_path(&self) -> UtilResult<&Path> {
139        Ok(&self.cache_files_path)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use std::{env, path::Path};
146
147    use tempfile::TempDir;
148
149    use super::LocalFileStore;
150    use crate::file_store::FileStore;
151
152    #[tokio::test]
153    async fn upload_download_delete_works() {
154        // TODO: Audit that the environment access only happens in single-threaded code.
155        unsafe { env::set_var("HEADLESS_LMS_CACHE_FILES_PATH", "/tmp") };
156        let dir = TempDir::new().expect("Failed to create a temp dir");
157        let base_path = dir.path().to_path_buf();
158        let local_file_store =
159            LocalFileStore::new(base_path.clone(), "http://localhost:3000".to_string())
160                .expect("Could not create local file storage");
161
162        let path1 = Path::new("file1");
163        let test_file_contents = "Test file contents".as_bytes().to_vec();
164        // Put content to storage and read it back
165        local_file_store
166            .upload(path1, test_file_contents.clone(), "text/plain")
167            .await
168            .expect("Failed to put a file into local file storage.");
169        let retrivied_file = local_file_store
170            .download(path1)
171            .await
172            .expect("Failed to retrieve a file from local file storage");
173        assert_eq!(test_file_contents, retrivied_file);
174
175        local_file_store
176            .delete(path1)
177            .await
178            .expect("Failed to delete a file");
179
180        // After deletion getting the file should fail
181        let retrivied_file2 = local_file_store.download(path1).await;
182        assert!(retrivied_file2.is_err());
183    }
184
185    #[tokio::test]
186    async fn get_download_url_works() {
187        // TODO: Audit that the environment access only happens in single-threaded code.
188        unsafe { env::set_var("HEADLESS_LMS_CACHE_FILES_PATH", "/tmp") };
189        let dir = TempDir::new().expect("Failed to create a temp dir");
190        let base_path = dir.path().to_path_buf();
191        let local_file_store =
192            LocalFileStore::new(base_path.clone(), "http://localhost:3000".to_string())
193                .expect("Could not create local file storage");
194        let test_file_contents = "Test file contents 2".as_bytes().to_vec();
195        let path1 = Path::new("file1");
196        local_file_store
197            .upload(path1, test_file_contents.clone(), "text/plain")
198            .await
199            .expect("Failed to put a file into local file storage.");
200        let url = local_file_store
201            .get_direct_download_url(path1)
202            .await
203            .expect("Failed to get a download url");
204        let expected_url = format!("http://localhost:3000/{}", path1.to_string_lossy());
205        assert_eq!(url, expected_url);
206
207        let nonexistant_file = Path::new("does-not-exist");
208        let res = local_file_store
209            .get_direct_download_url(nonexistant_file)
210            .await;
211        assert!(res.is_err());
212    }
213}