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;
21
22pub type GenericPayload = Pin<Box<dyn Stream<Item = Result<Bytes, anyhow::Error>>>>;
23#[async_trait(?Send)]
27pub trait FileStore {
28 async fn upload(&self, path: &Path, contents: Vec<u8>, mime_type: &str) -> UtilResult<()>;
30 async fn upload_stream(
32 &self,
33 path: &Path,
34 mut contents: GenericPayload,
35 mime_type: &str,
36 ) -> UtilResult<()>;
37 async fn download(&self, path: &Path) -> UtilResult<Vec<u8>>;
39 async fn download_stream(
41 &self,
42 path: &Path,
43 ) -> UtilResult<Box<dyn Stream<Item = std::io::Result<Bytes>>>>;
44 async fn get_direct_download_url(&self, path: &Path) -> UtilResult<String>;
47 fn get_download_url(&self, path: &Path, app_conf: &ApplicationConfiguration) -> String {
49 format!(
50 "{}/api/v0/files/{}",
51 app_conf.base_url,
52 path.to_string_lossy()
53 )
54 }
55 async fn delete(&self, path: &Path) -> UtilResult<()>;
57
58 fn get_cache_files_folder_path(&self) -> UtilResult<&Path>;
60
61 async fn fetch_file_content_or_use_filesystem_cache(
62 &self,
63 file_path: &Path,
64 ) -> UtilResult<Vec<u8>> {
65 let cache_folder = self.get_cache_files_folder_path()?;
66 let hash = blake3::hash(file_path.as_os_str().as_bytes());
67 let cached_file_path = cache_folder.join(hash.to_hex().as_str());
68 match tokio::fs::read(&cached_file_path).await {
69 Ok(string) => return Ok(string),
70 Err(_) => {
71 info!(
72 "File not found in cache, fetching from file store using path: {}",
73 file_path.to_str().unwrap_or_default()
74 );
75 }
76 }
77
78 let random_filename = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 32);
79 let temp_path = cache_folder.join(random_filename.as_str());
80
81 let file_content = self.download(file_path).await?;
82
83 tokio::fs::write(&temp_path, &file_content).await?;
84 tokio::fs::rename(&temp_path, &cached_file_path).await?;
85 Ok(file_content.to_vec())
86 }
87}
88
89fn generate_cache_folder_dir() -> UtilResult<PathBuf> {
90 let cache_files_path =
91 std::env::var("HEADLESS_LMS_CACHE_FILES_PATH").map_err(|original_error| {
92 UtilError::new(
93 UtilErrorType::Other,
94 "You need to define the HEADLESS_LMS_CACHE_FILES_PATH environment variable."
95 .to_string(),
96 Some(original_error.into()),
97 )
98 })?;
99 let path = PathBuf::from(cache_files_path).join("headlesss-lms-cached-files");
100 if !path.exists() {
101 std::fs::create_dir_all(&path)?;
102 }
103 Ok(path)
104}
105
106fn path_to_str(path: &Path) -> UtilResult<&str> {
107 let str = path.to_str();
108 match str {
109 Some(s) => Ok(s),
110 None => Err(UtilError::new(
111 UtilErrorType::Other,
112 "Could not convert path to string because it contained invalid UTF-8 characters."
113 .to_string(),
114 None,
115 )),
116 }
117}
118
119pub fn organization_image_path(organization_id: Uuid, image_name: &str) -> UtilResult<PathBuf> {
120 let path = PathBuf::from(format!(
121 "organizations/{}/images/{}",
122 organization_id, image_name
123 ));
124 Ok(path)
125}
126
127pub fn organization_audio_path(organization_id: Uuid, audio_name: &str) -> UtilResult<PathBuf> {
128 let path = PathBuf::from(format!(
129 "organizations/{}/audios/{}",
130 organization_id, audio_name
131 ));
132 Ok(path)
133}
134
135pub fn organization_file_path(organization_id: Uuid, file_name: &str) -> UtilResult<PathBuf> {
136 let path = PathBuf::from(format!(
137 "organizations/{}/files/{}",
138 organization_id, file_name
139 ));
140 Ok(path)
141}
142
143pub fn repository_exercise_path(repository_id: Uuid, repository_exercise_id: Uuid) -> PathBuf {
144 PathBuf::from(format!(
145 "repository_exercises/{repository_id}/{repository_exercise_id}",
146 ))
147}