diff --git a/cut-be/src/app/controllers/v1/cut.rs b/cut-be/src/app/controllers/v1/cut.rs index bb70413..3a5f5da 100644 --- a/cut-be/src/app/controllers/v1/cut.rs +++ b/cut-be/src/app/controllers/v1/cut.rs @@ -14,7 +14,9 @@ pub async fn get_cut_raw(m: web::Data, req: HttpRequest) -> impl Respond let id: String = req.match_info().query("id").parse().unwrap(); match handlers::cut::get_one(m, id) { Ok(cut) => match cut.variant.as_str() { - constants::VARIANT_SNIPPET => HttpResponse::Ok().body(cut.data), + constants::VARIANT_SNIPPET => HttpResponse::Ok() + .header("Content-Type", "text/plain") + .body(cut.data), constants::VARIANT_URL => HttpResponse::TemporaryRedirect() .header("Location", cut.data) .finish(), @@ -42,7 +44,7 @@ pub async fn get_cut(m: web::Data, req: HttpRequest) -> impl Responder { #[post("")] pub async fn post_snippet_create( m: web::Data, - cut: web::Json, + mut cut: web::Json, req: HttpRequest, ) -> impl Responder { let user: TokenInfo = match handlers::auth::authorize(&m, &req).await { @@ -57,7 +59,11 @@ pub async fn post_snippet_create( constants::VARIANT_URL => (), _ => return HttpResponse::BadRequest().finish(), }; - match handlers::cut::insert(m, user.sub, cut.0) { + if cut.data.trim().chars().count() == 0 { + return HttpResponse::BadRequest().finish(); + }; + cut.0.owner = user.sub; + match handlers::cut::insert(m, cut.0) { Ok(hash) => HttpResponse::Ok().json(CreateResponse { hash: hash }), Err(e) => HttpResponse::InternalServerError().body(format!("{:?}", e)), } diff --git a/cut-be/src/app/datatransfers/cut.rs b/cut-be/src/app/datatransfers/cut.rs index b69edd6..d6106bd 100644 --- a/cut-be/src/app/datatransfers/cut.rs +++ b/cut-be/src/app/datatransfers/cut.rs @@ -1,4 +1,7 @@ +use crate::core::error::{HandlerError, HandlerErrorKind}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Debug, Serialize, Deserialize)] pub struct Cut { @@ -8,8 +11,70 @@ pub struct Cut { pub variant: String, pub metadata: String, pub data: String, - #[serde(skip_deserializing)] + pub expiry: i64, + #[serde(default = "current_time")] pub created_at: u64, + #[serde(default = "Default::default")] + pub views: u64, +} + +fn current_time() -> u64 { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(duration) => duration.as_secs(), + Err(_) => 0, + } +} + +impl Cut { + pub fn from_hashmap(res: HashMap) -> Result { + Ok(Cut { + name: res + .get("name") + .ok_or(HandlerErrorKind::RedisError)? + .to_string(), + owner: res + .get("owner") + .ok_or(HandlerErrorKind::RedisError)? + .to_string(), + variant: res + .get("variant") + .ok_or(HandlerErrorKind::RedisError)? + .to_string(), + metadata: res + .get("metadata") + .ok_or(HandlerErrorKind::RedisError)? + .to_string(), + data: res + .get("data") + .ok_or(HandlerErrorKind::RedisError)? + .to_string(), + expiry: res + .get("expiry") + .ok_or(HandlerErrorKind::RedisError)? + .parse()?, + created_at: res + .get("created_at") + .ok_or(HandlerErrorKind::RedisError)? + .parse()?, + views: res + .get("views") + .ok_or(HandlerErrorKind::RedisError)? + .parse()?, + }) + } + + pub fn to_array(&self) -> [(&str, String); 8] { + return [ + ("name", self.name.clone()), + ("owner", self.owner.clone()), + ("variant", self.variant.clone()), + ("metadata", self.metadata.clone()), + ("data", self.data.clone()), + ("expiry", self.expiry.to_string()), + ("created_at", self.created_at.to_string()), + ("views", self.views.to_string()), + ]; + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/cut-be/src/app/handlers/cut.rs b/cut-be/src/app/handlers/cut.rs index aad82f0..d5ab51c 100644 --- a/cut-be/src/app/handlers/cut.rs +++ b/cut-be/src/app/handlers/cut.rs @@ -3,72 +3,43 @@ use crate::core::error::{HandlerError, HandlerErrorKind}; use crate::utils::hash; use actix_web::web; use r2d2_redis::redis::Commands; -use std::{ - collections::HashMap, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::collections::HashMap; -pub fn get_one(m: web::Data, id: String) -> Result { +pub fn get_one(m: web::Data, hash: String) -> Result { let rd = &mut m.rd_pool.get()?; - match rd.hgetall::>(id) { + let key = format!("cut::{}", hash); + let mut cut: Cut = match rd.hgetall::>(key.clone()) { Ok(res) => { if res.is_empty() { return Err(HandlerErrorKind::CutNotFoundError.into()); } - Ok(Cut { - name: res - .get("name") - .ok_or(HandlerErrorKind::CutNotFoundError)? - .to_string(), - owner: res - .get("owner") - .ok_or(HandlerErrorKind::CutNotFoundError)? - .to_string(), - variant: res - .get("variant") - .ok_or(HandlerErrorKind::CutNotFoundError)? - .to_string(), - metadata: res - .get("metadata") - .ok_or(HandlerErrorKind::CutNotFoundError)? - .to_string(), - data: res - .get("data") - .ok_or(HandlerErrorKind::CutNotFoundError)? - .to_string(), - created_at: res - .get("created_at") - .ok_or(HandlerErrorKind::CutNotFoundError)? - .parse()?, - }) + Cut::from_hashmap(res)? } - Err(e) => Err(e.into()), + Err(e) => return Err(e.into()), + }; + cut.views += 1; + if cut.expiry < 0 { + let _ = rd.del::(key.clone()); + } else { + let _ = rd.hset::(key.clone(), "views", cut.views); } + Ok(cut) } -pub fn insert( - m: web::Data, - user_subject: String, - cut: Cut, -) -> Result { +pub fn insert(m: web::Data, cut: Cut) -> Result { let rd = &mut m.rd_pool.get()?; let hash: String = hash::generate(HASH_LENGTH).into(); - let created_at = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(duration) => duration.as_secs(), - Err(_) => return Err(HandlerErrorKind::GeneralError.into()), - }; - match rd.hset_multiple::( - hash.clone(), - &[ - ("name", cut.name), - ("owner", user_subject), - ("variant", cut.variant), - ("metadata", cut.metadata), - ("data", cut.data), - ("created_at", created_at.to_string()), - ], - ) { - Ok(_) => Ok(hash), + let key = format!("cut::{}", hash.clone()); + match rd.hset_multiple::(key.clone(), &cut.to_array()) { + Ok(_) => { + if cut.expiry < 0 { + return Ok(hash); + }; + match rd.expire::(key.clone(), cut.expiry as usize) { + Ok(_) => Ok(hash), + Err(e) => Err(e.into()), + } + } Err(e) => Err(e.into()), } } diff --git a/cut-fe/src/constants/expiries.ts b/cut-fe/src/constants/expiries.ts new file mode 100644 index 0000000..c278be9 --- /dev/null +++ b/cut-fe/src/constants/expiries.ts @@ -0,0 +1,12 @@ +export const expiries: { + [name: string]: number; +} = { + "Delete after read": -1, + "1 minute": 60, + "10 minutes": 600, + "1 hour": 3600, + "12 hours": 12 * 3600, + "1 day": 24 * 3600, + "2 days": 2 * 24 * 3600, + "1 week": 7 * 24 * 3600 +}; diff --git a/cut-fe/src/constants/index.ts b/cut-fe/src/constants/index.ts new file mode 100644 index 0000000..962ff76 --- /dev/null +++ b/cut-fe/src/constants/index.ts @@ -0,0 +1,5 @@ +import { expiries } from "./expiries"; +import { languages } from "./languages"; +import { STATUS } from "./status"; + +export { expiries, languages, STATUS }; diff --git a/cut-fe/src/router/index.ts b/cut-fe/src/router/index.ts index c29ed03..b47752b 100644 --- a/cut-fe/src/router/index.ts +++ b/cut-fe/src/router/index.ts @@ -32,7 +32,7 @@ const routes: Array = [ beforeEnter: authenticatedOnly, component: Create, meta: { - title: "Create Cut | Cut" + title: "Create | Cut" } }, { @@ -75,6 +75,10 @@ const routes: Array = [ meta: { title: "View Cut | Cut" } + }, + { + path: "*", + redirect: { name: "home" } } ] } diff --git a/cut-fe/src/styles/App.sass b/cut-fe/src/styles/App.sass index f2bc1bb..19992a0 100644 --- a/cut-fe/src/styles/App.sass +++ b/cut-fe/src/styles/App.sass @@ -20,6 +20,9 @@ a.text-link:hover box-shadow: inset 0 -2px 0 var(--v-primary-base) transition: 200ms +h1 + line-height: normal !important + .v-card > .v-card__title padding: 16px 16px 16px 24px !important diff --git a/cut-fe/src/styles/Create.sass b/cut-fe/src/styles/Create.sass index dc8fa7d..89a78dc 100644 --- a/cut-fe/src/styles/Create.sass +++ b/cut-fe/src/styles/Create.sass @@ -5,6 +5,7 @@ font-size: 14px line-height: 1.5 padding: 20px 5px + border: 1px solid #fff4 #view-link, #raw-link font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace diff --git a/cut-fe/src/utils/highlighter.ts b/cut-fe/src/utils/highlighter.ts new file mode 100644 index 0000000..a6224d6 --- /dev/null +++ b/cut-fe/src/utils/highlighter.ts @@ -0,0 +1,17 @@ +import Prism from "prismjs"; +import { languages } from "@/constants"; + +export function highlighter(language: string): Function { + return (code: string): string => { + if (language === "Plaintext") + return code + .replace(/&/g, "&") + .replace(//g, ">"); + return Prism.highlight( + code, + languages[language].grammar, + languages[language].language + ); + }; +} diff --git a/cut-fe/src/views/View.vue b/cut-fe/src/views/View.vue index 987efdb..81b1a87 100644 --- a/cut-fe/src/views/View.vue +++ b/cut-fe/src/views/View.vue @@ -2,19 +2,48 @@
- + -

-
- {{ snippet.name }} -
-
- Redirecting... -
-
- Download File -
+

+ {{ snippet.name }}

+

+ Redirecting... +

+

+ Download File +

+
+ + + + + {{ + Intl.DateTimeFormat("default", { + dateStyle: "full", + timeStyle: "medium" + }).format(createdAt) + }} + + + + + + {{ views }} + + + + + + {{ snippet.language }} + +
@@ -27,7 +56,7 @@ import Vue from "vue"; +import api from "@/apis/api"; import { STATUS } from "@/constants/status"; -import { languages } from "@/constants/languages"; -import "@/styles/Create.sass"; +import { highlighter } from "@/utils/highlighter"; -import Prism from "prismjs"; +import "@/styles/Create.sass"; import "@/styles/prism-atom-dark.css"; -import api from "@/apis/api"; export default Vue.extend({ data() { return { variant: "", + createdAt: new Date(), + views: 0, snippet: { name: "", language: "Plaintext", - languageSelect: languages, data: "" }, url: { target: "" }, - pageLoadStatus: STATUS.IDLE, - linkView: "", - linkRaw: "", - copiedTooltip: { - view: false, - raw: false, - viewTimeout: 0, - rawTimeout: 0 - } + pageLoadStatus: STATUS.LOADING }; }, @@ -130,15 +151,6 @@ export default Vue.extend({ }, methods: { - highlighter(code: string): string { - if (this.snippet.language === "Plaintext") return code; - Prism.highlightAll(); - return Prism.highlight( - code, - languages[this.snippet.language].grammar, - languages[this.snippet.language].language - ); - }, getCut(hash: string) { this.pageLoadStatus = STATUS.LOADING; api.cut @@ -147,6 +159,8 @@ export default Vue.extend({ const data = response.data; const metadata = JSON.parse(data.metadata); this.variant = data.variant; + this.views = data.views; + this.createdAt = new Date(data.created_at * 1000); switch (response.data.variant) { case "snippet": this.snippet.name = data.name; @@ -165,9 +179,9 @@ export default Vue.extend({ }) .catch(err => { this.pageLoadStatus = STATUS.ERROR; - console.error(err); }); - } + }, + highlighter: (language: string) => highlighter(language) } }); diff --git a/cut-fe/src/views/manage/Create.vue b/cut-fe/src/views/manage/Create.vue index bd8e41d..a27f691 100644 --- a/cut-fe/src/views/manage/Create.vue +++ b/cut-fe/src/views/manage/Create.vue @@ -3,7 +3,7 @@

- Create Cut + Create

@@ -32,6 +32,7 @@ v-model.trim="snippet.name" label="Name" outlined + dense maxlength="50" background-color="#2d2d2d" hide-details="auto" @@ -47,6 +48,7 @@ :items="Object.keys(snippet.languageSelect)" label="Language" outlined + dense background-color="#2d2d2d" hide-details="auto" style="border: none !important" @@ -55,12 +57,22 @@ />
- + +
+ + Snippet may not be empty! + +
+
+
+ +
@@ -101,16 +113,35 @@
- - Create - + + + + + + + Cut + + + @@ -130,11 +161,12 @@ - + Confirm @@ -192,24 +224,26 @@