iqps_backend/
pathutils.rs

1//! Utils for parsing paths on the server and to store/retrieve paths from the database
2//! A "slug" is the part of the path common to the question paper and is stored in the database. Depending on the requirements, either a URL (eg: static.metakgp.org) or a path (/srv/static) can be prepended to the slug to get the final path to copy/serve/move the question paper to/from.
3
4use std::{
5    fs,
6    path::{self, Path, PathBuf},
7};
8
9use color_eyre::eyre::eyre;
10use url::Url;
11
12/// A category of papers, can also be used to represent the directory where these papers are stored
13#[allow(unused)]
14pub enum PaperCategory {
15    /// Unapproved paper
16    Unapproved,
17    /// Approved paper
18    Approved,
19    /// Library paper (scraped using the peqp scraper)
20    Library,
21}
22
23#[derive(Clone, Default)]
24/// A set of paths (absolute, relative, or even URLs) for all three categories of papers (directories)
25struct PathTriad {
26    /// Unapproved paper path
27    pub unapproved: PathBuf,
28    /// Approved paper path
29    pub approved: PathBuf,
30    /// Library paper path
31    pub library: PathBuf,
32}
33
34impl PathTriad {
35    /// Gets the path in the triad corresponding to the given paper category.
36    pub fn get(&self, category: PaperCategory) -> PathBuf {
37        match category {
38            PaperCategory::Approved => self.approved.to_owned(),
39            PaperCategory::Unapproved => self.unapproved.to_owned(),
40            PaperCategory::Library => self.library.to_owned(),
41        }
42    }
43}
44
45#[derive(Clone)]
46#[allow(unused)]
47/// Struct containing all the paths and URLs required to parse or create any question paper's slug, absolute path, or URL.
48pub struct Paths {
49    /// URL of the static files server
50    static_files_url: Url,
51    /// The absolute path to the location from where the static files server serves files
52    static_files_path: PathBuf,
53
54    /// The absolute system paths to all three directories on the server
55    system_paths: PathTriad,
56
57    /// The slugs to all three directories
58    ///
59    /// A slug is a relative path independent of the URL or system path. This slug is stored in the database and either the [`crate::pathutils::Paths::static_files_url`] or the [`crate::pathutils::Paths::static_files_path`] is prepended to it to get its URL (to send to the frontend) or the system path (for backend operations)
60    path_slugs: PathTriad,
61}
62
63impl Default for Paths {
64    fn default() -> Self {
65        Self {
66            static_files_url: Url::parse("https://metakgp.org")
67                .expect("This library thinks https://metakgp.org is not a valid URL."),
68            static_files_path: PathBuf::default(),
69            system_paths: PathTriad::default(),
70            path_slugs: PathTriad::default(),
71        }
72    }
73}
74
75#[allow(unused)]
76impl Paths {
77    /// Creates a new `Paths` struct
78    /// # Arguments
79    ///
80    /// * `static_files_url` - The static files server URL (eg: https://static.metakgp.org)
81    /// * `static_file_storage_location` - The path to the location on the server from which the static files are served (eg: /srv/static)
82    /// * `uploaded_qps_relative_path` - The path to the uploaded question papers, relative to the static files storage location. (eg: /iqps/uploaded)
83    /// * `library_qps_relative_path` - The path to the library question papers, relative to the static files storage location. (eg: /peqp/qp)
84    pub fn new(
85        static_files_url: &str,
86        static_file_storage_location: &Path,
87        uploaded_qps_relative_path: &Path,
88        library_qps_relative_path: &Path,
89    ) -> Result<Self, color_eyre::eyre::Error> {
90        // The slugs for each of the uploaded papers directories
91        let path_slugs = PathTriad {
92            // Use subdirectories `/unapproved` and `/approved` inside the uploaded qps path
93            unapproved: uploaded_qps_relative_path.join("unapproved"),
94            approved: uploaded_qps_relative_path.join("approved"),
95            library: library_qps_relative_path.to_owned(),
96        };
97
98        // The absolute system paths for each of the directories
99        let system_paths = PathTriad {
100            unapproved: path::absolute(static_file_storage_location.join(&path_slugs.unapproved))?,
101            approved: path::absolute(static_file_storage_location.join(&path_slugs.approved))?,
102            library: path::absolute(static_file_storage_location.join(&path_slugs.library))?,
103        };
104
105        // Ensure these system paths exist
106
107        // Throw error for uploaded and library paths
108        if !path::absolute(static_file_storage_location.join(uploaded_qps_relative_path))?.exists()
109        {
110            return Err(eyre!(
111                "Path for uploaded papers does not exist: {}",
112                system_paths.unapproved.to_string_lossy()
113            ));
114        }
115        if !system_paths.library.exists() {
116            return Err(eyre!(
117                "Path for library papers does not exist: {}",
118                system_paths.library.to_string_lossy()
119            ));
120        }
121
122        // Create dirs for unapproved and approved
123        if !system_paths.unapproved.exists() {
124            fs::create_dir(&system_paths.unapproved)?;
125        }
126        if !system_paths.approved.exists() {
127            fs::create_dir(&system_paths.approved)?;
128        }
129
130        Ok(Self {
131            static_files_url: Url::parse(static_files_url)?,
132            static_files_path: path::absolute(static_file_storage_location)?,
133            system_paths,
134            path_slugs,
135        })
136    }
137
138    /// Returns the slug for a given filename and paper category (directory)
139    pub fn get_slug(&self, filename: &str, category: PaperCategory) -> String {
140        self.path_slugs
141            .get(category)
142            .join(filename)
143            .to_string_lossy()
144            .to_string()
145    }
146
147    /// Returns the absolute system path for the specified directory and filename
148    pub fn get_path(&self, filename: &str, dir: PaperCategory) -> PathBuf {
149        self.system_paths.get(dir).join(filename)
150    }
151
152    /// Returns the absolute system path from a given slug
153    pub fn get_path_from_slug(&self, slug: &str) -> PathBuf {
154        self.static_files_path.join(slug)
155    }
156
157    /// Returns the static server URL for the specified directory and filename
158    pub fn get_url(
159        &self,
160        filename: &str,
161        dir: PaperCategory,
162    ) -> Result<String, color_eyre::eyre::Error> {
163        let slug = self
164            .path_slugs
165            .get(dir)
166            .join(filename)
167            .to_string_lossy()
168            .into_owned();
169
170        self.get_url_from_slug(&slug)
171    }
172
173    /// Returns the static server URL for a given slug
174    pub fn get_url_from_slug(&self, slug: &str) -> Result<String, color_eyre::eyre::Error> {
175        Ok(self.static_files_url.join(slug)?.as_str().to_string())
176    }
177
178    /// Removes any non-alphanumeric character and replaces whitespaces with `-`
179    /// Also replaces `/` with `-` and multiple spaces or hyphens will be replaced with a single one
180    pub fn sanitize_path(path: &str) -> String {
181        path.replace('/', "-") // Replace specific characters with a `-`
182            .replace('-', " ") // Convert any series of spaces and hyphens to just spaces
183            .split_whitespace() // Split at whitespaces to later replace all whitespaces with `-`
184            .map(|part| {
185                part.chars()
186                    .filter(|&character| {
187                        character.is_alphanumeric() || character == '-' || character == '_'
188                    }) // Remove any character that is not a `-` or alphanumeric
189                    .collect::<String>()
190            })
191            .collect::<Vec<String>>()
192            .join("-") // Join the parts with `-`
193    }
194}