headless_lms_utils/file_store/
mod.rs

1//! Allows storing files to a file storage backend.
2pub 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/**
24Allows storing files to a file storage backend.
25*/
26#[async_trait(?Send)]
27pub trait FileStore {
28    /// Upload a file that's in memory to a path.
29    async fn upload(&self, path: &Path, contents: Vec<u8>, mime_type: &str) -> UtilResult<()>;
30    /// Upload a file without loading the whole file to memory
31    async fn upload_stream(
32        &self,
33        path: &Path,
34        mut contents: GenericPayload,
35        mime_type: &str,
36    ) -> UtilResult<()>;
37    /// Download a file to memory.
38    async fn download(&self, path: &Path) -> UtilResult<Vec<u8>>;
39    /// Download a file without loading the whole file to memory.
40    async fn download_stream(
41        &self,
42        path: &Path,
43    ) -> UtilResult<Box<dyn Stream<Item = std::io::Result<Bytes>>>>;
44    /// Get a url that can be used to download the file without authentication for a while.
45    /// In most cases you probably want to use get_download_url() instead.
46    async fn get_direct_download_url(&self, path: &Path) -> UtilResult<String>;
47    /// Get a url for a file in FileStore that can be used to access the resource.
48    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    /// Delete a file.
56    async fn delete(&self, path: &Path) -> UtilResult<()>;
57
58    /// This function returns a path to a folder where downloaded files can be cached.
59    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}