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