diff --git a/.gitignore b/.gitignore index 87d710a..1ce282b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ **/database **/dist/ **/node_modules -test.* \ No newline at end of file +test.* +dist.tar.zst \ No newline at end of file diff --git a/build.sh b/build.sh index 120b653..0699245 100755 --- a/build.sh +++ b/build.sh @@ -6,6 +6,9 @@ set -euxo pipefail yarn trunk build --release +sed -i 's#/index#/static/index#g' dist/index.html +sed -i 's#stylesheet" href="/main#stylesheet" href="/static/main#g' dist/index.html + # Build server cargo build --release --bin omegaupload-server @@ -18,4 +21,8 @@ mkdir -p dist/static find dist -type f -exec mv {} dist/static/ ";" strip target/release/omegaupload-server -cp target/release/omegaupload-server dist/omegaupload-server \ No newline at end of file +cp target/release/omegaupload-server dist/omegaupload-server + +tar -cvf dist.tar dist +rm -r dist.tar.zst +zstd -T0 --ultra --rm -22 dist.tar \ No newline at end of file diff --git a/cli/src/main.rs b/cli/src/main.rs index 1aca838..3a08fab 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, bail, Context, Result}; use atty::Stream; use clap::Parser; use omegaupload_common::crypto::{gen_key_nonce, open_in_place, seal_in_place, Key}; -use omegaupload_common::{base64, hash, Expiration, ParsedUrl, Url}; +use omegaupload_common::{base64, hash, Expiration, ParsedUrl, Url, API_ENDPOINT}; use reqwest::blocking::Client; use reqwest::header::EXPIRES; use reqwest::StatusCode; @@ -99,7 +99,12 @@ fn handle_upload(mut url: Url, password: Option) -> Result<()> { Ok(()) } -fn handle_download(url: ParsedUrl) -> Result<()> { +fn handle_download(mut url: ParsedUrl) -> Result<()> { + url.sanitized_url.set_path(&dbg!(format!( + "{}{}", + API_ENDPOINT, + url.sanitized_url.path() + ))); let res = Client::new() .get(url.sanitized_url) .send() @@ -109,7 +114,8 @@ fn handle_download(url: ParsedUrl) -> Result<()> { bail!("Got bad response from server: {}", res.status()); } - let expiration_text = dbg!(res.headers()) + let expiration_text = res + .headers() .get(EXPIRES) .and_then(|v| Expiration::try_from(v).ok()) .as_ref() diff --git a/common/src/lib.rs b/common/src/lib.rs index 90d8cef..965b956 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -16,6 +16,8 @@ pub use url::Url; use crate::crypto::{Key, Nonce}; +pub const API_ENDPOINT: &str = "/api"; + pub mod base64 { /// URL-safe Base64 encoding. pub fn encode(input: impl AsRef<[u8]>) -> String { @@ -153,13 +155,13 @@ impl From<&str> for PartialParsedUrl { for (key, value) in args { match (key, value) { ("key", Some(value)) => { - decryption_key = dbg!(base64::decode(value).map(|k| *Key::from_slice(&k)).ok()); + decryption_key = base64::decode(value).map(|k| *Key::from_slice(&k)).ok(); } ("pw", _) => { needs_password = true; } ("nonce", Some(value)) => { - nonce = dbg!(base64::decode(value).as_deref().map(Nonce::from_slice).ok()); + nonce = base64::decode(value).as_deref().map(Nonce::from_slice).ok(); } _ => (), } diff --git a/server/src/main.rs b/server/src/main.rs index b4a9584..c6552e9 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -10,10 +10,11 @@ use axum::extract::{Extension, Path, TypedHeader}; use axum::handler::{get, post}; use axum::http::header::EXPIRES; use axum::http::StatusCode; +use axum::response::Html; use axum::{service, AddExtensionLayer, Router}; use chrono::Utc; use headers::HeaderMap; -use omegaupload_common::Expiration; +use omegaupload_common::{Expiration, API_ENDPOINT}; use rand::thread_rng; use rand::Rng; use rocksdb::{ColumnFamilyDescriptor, IteratorMode}; @@ -61,11 +62,11 @@ async fn main() -> Result<()> { .route("/", post(upload::)) .route( "/:code", - get(|| async { include_str!("../../dist/index.html") }), + get(|| async { Html(include_str!("../../dist/index.html")) }), ) .nest("/static", root_service) .route( - "/api/:code", + &format!("{}{}", API_ENDPOINT.to_string(), "/:code"), get(paste::).delete(delete::), ) .layer(AddExtensionLayer::new(db)) diff --git a/web/Cargo.toml b/web/Cargo.toml index c81a65f..1e006e8 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -27,6 +27,7 @@ zip = { version = "0.5", default-features = false, features = ["deflate"] } [dependencies.web-sys] version = "0.3" features = [ + "BlobPropertyBag", "TextDecoder", "IdbFactory", "IdbOpenDbRequest", diff --git a/web/src/decrypt.rs b/web/src/decrypt.rs index 50921b8..8df3590 100644 --- a/web/src/decrypt.rs +++ b/web/src/decrypt.rs @@ -5,9 +5,9 @@ use std::sync::Arc; use gloo_console::log; use js_sys::{Array, Uint8Array}; use omegaupload_common::crypto::{open_in_place, Key, Nonce}; -use wasm_bindgen::JsCast; -use web_sys::Blob; use serde::Serialize; +use wasm_bindgen::JsCast; +use web_sys::{Blob, BlobPropertyBag}; #[derive(Clone, Serialize)] pub struct ArchiveMeta { @@ -66,6 +66,9 @@ pub fn decrypt( if let Ok(decrypted) = std::str::from_utf8(container) { Ok(DecryptedData::String(Arc::new(decrypted.to_owned()))) } else { + let mime_type = tree_magic_mini::from_u8(container); + log!("Mimetype: ", mime_type); + log!("Blob conversion started."); let start = now(); let blob_chunks = Array::new_with_length(container.chunks(65536).len().try_into().unwrap()); @@ -74,13 +77,17 @@ pub fn decrypt( array.copy_from(chunk); blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap()); } - let blob = - Arc::new(Blob::new_with_u8_array_sequence(blob_chunks.dyn_ref().unwrap()).unwrap()); + let mut blob_props = BlobPropertyBag::new(); + blob_props.type_(mime_type); + let blob = Arc::new( + Blob::new_with_u8_array_sequence_and_options( + blob_chunks.dyn_ref().unwrap(), + &blob_props, + ) + .unwrap(), + ); log!(format!("Blob conversion completed in {}ms", now() - start)); - let mime_type = tree_magic_mini::from_u8(container); - log!("Mimetype: ", mime_type); - if mime_type.starts_with("image/") || mime_type == "application/x-riff" { Ok(DecryptedData::Image(blob, container.len())) } else if mime_type.starts_with("audio/") { @@ -93,15 +100,18 @@ pub fn decrypt( if let Ok(mut zip) = zip::ZipArchive::new(cursor) { for i in 0..zip.len() { match zip.by_index(i) { - Ok(file) => { - entries.push(ArchiveMeta{name: file.name().to_string(), file_size: file.size() as usize}) - }, - Err(err) => { - match err { - zip::result::ZipError::UnsupportedArchive(s) => { log!("Unsupported: ", s.to_string()); } - _ => { log!(format!("Error: {}", err)); } + Ok(file) => entries.push(ArchiveMeta { + name: file.name().to_string(), + file_size: file.size() as usize, + }), + Err(err) => match err { + zip::result::ZipError::UnsupportedArchive(s) => { + log!("Unsupported: ", s.to_string()); } - } + _ => { + log!(format!("Error: {}", err)); + } + }, } } } diff --git a/web/src/main.scss b/web/src/main.scss index c6dcb65..4c33c3d 100644 --- a/web/src/main.scss +++ b/web/src/main.scss @@ -40,8 +40,12 @@ main { font-family: 'Mplus Code', sans-serif; } -.hljs-ln td.hljs-ln-numbers { +.align-right { text-align: right; +} + +.hljs-ln td.hljs-ln-numbers { + @extend .align-right; padding-right: $padding; } @@ -73,4 +77,15 @@ img, audio, video { .primary { @extend .hljs; +} + +.archive { + &-table { + width: 100%; + } + + &-file-size { + @extend .align-right; + padding-left: $padding; + } } \ No newline at end of file diff --git a/web/src/main.ts b/web/src/main.ts index a8efe9d..127ade0 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -62,8 +62,9 @@ function createStringPasteUi(data) { let preEle = document.createElement("pre"); preEle.classList.add("paste"); - let headerEle = document.createElement("header"); + let headerEle = document.createElement("p"); headerEle.classList.add("unselectable"); + headerEle.classList.add("centered"); headerEle.textContent = data.expiration; preEle.appendChild(headerEle); @@ -141,41 +142,58 @@ function createArchivePasteUi({ expiration, data, entries }) { bodyEle.textContent = ''; let mainEle = document.createElement("main"); - mainEle.classList.add("hljs"); - mainEle.classList.add("centered"); - mainEle.classList.add("fullscreen"); - const downloadLink = URL.createObjectURL(data); + let sectionEle = document.createElement("section"); + sectionEle.classList.add("paste"); let expirationEle = document.createElement("p"); expirationEle.textContent = expiration; - mainEle.appendChild(expirationEle); - - let mediaEle = document.createElement("table"); - mediaEle.style.width = "50%"; - const tr = mediaEle.insertRow(); - const tdName = tr.insertCell(); - tdName.appendChild(document.createTextNode("Name")); - const tdSize = tr.insertCell(); - tdSize.appendChild(document.createTextNode("File Size")); - for (const entry of entries) { - const tr = mediaEle.insertRow(); - const tdName = tr.insertCell(); - tdName.appendChild(document.createTextNode(entry.name)); - const tdSize = tr.insertCell(); - tdSize.appendChild(document.createTextNode(entry.file_size)); - } - mainEle.appendChild(mediaEle); + expirationEle.classList.add("centered"); + sectionEle.appendChild(expirationEle); let downloadEle = document.createElement("a"); - downloadEle.href = downloadLink; + downloadEle.href = URL.createObjectURL(data); downloadEle.download = window.location.pathname; - downloadEle.classList.add("hljs-meta"); - mainEle.appendChild(downloadEle); - - bodyEle.appendChild(mainEle); - downloadEle.textContent = "Download"; + downloadEle.classList.add("hljs-meta"); + downloadEle.classList.add("centered"); + sectionEle.appendChild(downloadEle); + + sectionEle.appendChild(document.createElement("hr")); + + let mediaEle = document.createElement("table"); + mediaEle.classList.add("archive-table"); + const tr = mediaEle.insertRow(); + tr.classList.add("hljs-title"); + const tdName = tr.insertCell(); + tdName.textContent = "Name"; + const tdSize = tr.insertCell(); + tdSize.classList.add("align-right"); + tdSize.textContent = "File Size"; + + // Because it's a stable sort, we can first sort by name (to get all folder + // items grouped together) and then sort by if there's a / or not. + entries.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + + entries.sort((a, b) => { + return b.name.includes("/") - a.name.includes("/"); + }); + + for (const { name, file_size } of entries) { + const tr = mediaEle.insertRow(); + const tdName = tr.insertCell(); + tdName.textContent = name; + const tdSize = tr.insertCell(); + tdSize.textContent = file_size; + tdSize.classList.add("align-right"); + tdSize.classList.add("hljs-number"); + } + + sectionEle.appendChild(mediaEle); + mainEle.appendChild(sectionEle); + bodyEle.appendChild(mainEle); } function createMultiMediaPasteUi(tag, expiration, data, on_create?) { @@ -198,6 +216,7 @@ function createMultiMediaPasteUi(tag, expiration, data, on_create?) { mediaEle.controls = true; mainEle.appendChild(mediaEle); + let downloadEle = document.createElement("a"); downloadEle.href = downloadLink; downloadEle.download = window.location.pathname;