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