Compare commits
2 Commits
9671be1ff3
...
61b3a5fe80
Author | SHA1 | Date | |
---|---|---|---|
61b3a5fe80 | |||
ac200c0c0d |
3
clippable-svc/.vscode/settings.json
vendored
3
clippable-svc/.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"rust.all_features": true
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
def Settings(**kwargs):
|
|
||||||
return {
|
|
||||||
'ls': {
|
|
||||||
'cargo': {
|
|
||||||
'features': ['admin'],
|
|
||||||
'noDefaultFeatures': True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
2112
clippable-svc/Cargo.lock
generated
2112
clippable-svc/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,24 +0,0 @@
|
|||||||
[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"]
|
|
@ -1,51 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
#[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())
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
#[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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
//#![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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
/// 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()
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
#[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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:bc7637a1b3f7e75322e3dddb3499931dc9cd57804afbf46c7941e6bc5211d03c
|
|
||||||
size 21076
|
|
@ -1,100 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0cfdedb04d45802c1c008c9610bda2df8e3482f195bd254c11ebe07205e2bd5d
|
|
||||||
size 8560
|
|
@ -1,57 +0,0 @@
|
|||||||
<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>
|
|
@ -1,84 +0,0 @@
|
|||||||
<!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>
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,32 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
|||||||
<!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
Normal file
1
service/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target/
|
2487
service/Cargo.lock
generated
Normal file
2487
service/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
service/Cargo.toml
Normal file
9
service/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[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"] }
|
33
service/src/db.rs
Normal file
33
service/src/db.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
27
service/src/main.rs
Normal file
27
service/src/main.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#[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;
|
||||||
|
}
|
33
service/src/video.rs
Normal file
33
service/src/video.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
}
|
8
service/static/style.css
Normal file
8
service/static/style.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.content {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: auto;
|
||||||
|
width: 75%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
16
service/templates/index.html.tera
Normal file
16
service/templates/index.html.tera
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!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