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