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