Features
This commit is contained in:
parent
16bce1d11b
commit
d2755f82c9
9 changed files with 116 additions and 54 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@
|
|||
**/dist/
|
||||
**/node_modules
|
||||
test.*
|
||||
dist.tar.zst
|
7
build.sh
7
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
|
||||
|
||||
|
@ -19,3 +22,7 @@ find dist -type f -exec mv {} dist/static/ ";"
|
|||
|
||||
strip target/release/omegaupload-server
|
||||
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
|
|
@ -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<SecretString>) -> 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()
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
|
|
@ -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::<SHORT_CODE_SIZE>))
|
||||
.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::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>),
|
||||
)
|
||||
.layer(AddExtensionLayer::new(db))
|
||||
|
|
|
@ -27,6 +27,7 @@ zip = { version = "0.5", default-features = false, features = ["deflate"] }
|
|||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"BlobPropertyBag",
|
||||
"TextDecoder",
|
||||
"IdbFactory",
|
||||
"IdbOpenDbRequest",
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -74,3 +78,14 @@ img, audio, video {
|
|||
.primary {
|
||||
@extend .hljs;
|
||||
}
|
||||
|
||||
.archive {
|
||||
&-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-file-size {
|
||||
@extend .align-right;
|
||||
padding-left: $padding;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue