1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use actix_web::{HttpRequest, HttpResponse, web};
6use actix_web::http::StatusCode;
7use fernet::Fernet;
8use minijinja;
9use serde::Deserialize;
10use url::form_urlencoded;
11
12
13use crate::{constant, routes, squire};
14
15#[derive(Deserialize)]
17pub struct Payload {
18 file: String,
19}
20
21struct Subtitles {
23 srt: PathBuf,
24 vtt: PathBuf,
25 vtt_file: String,
26}
27
28fn url_encode(path: &String) -> String {
44 form_urlencoded::byte_serialize(path.as_bytes())
45 .collect::<Vec<_>>()
46 .join("")
47}
48
49fn subtitles(true_path: PathBuf, relative_path: &String) -> Subtitles {
60 let srt = true_path.with_extension("srt");
62 let vtt = true_path.with_extension("vtt");
63
64 let vtt_filepath = PathBuf::new().join(relative_path).with_extension("vtt");
66 let vtt_file = vtt_filepath.to_string_lossy().to_string();
67
68 Subtitles { srt, vtt, vtt_file }
69}
70
71#[get("/track")]
87pub async fn track(request: HttpRequest,
88 info: web::Query<Payload>,
89 fernet: web::Data<Arc<Fernet>>,
90 session: web::Data<Arc<constant::Session>>,
91 metadata: web::Data<Arc<constant::MetaData>>,
92 config: web::Data<Arc<squire::settings::Config>>,
93 template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
94 let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
95 if !auth_response.ok {
96 return routes::auth::failed_auth(auth_response, &config);
97 }
98 if !squire::authenticator::verify_secure_index(&PathBuf::from(&info.file), &auth_response.username) {
99 return squire::custom::error(
100 "RESTRICTED SECTION",
101 template.get_template("error").unwrap(),
102 &metadata.pkg_version,
103 format!("This content is not accessible, as it does not belong to the user profile '{}'", auth_response.username),
104 StatusCode::FORBIDDEN
105 );
106 }
107 let (_host, _last_accessed) = squire::custom::log_connection(&request, &session);
108 log::debug!("{}", auth_response.detail);
109 log::debug!("Track requested: {}", &info.file);
110 let filepath = Path::new(&config.media_source).join(&info.file);
111 log::debug!("Track file lookup: {}", &filepath.to_string_lossy());
112 match std::fs::read_to_string(&filepath) {
113 Ok(content) => HttpResponse::Ok()
114 .content_type("text/plain")
115 .body(content),
116 Err(_) => squire::custom::error(
117 "CONTENT UNAVAILABLE",
118 template.get_template("error").unwrap(),
119 &metadata.pkg_version,
120 format!("'{}' was not found", &info.file),
121 StatusCode::NOT_FOUND
122 )
123 }
124}
125
126fn render_content(landing: minijinja::Template,
133 serializable: HashMap<&str, &String>) -> HttpResponse {
134 return match landing.render(serializable) {
135 Ok(response_body) => {
136 HttpResponse::build(StatusCode::OK)
137 .content_type("text/html; charset=utf-8").body(response_body)
138 }
139 Err(err) => {
140 log::error!("{}", err);
141 HttpResponse::FailedDependency().json("Failed to render content.")
142 }
143 };
144}
145
146#[get("/stream/{media_path:.*}")]
162pub async fn stream(request: HttpRequest,
163 media_path: web::Path<String>,
164 fernet: web::Data<Arc<Fernet>>,
165 session: web::Data<Arc<constant::Session>>,
166 metadata: web::Data<Arc<constant::MetaData>>,
167 config: web::Data<Arc<squire::settings::Config>>,
168 template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
169 let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
170 if !auth_response.ok {
171 return routes::auth::failed_auth(auth_response, &config);
172 }
173 let (_host, _last_accessed) = squire::custom::log_connection(&request, &session);
174 log::debug!("{}", auth_response.detail);
175 let filepath = media_path.to_string();
176 if !squire::authenticator::verify_secure_index(&PathBuf::from(&filepath), &auth_response.username) {
177 return squire::custom::error(
178 "RESTRICTED SECTION",
179 template.get_template("error").unwrap(),
180 &metadata.pkg_version,
181 format!("This content is not accessible, as it does not belong to the user profile '{}'", auth_response.username),
182 StatusCode::FORBIDDEN
183 );
184 }
185 let secure_path = if filepath.contains(constant::SECURE_INDEX) { "true" } else { "false" };
186 let secure_flag = secure_path.to_string();
187 let __target = config.media_source.join(&filepath);
189 if !__target.exists() {
190 return squire::custom::error(
191 "CONTENT UNAVAILABLE",
192 template.get_template("error").unwrap(),
193 &metadata.pkg_version,
194 format!("'{}' was not found", filepath),
195 StatusCode::NOT_FOUND
196 )
197 }
198 let __target_str = __target.to_string_lossy().to_string();
200 let __filename = __target.file_name().unwrap().to_string_lossy().to_string();
201 if __target.is_file() {
202 let landing = template.get_template("landing").unwrap();
203 let rust_iter = squire::content::get_iter(&__target, &config.file_formats);
204 let render_path = format!("/media?file={}", url_encode(&filepath));
205 let prev = rust_iter.previous.unwrap_or_default();
206 let next = rust_iter.next.unwrap_or_default();
207 let secure_index = constant::SECURE_INDEX.to_string();
208 let mut context_builder = vec![
209 ("version", &metadata.pkg_version),
210 ("media_title", &__filename),
211 ("path", &render_path),
212 ("previous", &prev),
213 ("next", &next),
214 ("user", &auth_response.username),
215 ("secure_index", &secure_index),
216 ].into_iter().collect::<HashMap<_, _>>();
217 if constant::IMAGE_FORMATS
218 .contains(&render_path.split('.')
219 .last()
220 .unwrap() .to_lowercase().as_str()) {
222 context_builder.insert("render_image", &render_path);
223 return render_content(landing, context_builder);
224 }
225 let subtitle = subtitles(__target, &filepath);
226 let mut sfx_file = String::new();
227 if subtitle.vtt.exists() {
228 sfx_file = format!("/track?file={}", url_encode(&subtitle.vtt_file));
229 } else if subtitle.srt.exists() {
230 log::info!("Converting {:?} to {:?} for subtitles",
231 subtitle.srt.file_name().unwrap(),
232 subtitle.vtt.file_name().unwrap());
233 match squire::subtitles::srt_to_vtt(&subtitle.srt) {
234 Ok(_) => {
235 log::debug!("Successfully converted srt to vtt file");
236 sfx_file = format!("/track?file={}", url_encode(&subtitle.vtt_file));
237 }
238 Err(err) => log::error!("Failed to convert srt to vtt: {}", err),
239 }
240 }
241 if !sfx_file.is_empty() {
242 context_builder.insert("track", &sfx_file);
243 }
244 return render_content(landing, context_builder);
245 } else if __target.is_dir() {
246 let child_dir = __target.iter().last().unwrap().to_string_lossy().to_string();
247 let listing_page = squire::content::get_dir_stream_content(&__target_str, &child_dir, &config.file_formats);
248 let listing = template.get_template("listing").unwrap();
249 let custom_title = if child_dir.ends_with(constant::SECURE_INDEX) {
250 format!(
251 "<i class='fa-solid fa-lock'></i> {}",
252 child_dir.strip_suffix(&format!("_{}", constant::SECURE_INDEX)).unwrap()
253 )
254 } else {
255 child_dir
256 };
257 return HttpResponse::build(StatusCode::OK)
258 .content_type("text/html; charset=utf-8")
259 .body(listing.render(minijinja::context!(
260 version => metadata.pkg_version,
261 custom_title => custom_title,
262 files => listing_page.files,
263 user => auth_response.username,
264 secure_index => constant::SECURE_INDEX,
265 directories => listing_page.directories,
266 secured_directories => listing_page.secured_directories,
267 secure_path => &secure_flag
268 )).unwrap());
269 }
270 log::error!("Something went horribly wrong");
271 log::error!("Media Path: {}", filepath);
272 log::error!("Target: {}", __target_str);
273 HttpResponse::ExpectationFailed().json(routes::auth::DetailError {
274 detail: format!("'{}' was neither a file nor a folder", filepath)
275 })
276}
277
278#[get("/media")]
294pub async fn streaming_endpoint(request: HttpRequest,
295 info: web::Query<Payload>,
296 fernet: web::Data<Arc<Fernet>>,
297 session: web::Data<Arc<constant::Session>>,
298 metadata: web::Data<Arc<constant::MetaData>>,
299 config: web::Data<Arc<squire::settings::Config>>,
300 template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
301 let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
302 if !auth_response.ok {
303 return routes::auth::failed_auth(auth_response, &config);
304 }
305 let media_path = config.media_source.join(&info.file);
306 if !squire::authenticator::verify_secure_index(&media_path, &auth_response.username) {
307 return squire::custom::error(
308 "RESTRICTED SECTION",
309 template.get_template("error").unwrap(),
310 &metadata.pkg_version,
311 format!("This content is not accessible, as it does not belong to the user profile '{}'", auth_response.username),
312 StatusCode::FORBIDDEN
313 );
314 }
315 let (host, _last_accessed) = squire::custom::log_connection(&request, &session);
316 if media_path.exists() {
317 let file = actix_files::NamedFile::open_async(media_path).await.unwrap();
318 let mut tracker = session.tracker.lock().unwrap();
320 if tracker.get(&host).unwrap() != &info.file {
321 log::info!("Streaming {}", info.file);
322 tracker.insert(host, info.file.to_string());
323 }
324 return file.into_response(&request);
325 }
326 let error = format!("File {:?} not found", media_path);
327 log::error!("{}", error);
328 squire::custom::error(
329 "CONTENT UNAVAILABLE",
330 template.get_template("error").unwrap(),
331 &metadata.pkg_version,
332 format!("'{}' was not found", &info.file),
333 StatusCode::NOT_FOUND
334 )
335}