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}