1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
//! Utils for parsing paths on the server and to store/retrieve paths from the database
//! 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.

use std::{
    fs,
    path::{self, Path, PathBuf},
};

use color_eyre::eyre::eyre;
use url::Url;

/// A category of papers, can also be used to represent the directory where these papers are stored
#[allow(unused)]
pub enum PaperCategory {
    /// Unapproved paper
    Unapproved,
    /// Approved paper
    Approved,
    /// Library paper (scraped using the peqp scraper)
    Library,
}

#[derive(Clone, Default)]
/// A set of paths (absolute, relative, or even URLs) for all three categories of papers (directories)
struct PathTriad {
    /// Unapproved paper path
    pub unapproved: PathBuf,
    /// Approved paper path
    pub approved: PathBuf,
    /// Library paper path
    pub library: PathBuf,
}

impl PathTriad {
    /// Gets the path in the triad corresponding to the given paper category.
    pub fn get(&self, category: PaperCategory) -> PathBuf {
        match category {
            PaperCategory::Approved => self.approved.to_owned(),
            PaperCategory::Unapproved => self.unapproved.to_owned(),
            PaperCategory::Library => self.library.to_owned(),
        }
    }
}

#[derive(Clone)]
#[allow(unused)]
/// Struct containing all the paths and URLs required to parse or create any question paper's slug, absolute path, or URL.
pub struct Paths {
    /// URL of the static files server
    static_files_url: Url,
    /// The absolute path to the location from where the static files server serves files
    static_files_path: PathBuf,

    /// The absolute system paths to all three directories on the server
    system_paths: PathTriad,

    /// The slugs to all three directories
    ///
    /// 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)
    path_slugs: PathTriad,
}

impl Default for Paths {
    fn default() -> Self {
        Self {
            static_files_url: Url::parse("https://metakgp.org")
                .expect("This library thinks https://metakgp.org is not a valid URL."),
            static_files_path: PathBuf::default(),
            system_paths: PathTriad::default(),
            path_slugs: PathTriad::default(),
        }
    }
}

#[allow(unused)]
impl Paths {
    /// Creates a new `Paths` struct
    /// # Arguments
    ///
    /// * `static_files_url` - The static files server URL (eg: https://static.metakgp.org)
    /// * `static_file_storage_location` - The path to the location on the server from which the static files are served (eg: /srv/static)
    /// * `uploaded_qps_relative_path` - The path to the uploaded question papers, relative to the static files storage location. (eg: /iqps/uploaded)
    /// * `library_qps_relative_path` - The path to the library question papers, relative to the static files storage location. (eg: /peqp/qp)
    pub fn new(
        static_files_url: &str,
        static_file_storage_location: &Path,
        uploaded_qps_relative_path: &Path,
        library_qps_relative_path: &Path,
    ) -> Result<Self, color_eyre::eyre::Error> {
        // The slugs for each of the uploaded papers directories
        let path_slugs = PathTriad {
            // Use subdirectories `/unapproved` and `/approved` inside the uploaded qps path
            unapproved: uploaded_qps_relative_path.join("unapproved"),
            approved: uploaded_qps_relative_path.join("approved"),
            library: library_qps_relative_path.to_owned(),
        };

        // The absolute system paths for each of the directories
        let system_paths = PathTriad {
            unapproved: path::absolute(static_file_storage_location.join(&path_slugs.unapproved))?,
            approved: path::absolute(static_file_storage_location.join(&path_slugs.approved))?,
            library: path::absolute(static_file_storage_location.join(&path_slugs.library))?,
        };

        // Ensure these system paths exist

        // Throw error for uploaded and library paths
        if !path::absolute(static_file_storage_location.join(uploaded_qps_relative_path))?.exists()
        {
            return Err(eyre!(
                "Path for uploaded papers does not exist: {}",
                system_paths.unapproved.to_string_lossy()
            ));
        }
        if !system_paths.library.exists() {
            return Err(eyre!(
                "Path for library papers does not exist: {}",
                system_paths.library.to_string_lossy()
            ));
        }

        // Create dirs for unapproved and approved
        if !system_paths.unapproved.exists() {
            fs::create_dir(&system_paths.unapproved)?;
        }
        if !system_paths.approved.exists() {
            fs::create_dir(&system_paths.approved)?;
        }

        Ok(Self {
            static_files_url: Url::parse(static_files_url)?,
            static_files_path: path::absolute(static_file_storage_location)?,
            system_paths,
            path_slugs,
        })
    }

    /// Returns the slug for a given filename and paper category (directory)
    pub fn get_slug(&self, filename: &str, category: PaperCategory) -> String {
        self.path_slugs
            .get(category)
            .join(filename)
            .to_string_lossy()
            .to_string()
    }

    /// Returns the absolute system path for the specified directory and filename
    pub fn get_path(&self, filename: &str, dir: PaperCategory) -> PathBuf {
        self.system_paths.get(dir).join(filename)
    }

    /// Returns the absolute system path from a given slug
    pub fn get_path_from_slug(&self, slug: &str) -> PathBuf {
        self.static_files_path.join(slug)
    }

    /// Returns the static server URL for the specified directory and filename
    pub fn get_url(
        &self,
        filename: &str,
        dir: PaperCategory,
    ) -> Result<String, color_eyre::eyre::Error> {
        let slug = self
            .path_slugs
            .get(dir)
            .join(filename)
            .to_string_lossy()
            .into_owned();

        self.get_url_from_slug(&slug)
    }

    /// Returns the static server URL for a given slug
    pub fn get_url_from_slug(&self, slug: &str) -> Result<String, color_eyre::eyre::Error> {
        Ok(self.static_files_url.join(slug)?.as_str().to_string())
    }

    /// Removes any non-alphanumeric character and replaces whitespaces with `-`
    /// Also replaces `/` with `-` and multiple spaces or hyphens will be replaced with a single one
    pub fn sanitize_path(path: &str) -> String {
        path.replace('/', "-") // Replace specific characters with a `-`
            .replace('-', " ") // Convert any series of spaces and hyphens to just spaces
            .split_whitespace() // Split at whitespaces to later replace all whitespaces with `-`
            .map(|part| {
                part.chars()
                    .filter(|&character| character.is_alphanumeric() || character == '-' || character == '_') // Remove any character that is not a `-` or alphanumeric
                    .collect::<String>()
            })
            .collect::<Vec<String>>()
            .join("-") // Join the parts with `-`
    }
}