headless_lms_utils/file_store/
local_file_store.rs1use 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 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 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 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 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}