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}