Compare commits
	
		
			No commits in common. "61b3a5fe801f79cb614f4343f205228e7fa2c4ff" and "9671be1ff349586ae0e50d6fecbf4c8f961281d6" have entirely different histories.
		
	
	
		
			61b3a5fe80
			...
			9671be1ff3
		
	
		
							
								
								
									
										3
									
								
								clippable-svc/.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								clippable-svc/.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "rust.all_features": true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								clippable-svc/.ycm_extra_conf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								clippable-svc/.ycm_extra_conf.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					def Settings(**kwargs):
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        'ls': {
 | 
				
			||||||
 | 
					            'cargo': {
 | 
				
			||||||
 | 
					                'features': ['admin'],
 | 
				
			||||||
 | 
					                'noDefaultFeatures': True
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
							
								
								
									
										2112
									
								
								clippable-svc/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2112
									
								
								clippable-svc/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										24
									
								
								clippable-svc/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								clippable-svc/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "clippable-server"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2018"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					rocket = { version = "0.5.0-rc.1", features = [ "json" ] }
 | 
				
			||||||
 | 
					serde = { version = "1.0", features = [ "derive" ] }
 | 
				
			||||||
 | 
					lazy_static = { version = "1.4.0" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Required only for builds that contain admin features
 | 
				
			||||||
 | 
					serde_json = { version = "1.0", optional = true }
 | 
				
			||||||
 | 
					base64 = { version = "0.13.0", optional = true }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies.rocket_dyn_templates]
 | 
				
			||||||
 | 
					version = "0.1.0-rc.1"
 | 
				
			||||||
 | 
					features = ["tera"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[features]
 | 
				
			||||||
 | 
					default = []
 | 
				
			||||||
 | 
					admin = ["serde_json", "base64"]
 | 
				
			||||||
							
								
								
									
										51
									
								
								clippable-svc/src/admin/apikey.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								clippable-svc/src/admin/apikey.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					use rocket::request::{Outcome, Request, FromRequest};
 | 
				
			||||||
 | 
					use rocket::async_trait;
 | 
				
			||||||
 | 
					use rocket::http::Status;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::db::{self, DB_PATH};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ApiKey {
 | 
				
			||||||
 | 
					    // These are used by rocket's driver code/decl macros however cargo
 | 
				
			||||||
 | 
					    // is not able to check those as the code is generated at compile time.
 | 
				
			||||||
 | 
					    // The dead code thing is just to stifle pointless warnings
 | 
				
			||||||
 | 
					    #[allow(dead_code)]
 | 
				
			||||||
 | 
					    uid: String,
 | 
				
			||||||
 | 
					    #[allow(dead_code)]
 | 
				
			||||||
 | 
					    key: String
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub enum ApiKeyError {
 | 
				
			||||||
 | 
					    Missing,
 | 
				
			||||||
 | 
					    Invalid,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl<'r> FromRequest<'r> for ApiKey {
 | 
				
			||||||
 | 
					    type Error = ApiKeyError;
 | 
				
			||||||
 | 
					    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
 | 
				
			||||||
 | 
					        let key = req.headers().get_one("ADMIN-API-KEY");
 | 
				
			||||||
 | 
					        let uid = req.headers().get_one("ADMIN-API-UID");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if key.is_none() || uid.is_none() {
 | 
				
			||||||
 | 
					            return Outcome::Failure((Status::Forbidden, ApiKeyError::Missing));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (key, uid) = (key.unwrap(), uid.unwrap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        println!("Path to use for db file {:?}", DB_PATH.to_string());
 | 
				
			||||||
 | 
					        let db = db::Database::load(DB_PATH.as_str().into()).unwrap();
 | 
				
			||||||
 | 
					        if let Some(stored) = db.get(uid) {
 | 
				
			||||||
 | 
					            if stored == key {
 | 
				
			||||||
 | 
					                return Outcome::Success(ApiKey {
 | 
				
			||||||
 | 
					                    key: key.into(),
 | 
				
			||||||
 | 
					                    uid: uid.into()
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return Outcome::Failure((Status::Forbidden, ApiKeyError::Invalid))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Outcome::Failure((Status::Forbidden, ApiKeyError::Invalid))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										76
									
								
								clippable-svc/src/admin/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								clippable-svc/src/admin/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					#[cfg(feature = "admin")]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod apikey;
 | 
				
			||||||
 | 
					mod response;
 | 
				
			||||||
 | 
					mod util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					use std::io::Result;
 | 
				
			||||||
 | 
					use std::fs;
 | 
				
			||||||
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use rocket::data::{Data, ToByteUnit};
 | 
				
			||||||
 | 
					use rocket::serde::json::Json;
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					use rocket::response::Redirect;
 | 
				
			||||||
 | 
					use response::{bad_request, ok};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use apikey::ApiKey;
 | 
				
			||||||
 | 
					use response::ActionResponse;
 | 
				
			||||||
 | 
					use crate::common::get_clips_dir;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/")]
 | 
				
			||||||
 | 
					pub async fn login_dashboard_redirect()  -> Redirect {
 | 
				
			||||||
 | 
					    Redirect::to("/admin/dashboard")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/dashboard")]
 | 
				
			||||||
 | 
					pub async fn login_dashboard() -> Template {
 | 
				
			||||||
 | 
					    // This page is basically just a login form
 | 
				
			||||||
 | 
					    // However the rest of the form is present on this page, just hidden
 | 
				
			||||||
 | 
					    let h: HashMap<i32,i32> = HashMap::new(); // does not allocate
 | 
				
			||||||
 | 
					    return Template::render("admin", &h);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[post("/dashboard")]
 | 
				
			||||||
 | 
					pub async fn dashboard(_key: ApiKey) -> Json<ActionResponse> {
 | 
				
			||||||
 | 
					    // Assuming the api key check doesn't fail we can reply with Ok
 | 
				
			||||||
 | 
					    // at the application level
 | 
				
			||||||
 | 
					    ok()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[post("/upload-video/<category>/<filename>", data = "<data>")]
 | 
				
			||||||
 | 
					pub async fn updload_video(_key: ApiKey, category: PathBuf, filename: PathBuf, data: Data<'_>) 
 | 
				
			||||||
 | 
					-> Result<Json<ActionResponse>> {
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
 | 
					     * Uploads must have BOTH a valid filename and a category
 | 
				
			||||||
 | 
					     * Without the category the server will simply not find
 | 
				
			||||||
 | 
					     * the correct endpoint to reach and thus will 404
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    if util::valid_filename(&filename) == false {
 | 
				
			||||||
 | 
					        return Ok(bad_request(Some("Invalid filename(s)")));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let clips = get_clips_dir();
 | 
				
			||||||
 | 
					    fs::create_dir_all(Path::new(&clips).join(&category))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
 | 
					     * We allow up to 200 Megaytes per upload as most short
 | 
				
			||||||
 | 
					     * clips are not going to be very large anyway and this
 | 
				
			||||||
 | 
					     * should be a reasonably high limit for those that want
 | 
				
			||||||
 | 
					     * to upload "large" clips
 | 
				
			||||||
 | 
					     * */
 | 
				
			||||||
 | 
					    let filepath = Path::new(&clips).join(category).join(filename);
 | 
				
			||||||
 | 
					    data.open(250.megabytes()).into_file(filepath).await?;
 | 
				
			||||||
 | 
					    Ok(ok())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[delete("/remove-video/<category>/<filename>")]
 | 
				
			||||||
 | 
					pub async fn remove_video(_key: ApiKey, category: PathBuf, filename: PathBuf) 
 | 
				
			||||||
 | 
					-> Result<Json<ActionResponse>> {
 | 
				
			||||||
 | 
					    let clips = get_clips_dir();
 | 
				
			||||||
 | 
					    let path = Path::new(&clips).join(&category).join(&filename);
 | 
				
			||||||
 | 
					    fs::remove_file(path)?;
 | 
				
			||||||
 | 
					    Ok(ok())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								clippable-svc/src/admin/response.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								clippable-svc/src/admin/response.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Module handles general responses for the admin feature
 | 
				
			||||||
 | 
					 * Primarily these are responses for Admin related actions
 | 
				
			||||||
 | 
					 * like fetching video's, updating videos and deleting them
 | 
				
			||||||
 | 
					 * as well
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use rocket::serde::json::Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FAIL: &'static str = "fail";
 | 
				
			||||||
 | 
					const OK: &'static str = "fail";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					pub struct ActionResponse {
 | 
				
			||||||
 | 
					    status: &'static str,
 | 
				
			||||||
 | 
					    code: i32,
 | 
				
			||||||
 | 
					    details: Option<&'static str>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn ok() -> Json<ActionResponse> {
 | 
				
			||||||
 | 
					    Json(ActionResponse {
 | 
				
			||||||
 | 
					        status: OK,
 | 
				
			||||||
 | 
					        code: 200,
 | 
				
			||||||
 | 
					        details: None
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn bad_request(text: Option<&'static str>) -> Json<ActionResponse> {
 | 
				
			||||||
 | 
					    Json(ActionResponse {
 | 
				
			||||||
 | 
					        status: FAIL,
 | 
				
			||||||
 | 
					        code: 400,
 | 
				
			||||||
 | 
					        details: text
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								clippable-svc/src/admin/util.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								clippable-svc/src/admin/util.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					use std::path::PathBuf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn valid_filename(p: &PathBuf) -> bool {
 | 
				
			||||||
 | 
					    // Checks if a given filename is actually valid or not
 | 
				
			||||||
 | 
					    let mut valid = false;
 | 
				
			||||||
 | 
					    let s = p.file_name().unwrap_or_default().to_string_lossy();
 | 
				
			||||||
 | 
					    for e in [".mp4", ".webm", ".mkv"] {
 | 
				
			||||||
 | 
					        if s.ends_with(e) {
 | 
				
			||||||
 | 
					            valid = true;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    valid
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										82
									
								
								clippable-svc/src/category.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								clippable-svc/src/category.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use std::fs::DirEntry;
 | 
				
			||||||
 | 
					use std::path::Path;
 | 
				
			||||||
 | 
					use std::io;
 | 
				
			||||||
 | 
					use rocket::serde::json::Json;
 | 
				
			||||||
 | 
					use crate::common::{get_clips_dir, thumbs_dir};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Describes a category of videos as 
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					pub struct Category {
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    /// NOTE: this is simply a URI pathname
 | 
				
			||||||
 | 
					    /// EXAMPLE: /thumbnail/<category>/.thumbnail.png
 | 
				
			||||||
 | 
					    thumbnail: String
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns a vector of category directories
 | 
				
			||||||
 | 
					pub fn get_category_dirs(path: &str) -> std::io::Result<Vec<DirEntry>> {
 | 
				
			||||||
 | 
					    let path = std::path::Path::new(path);
 | 
				
			||||||
 | 
					    // Trying to ignore non-directory entries
 | 
				
			||||||
 | 
					    if !path.is_dir() {
 | 
				
			||||||
 | 
					        let e = io::Error::new(io::ErrorKind::NotFound, "Unable to open");
 | 
				
			||||||
 | 
					        return Err(e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut ret: Vec<DirEntry> = Vec::new();
 | 
				
			||||||
 | 
					    for entry in (std::fs::read_dir(path)?).flatten() {
 | 
				
			||||||
 | 
					        if entry.path().is_dir() {
 | 
				
			||||||
 | 
					            ret.push(entry)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(ret)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Will return the path to a category's thumb nail assuming it exists.
 | 
				
			||||||
 | 
					/// If nothing is found then it gives back the URI path to a not-found image
 | 
				
			||||||
 | 
					pub fn get_category_thumbnail(category: &str) -> std::io::Result<String> {
 | 
				
			||||||
 | 
					    let pathname = format!("{}/{}", thumbs_dir(), &category);
 | 
				
			||||||
 | 
					    let path = Path::new(&pathname);
 | 
				
			||||||
 | 
					    // Assume directory as we're only called from "safe" places
 | 
				
			||||||
 | 
					    let item = path.read_dir()?.find(|file| {
 | 
				
			||||||
 | 
					        if let Ok(file) = file {
 | 
				
			||||||
 | 
					            let name = file.file_name().into_string().unwrap();
 | 
				
			||||||
 | 
					            name == "category-thumbnail.jpg"
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Ok(match item {
 | 
				
			||||||
 | 
					        Some(name) => {
 | 
				
			||||||
 | 
					            let name = name.unwrap().file_name().into_string().unwrap();
 | 
				
			||||||
 | 
					            format!("/thumbnail/{}/{}", category, name)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        None => "/static/cantfindshit.jpg".to_string()
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns a List of categories
 | 
				
			||||||
 | 
					/// Primarily used on the main page
 | 
				
			||||||
 | 
					/// WARN: misconfigured servers are just going to get shafted and serve up
 | 
				
			||||||
 | 
					/// a tonne of 500's
 | 
				
			||||||
 | 
					#[get("/categories")]
 | 
				
			||||||
 | 
					pub fn list() -> Json<Vec<Category>> {
 | 
				
			||||||
 | 
					    let dir = get_clips_dir();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut cats: Vec<Category> = Vec::new();
 | 
				
			||||||
 | 
					    if let Ok(dirs) = get_category_dirs(&dir) {
 | 
				
			||||||
 | 
					        // Let's just assume that each item in this directory is a folder
 | 
				
			||||||
 | 
					        // That way we can do this blindly without 9999 allocs
 | 
				
			||||||
 | 
					        for d in dirs {
 | 
				
			||||||
 | 
					            let name = d.file_name().to_string_lossy().to_string();
 | 
				
			||||||
 | 
					            let thumbnail = match get_category_thumbnail(&name) {
 | 
				
			||||||
 | 
					                Ok(s) => s,
 | 
				
			||||||
 | 
					                _ => "/static/cantfindshit.jpg".to_string()
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            cats.push(Category {name, thumbnail});
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Json(cats)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								clippable-svc/src/common.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								clippable-svc/src/common.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns the absolute path to the videos directory
 | 
				
			||||||
 | 
					pub fn get_clips_dir() -> String {
 | 
				
			||||||
 | 
					    match env::var("CLIPS_DIR") {
 | 
				
			||||||
 | 
					        Ok(val) => val,
 | 
				
			||||||
 | 
					        Err(_) => "/media/clips/".to_string()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns the absolute path to the thumbnails directory
 | 
				
			||||||
 | 
					pub fn thumbs_dir() -> String {
 | 
				
			||||||
 | 
					    match env::var("THUMBS_DIR") {
 | 
				
			||||||
 | 
					        Ok(val) => val,
 | 
				
			||||||
 | 
					        Err(_) => "/media/thumbnails/".to_string()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										117
									
								
								clippable-svc/src/db.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								clippable-svc/src/db.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
				
			|||||||
 | 
					#[cfg(feature = "admin")]
 | 
				
			||||||
 | 
					// This module defines a tiny async interface for the "database" that this
 | 
				
			||||||
 | 
					// project uses for interfacing with the key store
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// WARN: at the moment there are no guarantees as far as data integrity is
 | 
				
			||||||
 | 
					// concerned. This means there are no real transactions
 | 
				
			||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					use std::fs::OpenOptions;
 | 
				
			||||||
 | 
					use std::io::{BufWriter, BufReader};
 | 
				
			||||||
 | 
					use std::path::PathBuf;
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					use rocket::serde::{Serialize, Deserialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    pub static ref DB_PATH: String = {
 | 
				
			||||||
 | 
					        env::var("DB_PATH").unwrap_or("keys.db".into())
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct Database {
 | 
				
			||||||
 | 
					    // uid's while random are fine to release as public as the key is more
 | 
				
			||||||
 | 
					    // important however ideally neither should be release. Furthermore
 | 
				
			||||||
 | 
					    // the frontend assists in keeping these secret by treating both as
 | 
				
			||||||
 | 
					    // password fields as they are both randomly generated via a script
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // uid -> key
 | 
				
			||||||
 | 
					    users: HashMap<String,String>,
 | 
				
			||||||
 | 
					    #[serde(skip)]
 | 
				
			||||||
 | 
					    path: PathBuf
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Database {
 | 
				
			||||||
 | 
					    // Opens a handle to a database file
 | 
				
			||||||
 | 
					    // if none is found then one is created with the new path
 | 
				
			||||||
 | 
					    // if there is one then the existing database is used
 | 
				
			||||||
 | 
					    // any thing else is invalid and causes this to return Err
 | 
				
			||||||
 | 
					    pub fn new(path: PathBuf) -> Result<Self, std::io::Error> {
 | 
				
			||||||
 | 
					        let file = OpenOptions::new()
 | 
				
			||||||
 | 
					            .write(true)
 | 
				
			||||||
 | 
					            .create(true)
 | 
				
			||||||
 | 
					            .open(&path)?;
 | 
				
			||||||
 | 
					        let writer = BufWriter::new(&file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Dummy value to write in place
 | 
				
			||||||
 | 
					        let empty = Database { users: HashMap::new(), path: "".into() };
 | 
				
			||||||
 | 
					        serde_json::to_writer(writer, &empty)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(empty)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn load(path: PathBuf) -> Result<Self, std::io::Error> {
 | 
				
			||||||
 | 
					        let file = OpenOptions::new()
 | 
				
			||||||
 | 
					            .read(true)
 | 
				
			||||||
 | 
					            .open(&path)?;
 | 
				
			||||||
 | 
					        let reader = BufReader::new(&file);
 | 
				
			||||||
 | 
					        let mut data: Database = serde_json::from_reader(reader)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data.path = path;
 | 
				
			||||||
 | 
					        return Ok(data);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get(&self, uid: &str) -> Option<&String> {
 | 
				
			||||||
 | 
					        return self.users.get(uid);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn write(&self) -> Result<(), std::io::Error> {
 | 
				
			||||||
 | 
					        let file = OpenOptions::new()
 | 
				
			||||||
 | 
					            .write(true)
 | 
				
			||||||
 | 
					            .open(&self.path)?;
 | 
				
			||||||
 | 
					        let writer = BufWriter::new(file);
 | 
				
			||||||
 | 
					        serde_json::to_writer(writer, &self.path)?;
 | 
				
			||||||
 | 
					        return Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn remove(&mut self, uid: &str) -> Result<(), std::io::Error> {
 | 
				
			||||||
 | 
					        self.users.remove_entry(uid);
 | 
				
			||||||
 | 
					        self.write()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn add(&mut self, key: &str, value: &str) -> Result<(), std::io::Error> {
 | 
				
			||||||
 | 
					        println!("{:?}", self.path);
 | 
				
			||||||
 | 
					        self.users.insert(key.into(), value.into());
 | 
				
			||||||
 | 
					        self.write()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod db_tests {
 | 
				
			||||||
 | 
					    use super::Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const DB: &'static str = "new.db";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn load_db() {
 | 
				
			||||||
 | 
					        match Database::new(DB.into()) {
 | 
				
			||||||
 | 
					            Ok(db) => println!("Loaded new sample.db: {:?}", db),
 | 
				
			||||||
 | 
					            Err(e) => panic!("Error fetching database: {}", e)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn add_simple_entries() {
 | 
				
			||||||
 | 
					        match Database::load(DB.into()) {
 | 
				
			||||||
 | 
					            Ok(mut db) => db.add("key", "value").unwrap(),
 | 
				
			||||||
 | 
					            Err(e) => println!("Error adding entries: {}", e)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn remove_simple_entries() {
 | 
				
			||||||
 | 
					        match Database::load(DB.into()) {
 | 
				
			||||||
 | 
					            Ok(mut db) => db.remove("key").unwrap(),
 | 
				
			||||||
 | 
					            Err(e) => println!("Error removing simple entries: {}", e)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										55
									
								
								clippable-svc/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								clippable-svc/src/main.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					//#![feature(decl_macro)]
 | 
				
			||||||
 | 
					#[macro_use] extern crate rocket;
 | 
				
			||||||
 | 
					#[macro_use] extern crate lazy_static;
 | 
				
			||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod page;
 | 
				
			||||||
 | 
					mod category;
 | 
				
			||||||
 | 
					mod video;
 | 
				
			||||||
 | 
					mod thumbnail;
 | 
				
			||||||
 | 
					mod common;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "admin")]
 | 
				
			||||||
 | 
					mod admin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "admin")]
 | 
				
			||||||
 | 
					mod sec;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "admin")]
 | 
				
			||||||
 | 
					mod db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[rocket::main]
 | 
				
			||||||
 | 
					async fn main() {
 | 
				
			||||||
 | 
					    // rid ourselves of random emoji's in logs
 | 
				
			||||||
 | 
					    env::set_var("ROCKET_CLI_COLORS", "false");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if cfg!(feature = "admin") {
 | 
				
			||||||
 | 
					        #[cfg(feature = "admin")]
 | 
				
			||||||
 | 
					        let _ = rocket::build()
 | 
				
			||||||
 | 
					            .mount("/", routes![page::home, page::category, page::video]) // html
 | 
				
			||||||
 | 
					            .mount("/static", routes![page::files]) // files
 | 
				
			||||||
 | 
					            .mount("/api", routes![category::list, video::list]) // json
 | 
				
			||||||
 | 
					            .mount("/thumbnail", routes![thumbnail::get]) // images
 | 
				
			||||||
 | 
					            .mount("/video", routes![video::get_video]) // videos
 | 
				
			||||||
 | 
					            .mount("/admin", routes![
 | 
				
			||||||
 | 
					              admin::login_dashboard_redirect, 
 | 
				
			||||||
 | 
					              admin::login_dashboard, 
 | 
				
			||||||
 | 
					              admin::dashboard,
 | 
				
			||||||
 | 
					              admin::updload_video,
 | 
				
			||||||
 | 
					              admin::remove_video
 | 
				
			||||||
 | 
					            ])
 | 
				
			||||||
 | 
					            .attach(Template::fairing())
 | 
				
			||||||
 | 
					            .launch().await;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let _ = rocket::build()
 | 
				
			||||||
 | 
					            .mount("/", routes![page::home, page::category, page::video]) // html
 | 
				
			||||||
 | 
					            .mount("/static", routes![page::files]) // files
 | 
				
			||||||
 | 
					            .mount("/api", routes![category::list, video::list]) // json
 | 
				
			||||||
 | 
					            .mount("/thumbnail", routes![thumbnail::get]) // images
 | 
				
			||||||
 | 
					            .mount("/video", routes![video::get_video]) // videos
 | 
				
			||||||
 | 
					            .attach(Template::fairing())
 | 
				
			||||||
 | 
					            .launch().await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										121
									
								
								clippable-svc/src/page.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								clippable-svc/src/page.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,121 @@
 | 
				
			|||||||
 | 
					/// This module takes care of returning bare webpage templates
 | 
				
			||||||
 | 
					/// Most of the work done here is simply to fill the templates
 | 
				
			||||||
 | 
					/// with the appropriate meta data that is required 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					use rocket::fs::NamedFile;
 | 
				
			||||||
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					use crate::common::get_clips_dir;
 | 
				
			||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: try... literally try to reduce the number of clones that happen here
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    static ref SITENAME: String = {
 | 
				
			||||||
 | 
					        env::var("SITE_NAME").unwrap_or_else(|_| "Clippable".to_string())
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    static ref SITEDESC: String = {
 | 
				
			||||||
 | 
					        env::var("SITE_DESC").unwrap_or_else(|_| "Short clips".to_string())
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    static ref SITEURL: String = {
 | 
				
			||||||
 | 
					        env::var("SITE_URL").unwrap_or_else(|_| "#".to_string())
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Default metadata that all web pages must be filled with
 | 
				
			||||||
 | 
					pub fn default_map() -> HashMap<&'static str, String> {
 | 
				
			||||||
 | 
					    let mut h: HashMap<&'static str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    h.insert("sitetitle", SITENAME.clone());
 | 
				
			||||||
 | 
					    h.insert("sitedesc", SITEDESC.clone());
 | 
				
			||||||
 | 
					    h.insert("siteurl", SITEURL.clone());
 | 
				
			||||||
 | 
					    h
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/")]
 | 
				
			||||||
 | 
					pub async fn home() -> Template {
 | 
				
			||||||
 | 
					    let mut h = default_map();
 | 
				
			||||||
 | 
					    h.insert("page", String::from("home"));
 | 
				
			||||||
 | 
					    h.insert("title", SITENAME.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Template::render("list", &h)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/category/<cat..>")]
 | 
				
			||||||
 | 
					pub async fn category(cat: PathBuf) -> Template {
 | 
				
			||||||
 | 
					    let mut h = default_map();
 | 
				
			||||||
 | 
					    h.insert("page", String::from("category"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Opengraph meta tags bro
 | 
				
			||||||
 | 
					    let cat = cat.file_name().unwrap().to_string_lossy();
 | 
				
			||||||
 | 
					    h.insert("title", cat.to_string());
 | 
				
			||||||
 | 
					    h.insert("url", format!("/category/{}", cat));
 | 
				
			||||||
 | 
					    h.insert("desc", format!("{} clips", cat));
 | 
				
			||||||
 | 
					    h.insert("ogimg", format!("/thumbnail/{}/category-thumbnail.jpg", cat));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Template::render("list", &h)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn map_base_vfile(category: &Path, base: &Path) -> String {
 | 
				
			||||||
 | 
					    // This takes a category and base filename and basically looks for the file
 | 
				
			||||||
 | 
					    // that maps to those parameters
 | 
				
			||||||
 | 
					    // This basically aims to get rid of the extension in the URL so that social
 | 
				
			||||||
 | 
					    // media sites will bother to fetch open graph tags
 | 
				
			||||||
 | 
					    let basename = base.to_string_lossy();
 | 
				
			||||||
 | 
					    let dirname = format!("{}/{}/", get_clips_dir(), category.to_string_lossy());
 | 
				
			||||||
 | 
					    let dir = Path::new(&dirname);
 | 
				
			||||||
 | 
					    let file = dir.read_dir().ok().unwrap().find(|item| {
 | 
				
			||||||
 | 
					        if let Ok(item) = item {
 | 
				
			||||||
 | 
					            let name = item.file_name().into_string().unwrap();
 | 
				
			||||||
 | 
					            name.starts_with(basename.as_ref())
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    match file {
 | 
				
			||||||
 | 
					        Some(ent) => ent.unwrap().file_name().into_string().unwrap(),
 | 
				
			||||||
 | 
					        None => "/static/cantfindshit.jpg".to_string()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/clip/<cat>/<file_base>")]
 | 
				
			||||||
 | 
					pub fn video(cat: PathBuf, file_base: PathBuf) -> Template {
 | 
				
			||||||
 | 
					    let mut h: HashMap<&'static str, &str> = HashMap::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Look for the file_base + [extension]
 | 
				
			||||||
 | 
					    let file = map_base_vfile(&cat, &file_base);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let cat = cat.to_string_lossy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut file_pretty = file.to_string();
 | 
				
			||||||
 | 
					    for c in [".mp4", ".mkv", ".webm"] {
 | 
				
			||||||
 | 
					        file_pretty = file_pretty.replace(c, "");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h.insert("title", &file_pretty);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let url = format!("/clip/{}/{}", &cat, &file);
 | 
				
			||||||
 | 
					    h.insert("url", &url);
 | 
				
			||||||
 | 
					    h.insert("page", "video");
 | 
				
			||||||
 | 
					    h.insert("desc", &SITEDESC);
 | 
				
			||||||
 | 
					    h.insert("category", &cat);
 | 
				
			||||||
 | 
					    h.insert("filename", &file_pretty);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let thumbnail = format!("/thumbnail/{}/{}", cat, file);
 | 
				
			||||||
 | 
					    h.insert("ogimg", &thumbnail);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let clip_url = format!("/video/{}/{}", &cat, &file);
 | 
				
			||||||
 | 
					    h.insert("clip_url", &clip_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let clip_thumbnail = format!("/thumbnail/{}/{}.jpg", &cat, &file);
 | 
				
			||||||
 | 
					    h.insert("clip_thumbnail", &clip_thumbnail);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Template::render("video", &h)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// This route handler returns static files that are comprised
 | 
				
			||||||
 | 
					/// of JS, CSS, the favicon and other static items
 | 
				
			||||||
 | 
					#[get("/<file..>")]
 | 
				
			||||||
 | 
					pub async fn files(file: PathBuf) -> Option<NamedFile> {
 | 
				
			||||||
 | 
					    NamedFile::open(Path::new("static/").join(file)).await.ok()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								clippable-svc/src/sec.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								clippable-svc/src/sec.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					#[cfg(feature = "admin")]
 | 
				
			||||||
 | 
					// This module concerns itself with the encrypting/decrypting of passwords
 | 
				
			||||||
 | 
					// as well as the storage of those items
 | 
				
			||||||
 | 
					use rocket::tokio::io::AsyncReadExt;
 | 
				
			||||||
 | 
					use rocket::tokio::fs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn random_string() -> Result<String, std::io::Error> {
 | 
				
			||||||
 | 
					    // First we read in some bytes from /dev/urandom
 | 
				
			||||||
 | 
					    let mut handle = fs::File::open("/dev/urandom").await?;
 | 
				
			||||||
 | 
					    let mut buffer = [0;32];
 | 
				
			||||||
 | 
					    handle.read(&mut buffer[..]).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(base64::encode_config(buffer, base64::URL_SAFE_NO_PAD))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod sec_api_tests {
 | 
				
			||||||
 | 
					    use rocket::tokio;
 | 
				
			||||||
 | 
					    use super::random_string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn generate_string() {
 | 
				
			||||||
 | 
					        println!("{:?}", random_string().await);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								clippable-svc/src/thumbnail.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								clippable-svc/src/thumbnail.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					use rocket::fs::NamedFile;
 | 
				
			||||||
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
 | 
					use crate::common::thumbs_dir;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns a thumbnail whose formatted name is <video-name>.<ext>.jpg
 | 
				
			||||||
 | 
					#[get("/<file..>")]
 | 
				
			||||||
 | 
					pub async fn get(file: PathBuf) -> Option<NamedFile> {
 | 
				
			||||||
 | 
					    let clips_dir = thumbs_dir();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Only serve jpg's and png's through this route
 | 
				
			||||||
 | 
					    let file_path = Path::new(&clips_dir).join(file);
 | 
				
			||||||
 | 
					    if file_path.is_file() {
 | 
				
			||||||
 | 
					        return match file_path.extension() {
 | 
				
			||||||
 | 
					            Some(ext) => {
 | 
				
			||||||
 | 
					                match ext == "jpg" {
 | 
				
			||||||
 | 
					                    true => NamedFile::open(file_path).await.ok(),
 | 
				
			||||||
 | 
					                    false => None
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            None => None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let path = Path::new("static/cantfindshit.jpg");
 | 
				
			||||||
 | 
					        return NamedFile::open(path).await.ok();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										71
									
								
								clippable-svc/src/video.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								clippable-svc/src/video.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
 | 
					use std::fs::DirEntry;
 | 
				
			||||||
 | 
					use std::ffi::OsStr;
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use rocket::serde::json::Json;
 | 
				
			||||||
 | 
					use rocket::fs::NamedFile;
 | 
				
			||||||
 | 
					use crate::common::get_clips_dir;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					pub struct VideoPreview  {
 | 
				
			||||||
 | 
					    /// Real filename on disk
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    /// URI path to what the browser can reasonably expect
 | 
				
			||||||
 | 
					    thumbnail: Option<String>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn vid_file_entries(path: &OsStr) -> std::io::Result<Vec<DirEntry>> {
 | 
				
			||||||
 | 
					    let mut dir = get_clips_dir();
 | 
				
			||||||
 | 
					    dir.push('/'); dir.push_str(&path.to_string_lossy());
 | 
				
			||||||
 | 
					    let path = std::path::Path::new(&dir);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if !path.is_dir() {
 | 
				
			||||||
 | 
					        panic!("<{:?}> is not a valid directory", path);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let mut entries: Vec<DirEntry> = Vec::new();
 | 
				
			||||||
 | 
					    for ent in (path.read_dir()?).flatten() {
 | 
				
			||||||
 | 
					        let name = ent.file_name().into_string().unwrap();
 | 
				
			||||||
 | 
					        if name.ends_with("mkv") || name.ends_with("mp4") || name.ends_with("webm") {
 | 
				
			||||||
 | 
					            entries.push(ent);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(entries)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/category/<cat..>")]
 | 
				
			||||||
 | 
					pub fn list(cat: PathBuf) -> Option<Json<Vec<VideoPreview>>> {
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
 | 
					     * List out the videos to a given category
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    // First we have to make sure this given category is even registered with us
 | 
				
			||||||
 | 
					    let file_path = cat.file_name().unwrap_or_else(|| OsStr::new(""));
 | 
				
			||||||
 | 
					    if let Ok(entries) = vid_file_entries(file_path) {
 | 
				
			||||||
 | 
					        let mut previews: Vec<VideoPreview> = Vec::new();
 | 
				
			||||||
 | 
					        // Autismo but at least its bare-able
 | 
				
			||||||
 | 
					        for ent in entries {
 | 
				
			||||||
 | 
					            let name = ent.file_name(); 
 | 
				
			||||||
 | 
					            let name = name.to_string_lossy();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let cat = cat.to_string_lossy();
 | 
				
			||||||
 | 
					            let thumbnail = format!("/thumbnail/{}/{}.jpg", cat, name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let item = VideoPreview {
 | 
				
			||||||
 | 
					                name: name.to_string(),
 | 
				
			||||||
 | 
					                thumbnail: Some(thumbnail)
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            previews.push(item);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Some(Json(previews))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    None
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Endpoint returns the video file itself that is saved on the server
 | 
				
			||||||
 | 
					#[get("/<cat>/<file>")]
 | 
				
			||||||
 | 
					pub async fn get_video(cat: PathBuf, file: PathBuf) -> Option<NamedFile> {
 | 
				
			||||||
 | 
					    let clips_dir = get_clips_dir();
 | 
				
			||||||
 | 
					    let path = Path::new(&clips_dir).join(cat).join(file);
 | 
				
			||||||
 | 
					    NamedFile::open(path).await.ok()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										3
									
								
								clippable-svc/static/cantfindshit.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								clippable-svc/static/cantfindshit.jpg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					version https://git-lfs.github.com/spec/v1
 | 
				
			||||||
 | 
					oid sha256:bc7637a1b3f7e75322e3dddb3499931dc9cd57804afbf46c7941e6bc5211d03c
 | 
				
			||||||
 | 
					size 21076
 | 
				
			||||||
							
								
								
									
										100
									
								
								clippable-svc/static/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								clippable-svc/static/css/style.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					html, body, div, tag {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    border: 0;
 | 
				
			||||||
 | 
					    font-size: 100%;
 | 
				
			||||||
 | 
					    vertical-align: baseline;
 | 
				
			||||||
 | 
					    background: #212121;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* {
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h1 {
 | 
				
			||||||
 | 
						text-transform: capitalize;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a {
 | 
				
			||||||
 | 
						text-decoration: none;
 | 
				
			||||||
 | 
						color: whitesmoke;
 | 
				
			||||||
 | 
					   text-shadow:
 | 
				
			||||||
 | 
							3px 3px 0   #191818,
 | 
				
			||||||
 | 
							-1px -1px 0 #191818,  
 | 
				
			||||||
 | 
							1px -1px 0  #191818,
 | 
				
			||||||
 | 
							-1px 1px 0  #191818,
 | 
				
			||||||
 | 
							1px 1px 0   #191818;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					a:hover {
 | 
				
			||||||
 | 
						text-decoration: none;
 | 
				
			||||||
 | 
						color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.content {
 | 
				
			||||||
 | 
					    margin: 0 auto;
 | 
				
			||||||
 | 
					    padding: 0 2em;
 | 
				
			||||||
 | 
					    line-height: 1.6em;
 | 
				
			||||||
 | 
					    color: whitesmoke;
 | 
				
			||||||
 | 
						max-width: 80%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.video-gallery {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.video-block {
 | 
				
			||||||
 | 
						display: block;
 | 
				
			||||||
 | 
					    padding: 1em;
 | 
				
			||||||
 | 
					    width: 400px;
 | 
				
			||||||
 | 
					    line-height: 1em;
 | 
				
			||||||
 | 
						margin-right: 1em;
 | 
				
			||||||
 | 
						margin-bottom: 1em;
 | 
				
			||||||
 | 
						border-radius: 1em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						background: linear-gradient(#191818,#191818,50%,#00ffcc,50%,#00ffcc);
 | 
				
			||||||
 | 
						background-size: 100% 200%;
 | 
				
			||||||
 | 
						transition: all 0.2s ease;
 | 
				
			||||||
 | 
						animation: 0.4s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.video-block:hover {
 | 
				
			||||||
 | 
						background-position: 100% 100%;
 | 
				
			||||||
 | 
						animation: 0.4s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					video {
 | 
				
			||||||
 | 
						max-width: 100%;
 | 
				
			||||||
 | 
						max-height: 80vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pure-form {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.pure-img {
 | 
				
			||||||
 | 
						border-radius: 0.5em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.login-form {
 | 
				
			||||||
 | 
						max-width: 500px;
 | 
				
			||||||
 | 
						margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#video-meta {
 | 
				
			||||||
 | 
						text-align: left;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.admin-video-li {
 | 
				
			||||||
 | 
						color: black;
 | 
				
			||||||
 | 
						text-shadow: none;
 | 
				
			||||||
 | 
						padding-right: 1em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.admin-video-li:hover {
 | 
				
			||||||
 | 
						color: #0a58ca;
 | 
				
			||||||
 | 
						text-shadow: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.align-left {
 | 
				
			||||||
 | 
						text-align: left;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										3
									
								
								clippable-svc/static/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								clippable-svc/static/favicon.png
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					version https://git-lfs.github.com/spec/v1
 | 
				
			||||||
 | 
					oid sha256:0cfdedb04d45802c1c008c9610bda2df8e3482f195bd254c11ebe07205e2bd5d
 | 
				
			||||||
 | 
					size 8560
 | 
				
			||||||
							
								
								
									
										57
									
								
								clippable-svc/static/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								clippable-svc/static/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					<html>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
						<title>Clippable</title>
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/pure-min.css" integrity="sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" crossorigin="anonymous">
 | 
				
			||||||
 | 
						<link rel="stylesheet" type="text/css" href="/style.css">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					<div id="main">
 | 
				
			||||||
 | 
						<div class="content">
 | 
				
			||||||
 | 
							<form class="pure-form">
 | 
				
			||||||
 | 
								<h1>Clippable</h1>
 | 
				
			||||||
 | 
								<fieldset>
 | 
				
			||||||
 | 
									<input type="text" placeholder="Search"/>
 | 
				
			||||||
 | 
									<button class="pure-button pure-button-primary">Search</button>
 | 
				
			||||||
 | 
								</fieldset>
 | 
				
			||||||
 | 
							</form>
 | 
				
			||||||
 | 
							<div class="video-gallery ">
 | 
				
			||||||
 | 
								<div class="video-block">
 | 
				
			||||||
 | 
									<h2>Video title</h2>
 | 
				
			||||||
 | 
									<p>some random description that's actually really long for some reason</p>
 | 
				
			||||||
 | 
									<img class="pure-img" src="/img/tmp.png">
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="video-block">
 | 
				
			||||||
 | 
									<h2>Video title</h2>
 | 
				
			||||||
 | 
									<p>some random description that's actually really long for some reason</p>
 | 
				
			||||||
 | 
									<img class="pure-img" src="/img/tmp.png">
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="video-block">
 | 
				
			||||||
 | 
									<h2>Video title</h2>
 | 
				
			||||||
 | 
									<p>some random description that's actually really long for some reason</p>
 | 
				
			||||||
 | 
									<img class="pure-img" src="/img/tmp.png">
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="video-block">
 | 
				
			||||||
 | 
									<h2>Video title</h2>
 | 
				
			||||||
 | 
									<p>some random description that's actually really long for some reason</p>
 | 
				
			||||||
 | 
									<img class="pure-img" src="/img/tmp.png">
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="video-block">
 | 
				
			||||||
 | 
									<h2>Video title</h2>
 | 
				
			||||||
 | 
									<p>some random description that's actually really long for some reason</p>
 | 
				
			||||||
 | 
									<img class="pure-img" src="/img/tmp.png">
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="video-block">
 | 
				
			||||||
 | 
									<h2>Video title</h2>
 | 
				
			||||||
 | 
									<p>some random description that's actually really long for some reason</p>
 | 
				
			||||||
 | 
									<img class="pure-img" src="/img/tmp.png">
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="video-block">
 | 
				
			||||||
 | 
									<h2>Video title</h2>
 | 
				
			||||||
 | 
									<p>some random description that's actually really long for some reason</p>
 | 
				
			||||||
 | 
									<img class="pure-img" src="/img/tmp.png">
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										84
									
								
								clippable-svc/templates/admin.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								clippable-svc/templates/admin.html.tera
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
						<meta charset="utf-8">
 | 
				
			||||||
 | 
						<meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<link rel="stylesheet" type="text/css" href="/static/css/style.css">
 | 
				
			||||||
 | 
						<title>Clippable Admin Dashboard</title>
 | 
				
			||||||
 | 
						<link rel="shortcut icon" type="image/png" href="/static/favicon.png"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<script src="/static/dist/bundle.js"></script>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					<div class="content">
 | 
				
			||||||
 | 
						<nav class="navbar navbar-expand-lg navbar-dark">
 | 
				
			||||||
 | 
							<a style="bread-crumb-item text-transform: capitalize" class="navbar-brand" href="/">
 | 
				
			||||||
 | 
								<img src="/static/favicon.png" class="rounded" width="50" height="50">
 | 
				
			||||||
 | 
							</a>
 | 
				
			||||||
 | 
							<ol class="breadcrumb">
 | 
				
			||||||
 | 
								<li class="breadcrumb-item">
 | 
				
			||||||
 | 
									<a href="/">Home</a>
 | 
				
			||||||
 | 
								</li>
 | 
				
			||||||
 | 
							</ol>
 | 
				
			||||||
 | 
						</nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class="content" id="login-display">
 | 
				
			||||||
 | 
							<h2>Login to Instance</h2>
 | 
				
			||||||
 | 
							<form class="form-inline login-form">
 | 
				
			||||||
 | 
							  <div class="input-group mb-2">
 | 
				
			||||||
 | 
								<input type="password" id="uid" class="form-control mx-sm-3" placeholder="UID code">
 | 
				
			||||||
 | 
								<div class="form-check form-check-inline">
 | 
				
			||||||
 | 
									<input class="form-check-input" type="checkbox" onclick="toggle_visible('uid')">
 | 
				
			||||||
 | 
									<label class="form-check-label" for="showuid">Show uid</label>
 | 
				
			||||||
 | 
								</div> </div>
 | 
				
			||||||
 | 
							  <div class="input-group mb-2">
 | 
				
			||||||
 | 
								  <input type="password" id="apikey" class="form-control mx-sm-3" placeholder="API Key">
 | 
				
			||||||
 | 
								<div class="form-check form-check-inline">
 | 
				
			||||||
 | 
									<input class="form-check-input" type="checkbox" onclick="toggle_visible('apikey')">
 | 
				
			||||||
 | 
									<label class="form-check-label" for="showuid">Show key</label>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							  </div>
 | 
				
			||||||
 | 
							  <strong id='error' hidden></strong>
 | 
				
			||||||
 | 
							  <button type="button" class="btn btn-info" id="verify-login-btn">Submit</button>
 | 
				
			||||||
 | 
							</form>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="content" id="dashboard" hidden>
 | 
				
			||||||
 | 
							<div class="container">
 | 
				
			||||||
 | 
								<form>
 | 
				
			||||||
 | 
									<div class="form-group">
 | 
				
			||||||
 | 
										<label for="video-file">Pick out video file to upload</label>
 | 
				
			||||||
 | 
										<input type="file" class="form-control" id="video-file">
 | 
				
			||||||
 | 
										<label for="category">What category should this go in?</label>
 | 
				
			||||||
 | 
										<input type="text" class="form-control" id="category">
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</form>
 | 
				
			||||||
 | 
								<div id="video-meta" hidden>
 | 
				
			||||||
 | 
									<p>Video file name: <code id="vmn"></code></p>
 | 
				
			||||||
 | 
									<p>Video file size: <code id="vms"></code></p>
 | 
				
			||||||
 | 
									<p>Video file type: <code id="vmt"></code></p>
 | 
				
			||||||
 | 
								    <button type="button" class="btn btn-primary" id="confirm-upload-btn">Upload</button>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div id="upload-response"></div>
 | 
				
			||||||
 | 
								<div class="vids-meta-list">
 | 
				
			||||||
 | 
									<h1>Videos</h1>
 | 
				
			||||||
 | 
									<ul class="list-group" id="videos-list"></ul>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<script type="text/javascript">
 | 
				
			||||||
 | 
					function toggle_visible(id) {
 | 
				
			||||||
 | 
					  var x = document.getElementById(id);
 | 
				
			||||||
 | 
					  if (x.type === "password") {
 | 
				
			||||||
 | 
					    x.type = "text";
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    x.type = "password";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										41
									
								
								clippable-svc/templates/list.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								clippable-svc/templates/list.html.tera
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en"><head>
 | 
				
			||||||
 | 
						<meta charset="utf-8">
 | 
				
			||||||
 | 
						<meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
 | 
				
			||||||
 | 
						<link rel="stylesheet" type="text/css" href="/static/css/style.css">
 | 
				
			||||||
 | 
						<title>{{title}}</title>
 | 
				
			||||||
 | 
						<link rel="shortcut icon" type="image/png" href="/static/favicon.png"/>
 | 
				
			||||||
 | 
						{% if page == "category"  %}
 | 
				
			||||||
 | 
							<meta property="og:title" content="{{title}}">
 | 
				
			||||||
 | 
							<meta property="og:site_name" content="Clippable">
 | 
				
			||||||
 | 
							<meta property="og:url" content="{{url}}">
 | 
				
			||||||
 | 
							<meta property="og:description" content="{{desc}}">
 | 
				
			||||||
 | 
							<meta property="og:type" content="website">
 | 
				
			||||||
 | 
							{% if ogimg %}
 | 
				
			||||||
 | 
								<meta property="og:image" content="{{ogimg}}">
 | 
				
			||||||
 | 
							{% else %}
 | 
				
			||||||
 | 
								<meta property="og:image" content="/static/favicon.png">
 | 
				
			||||||
 | 
							{% endif %}
 | 
				
			||||||
 | 
						{# Otherwise we defautl to the home tags #}
 | 
				
			||||||
 | 
						{% else %}
 | 
				
			||||||
 | 
							<meta property="og:title" content="{{sitetitle}}">
 | 
				
			||||||
 | 
							<meta property="og:site_name" content="Clippable">
 | 
				
			||||||
 | 
							<meta property="og:url" content="{{siteurl}}">
 | 
				
			||||||
 | 
							<meta property="og:description" content="{{sitedesc}}">
 | 
				
			||||||
 | 
							<meta property="og:type" content="website">
 | 
				
			||||||
 | 
							<meta property="og:image" content="/static/favicon.png">
 | 
				
			||||||
 | 
						{% endif %}
 | 
				
			||||||
 | 
						<script src="/static/dist/bundle.js"></script>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
						<div id="layout">
 | 
				
			||||||
 | 
							<div class="content">
 | 
				
			||||||
 | 
								{% include "navbar" %}
 | 
				
			||||||
 | 
								<h1>{{title}}</h1>
 | 
				
			||||||
 | 
								<div class="video-gallery" id="main-container">
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										32
									
								
								clippable-svc/templates/navbar.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								clippable-svc/templates/navbar.html.tera
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					<nav class="navbar navbar-expand-lg navbar-dark">
 | 
				
			||||||
 | 
							<a style="bread-crumb-item text-transform: capitalize" class="navbar-brand" href="/">
 | 
				
			||||||
 | 
								<img src="/static/favicon.png" class="rounded" width="50" height="50">
 | 
				
			||||||
 | 
							</a>
 | 
				
			||||||
 | 
							<ol class="breadcrumb">
 | 
				
			||||||
 | 
								{% if page == "category" %}
 | 
				
			||||||
 | 
									<li class="breadcrumb-item">
 | 
				
			||||||
 | 
										<a href="/">Home</a>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
									<li class="breadcrumb-item active" aria-current="page">
 | 
				
			||||||
 | 
										<a href="{{url}}">{{title}}</a>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								{% elif page == "video" %}
 | 
				
			||||||
 | 
									<li class="breadcrumb-item">
 | 
				
			||||||
 | 
										<a href="/">Home</a>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
									<li class="breadcrumb-item">
 | 
				
			||||||
 | 
										<a href="/category/{{category}}">{{category}}</a>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
									<li class="breadcrumb-item active" aria-current="page">
 | 
				
			||||||
 | 
										<a href="{{url}}">{{filename}}</a>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								{% else %}
 | 
				
			||||||
 | 
									<li class="breadcrumb-item active" aria-current="page">
 | 
				
			||||||
 | 
										<a href="/">Home</a>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
								{% endif %}
 | 
				
			||||||
 | 
							</ol>
 | 
				
			||||||
 | 
					</nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										42
									
								
								clippable-svc/templates/video.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								clippable-svc/templates/video.html.tera
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en"><head>
 | 
				
			||||||
 | 
						<meta name="generator" content="Hugo 0.88.1" />
 | 
				
			||||||
 | 
						<meta charset="utf-8">
 | 
				
			||||||
 | 
						<meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
 | 
				
			||||||
 | 
						<link rel="stylesheet" type="text/css" href="/static/css/style.css">
 | 
				
			||||||
 | 
						<title>{{title}}</title>
 | 
				
			||||||
 | 
						<link rel="shortcut icon" type="image/png" href="/static/favicon.png"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{# Required for a better video player #}
 | 
				
			||||||
 | 
						<link href="//vjs.zencdn.net/7.10.2/video-js.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
						<script src="//vjs.zencdn.net/7.10.2/video.min.js"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{# Opengraph tags #}
 | 
				
			||||||
 | 
						<meta property="og:title" content="{{title}}">
 | 
				
			||||||
 | 
						<meta property="og:site_name" content="Clippable">
 | 
				
			||||||
 | 
						<meta property="og:url" content="{{url}}">
 | 
				
			||||||
 | 
						<meta property="og:description" content="{{desc}}">
 | 
				
			||||||
 | 
						<meta property="og:type" content="video.other">
 | 
				
			||||||
 | 
						<meta property="og:image" content="{{clip_thumbnail}}">
 | 
				
			||||||
 | 
						{# Image tag requires a default in case of disk memery #}
 | 
				
			||||||
 | 
						<meta property="og:video" content="{{clip_url}}">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						{% if script %}
 | 
				
			||||||
 | 
							<script src="/static/dist/bundle.js"></script>
 | 
				
			||||||
 | 
						{% endif %}
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					<div id="layout">
 | 
				
			||||||
 | 
						<div class="content">
 | 
				
			||||||
 | 
							{% include "navbar" %}
 | 
				
			||||||
 | 
							<h1>{{title}}</h1>
 | 
				
			||||||
 | 
							<video width="1280" height="720" class="video-js" controls autoplay>
 | 
				
			||||||
 | 
								<source src="{{clip_url}}">
 | 
				
			||||||
 | 
								wtf get a better browser 1d10t
 | 
				
			||||||
 | 
							</video>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										1
									
								
								service/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								service/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1 +0,0 @@
 | 
				
			|||||||
target/
 | 
					 | 
				
			||||||
							
								
								
									
										2487
									
								
								service/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2487
									
								
								service/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,9 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "backend"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
rocket = { version = "0.5.1", features = ["json"] }
 | 
					 | 
				
			||||||
serde = {version = "1.0", features = ["derive"]}
 | 
					 | 
				
			||||||
rocket_dyn_templates = {version = "0.2.0", features = ["tera"] }
 | 
					 | 
				
			||||||
@ -1,33 +0,0 @@
 | 
				
			|||||||
use std::collections::HashMap;
 | 
					 | 
				
			||||||
use std::fs;
 | 
					 | 
				
			||||||
use rocket::serde::json;
 | 
					 | 
				
			||||||
use serde::Deserialize;
 | 
					 | 
				
			||||||
use serde::Serialize;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::CONFIG;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct Row {
 | 
					 | 
				
			||||||
    pub id: i64,
 | 
					 | 
				
			||||||
    pub file: String,
 | 
					 | 
				
			||||||
    pub thumbnail: String
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn get_video(id: i64) -> Option<Row> {
 | 
					 | 
				
			||||||
    // Used to fetch video information from the db for client's to request
 | 
					 | 
				
			||||||
    // video and thumbnail files from us
 | 
					 | 
				
			||||||
    let db_file = fs::read_to_string(CONFIG.db_file)
 | 
					 | 
				
			||||||
        .expect("Unable to read DB_FILE");
 | 
					 | 
				
			||||||
    let db_data: HashMap<i64, Row> = json::from_str(&db_file)
 | 
					 | 
				
			||||||
        .expect("Unable to parse DB_FILE content");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match db_data.get(&id) {
 | 
					 | 
				
			||||||
        Some(video) => Some(Row {
 | 
					 | 
				
			||||||
            id,
 | 
					 | 
				
			||||||
            file: video.file.clone(),
 | 
					 | 
				
			||||||
            thumbnail: video.thumbnail.clone()
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
        None => None
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,27 +0,0 @@
 | 
				
			|||||||
#[macro_use] extern crate rocket;
 | 
					 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					 | 
				
			||||||
use rocket::fs::FileServer;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
mod video;
 | 
					 | 
				
			||||||
mod db;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct BackendConfig {
 | 
					 | 
				
			||||||
    pub root: &'static str,
 | 
					 | 
				
			||||||
    pub db_file: &'static str,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const CONFIG: BackendConfig = BackendConfig {
 | 
					 | 
				
			||||||
    root: "/opt/clippable",
 | 
					 | 
				
			||||||
    db_file: "/opt/clippable/db.json"
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[rocket::main]
 | 
					 | 
				
			||||||
async fn main() {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let _ = rocket::build()
 | 
					 | 
				
			||||||
        .mount("/static", FileServer::from("static"))
 | 
					 | 
				
			||||||
        .mount("/c", routes![video::index])
 | 
					 | 
				
			||||||
        .mount("/video", routes![video::file])
 | 
					 | 
				
			||||||
        .attach(Template::fairing())
 | 
					 | 
				
			||||||
        .launch().await;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,33 +0,0 @@
 | 
				
			|||||||
use crate::db;
 | 
					 | 
				
			||||||
use std::path::PathBuf;
 | 
					 | 
				
			||||||
use rocket::fs::NamedFile;
 | 
					 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					 | 
				
			||||||
use rocket_dyn_templates::context;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/<video_id>")]
 | 
					 | 
				
			||||||
pub fn index(video_id: i64) -> Template {
 | 
					 | 
				
			||||||
    // Read the db file we have to get the ID
 | 
					 | 
				
			||||||
    // Now we can fetch the row from the DB content
 | 
					 | 
				
			||||||
    match db::get_video(video_id) {
 | 
					 | 
				
			||||||
        Some(video) => {
 | 
					 | 
				
			||||||
            let path = PathBuf::from(&video.file);
 | 
					 | 
				
			||||||
            Template::render("index", context! {
 | 
					 | 
				
			||||||
                title: path.file_name(),
 | 
					 | 
				
			||||||
                kind: path.extension(),
 | 
					 | 
				
			||||||
                video: video.file
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        None => Template::render("index", context! {
 | 
					 | 
				
			||||||
            title: "Not found"
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[get("/<path>")]
 | 
					 | 
				
			||||||
pub async fn file(path: PathBuf) -> Option<NamedFile> {
 | 
					 | 
				
			||||||
    if path.is_file() {
 | 
					 | 
				
			||||||
        return NamedFile::open(path).await.ok();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    None
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,8 +0,0 @@
 | 
				
			|||||||
.content {
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    margin: auto;
 | 
					 | 
				
			||||||
    width: 75%;
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ -1,16 +0,0 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					 | 
				
			||||||
<head lang="en-us">
 | 
					 | 
				
			||||||
    <link rel="stylesheet" href="/static/style.css">
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
    <div class="content">
 | 
					 | 
				
			||||||
        <h1 class="title">{{ title }}</h1>
 | 
					 | 
				
			||||||
        {% if video %}
 | 
					 | 
				
			||||||
            <video controls>
 | 
					 | 
				
			||||||
                <source src="/video/{{ video }}" type="{{ kind }}">
 | 
					 | 
				
			||||||
            </video>
 | 
					 | 
				
			||||||
        {% else %}
 | 
					 | 
				
			||||||
            <p>Nothing to see here</p>
 | 
					 | 
				
			||||||
        {% endif %}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user