headless_lms_utils/file_store/
mod.rs1pub mod file_utils;
3pub mod google_cloud_file_store;
4pub mod local_file_store;
5
6use std::{
7 os::unix::prelude::OsStrExt,
8 path::{Path, PathBuf},
9 pin::Pin,
10};
11
12use async_trait::async_trait;
13use bytes::Bytes;
14use futures::Stream;
15use rand::distr::SampleString;
16
17use uuid::Uuid;
18
19use crate::prelude::*;
20use headless_lms_base::config::ApplicationConfiguration;
21use headless_lms_base::jwt::{DownloadClaim, JwtKey, claimed_file_url};
22
23pub type GenericPayload = Pin<Box<dyn Stream<Item = Result<Bytes, anyhow::Error>>>>;
24#[async_trait(?Send)]
28pub trait FileStore: Send + Sync {
32 async fn upload(&self, path: &Path, contents: Vec<u8>, mime_type: &str) -> UtilResult<()>;
34 async fn upload_stream(
36 &self,
37 path: &Path,
38 mut contents: GenericPayload,
39 mime_type: &str,
40 ) -> UtilResult<()>;
41 async fn download(&self, path: &Path) -> UtilResult<Vec<u8>>;
43 async fn download_stream(
45 &self,
46 path: &Path,
47 ) -> UtilResult<Box<dyn Stream<Item = std::io::Result<Bytes>>>>;
48 async fn get_direct_download_url(&self, path: &Path) -> UtilResult<String>;
51 fn get_download_url(&self, path: &Path, app_conf: &ApplicationConfiguration) -> String {
53 format!(
54 "{}/api/v0/files/{}",
55 app_conf.base_url,
56 path.to_string_lossy()
57 )
58 }
59 fn get_claimed_download_url(
66 &self,
67 file_upload_id: Uuid,
68 app_conf: &ApplicationConfiguration,
69 ) -> UtilResult<String> {
70 let jwt_key = JwtKey::new(&app_conf.jwt_password).map_err(|err| {
71 util_err!(
72 Other,
73 "Failed to build the JWT signing key.".to_string(),
74 err
75 )
76 })?;
77 claimed_file_url(
78 &app_conf.base_url,
79 &jwt_key,
80 DownloadClaim::expiring_in_1_hour(file_upload_id),
81 )
82 .map_err(|err| {
83 util_err!(
84 Other,
85 "Failed to sign a file download claim.".to_string(),
86 err
87 )
88 })
89 }
90 async fn delete(&self, path: &Path) -> UtilResult<()>;
92
93 fn get_cache_files_folder_path(&self) -> UtilResult<&Path>;
95
96 async fn fetch_file_content_or_use_filesystem_cache(
97 &self,
98 file_path: &Path,
99 ) -> UtilResult<Vec<u8>> {
100 let cache_folder = self.get_cache_files_folder_path()?;
101 let hash = blake3::hash(file_path.as_os_str().as_bytes());
102 let cached_file_path = cache_folder.join(hash.to_hex().as_str());
103 match tokio::fs::read(&cached_file_path).await {
104 Ok(string) => return Ok(string),
105 Err(_) => {
106 info!(
107 "File not found in cache, fetching from file store using path: {}",
108 file_path.to_str().unwrap_or_default()
109 );
110 }
111 }
112
113 let random_filename = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 32);
114 let temp_path = cache_folder.join(random_filename.as_str());
115
116 let file_content = self.download(file_path).await?;
117
118 tokio::fs::write(&temp_path, &file_content).await?;
119 tokio::fs::rename(&temp_path, &cached_file_path).await?;
120 Ok(file_content.to_vec())
121 }
122}
123
124fn generate_cache_folder_dir() -> UtilResult<PathBuf> {
125 let cache_files_path =
126 std::env::var("HEADLESS_LMS_CACHE_FILES_PATH").map_err(|original_error| {
127 UtilError::new(
128 UtilErrorType::Other,
129 "You need to define the HEADLESS_LMS_CACHE_FILES_PATH environment variable."
130 .to_string(),
131 Some(original_error.into()),
132 )
133 })?;
134 let path = PathBuf::from(cache_files_path).join("headlesss-lms-cached-files");
135 if !path.exists() {
136 std::fs::create_dir_all(&path)?;
137 }
138 Ok(path)
139}
140
141fn path_to_str(path: &Path) -> UtilResult<&str> {
142 let str = path.to_str();
143 match str {
144 Some(s) => Ok(s),
145 None => Err(UtilError::new(
146 UtilErrorType::Other,
147 "Could not convert path to string because it contained invalid UTF-8 characters."
148 .to_string(),
149 None,
150 )),
151 }
152}
153
154pub fn organization_image_path(organization_id: Uuid, image_name: &str) -> UtilResult<PathBuf> {
155 let path = PathBuf::from(format!(
156 "organizations/{}/images/{}",
157 organization_id, image_name
158 ));
159 Ok(path)
160}
161
162pub fn organization_audio_path(organization_id: Uuid, audio_name: &str) -> UtilResult<PathBuf> {
163 let path = PathBuf::from(format!(
164 "organizations/{}/audios/{}",
165 organization_id, audio_name
166 ));
167 Ok(path)
168}
169
170pub fn organization_file_path(organization_id: Uuid, file_name: &str) -> UtilResult<PathBuf> {
171 let path = PathBuf::from(format!(
172 "organizations/{}/files/{}",
173 organization_id, file_name
174 ));
175 Ok(path)
176}
177
178pub fn repository_exercise_path(repository_id: Uuid, repository_exercise_id: Uuid) -> PathBuf {
179 PathBuf::from(format!(
180 "repository_exercises/{repository_id}/{repository_exercise_id}",
181 ))
182}