Features
This commit is contained in:
parent
16bce1d11b
commit
d2755f82c9
9 changed files with 116 additions and 54 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,4 +2,5 @@
|
||||||
**/database
|
**/database
|
||||||
**/dist/
|
**/dist/
|
||||||
**/node_modules
|
**/node_modules
|
||||||
test.*
|
test.*
|
||||||
|
dist.tar.zst
|
9
build.sh
9
build.sh
|
@ -6,6 +6,9 @@ set -euxo pipefail
|
||||||
yarn
|
yarn
|
||||||
trunk build --release
|
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
|
# Build server
|
||||||
cargo build --release --bin omegaupload-server
|
cargo build --release --bin omegaupload-server
|
||||||
|
|
||||||
|
@ -18,4 +21,8 @@ mkdir -p dist/static
|
||||||
find dist -type f -exec mv {} dist/static/ ";"
|
find dist -type f -exec mv {} dist/static/ ";"
|
||||||
|
|
||||||
strip target/release/omegaupload-server
|
strip target/release/omegaupload-server
|
||||||
cp target/release/omegaupload-server dist/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 atty::Stream;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use omegaupload_common::crypto::{gen_key_nonce, open_in_place, seal_in_place, Key};
|
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::blocking::Client;
|
||||||
use reqwest::header::EXPIRES;
|
use reqwest::header::EXPIRES;
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
@ -99,7 +99,12 @@ fn handle_upload(mut url: Url, password: Option<SecretString>) -> Result<()> {
|
||||||
Ok(())
|
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()
|
let res = Client::new()
|
||||||
.get(url.sanitized_url)
|
.get(url.sanitized_url)
|
||||||
.send()
|
.send()
|
||||||
|
@ -109,7 +114,8 @@ fn handle_download(url: ParsedUrl) -> Result<()> {
|
||||||
bail!("Got bad response from server: {}", res.status());
|
bail!("Got bad response from server: {}", res.status());
|
||||||
}
|
}
|
||||||
|
|
||||||
let expiration_text = dbg!(res.headers())
|
let expiration_text = res
|
||||||
|
.headers()
|
||||||
.get(EXPIRES)
|
.get(EXPIRES)
|
||||||
.and_then(|v| Expiration::try_from(v).ok())
|
.and_then(|v| Expiration::try_from(v).ok())
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
|
@ -16,6 +16,8 @@ pub use url::Url;
|
||||||
|
|
||||||
use crate::crypto::{Key, Nonce};
|
use crate::crypto::{Key, Nonce};
|
||||||
|
|
||||||
|
pub const API_ENDPOINT: &str = "/api";
|
||||||
|
|
||||||
pub mod base64 {
|
pub mod base64 {
|
||||||
/// URL-safe Base64 encoding.
|
/// URL-safe Base64 encoding.
|
||||||
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
||||||
|
@ -153,13 +155,13 @@ impl From<&str> for PartialParsedUrl {
|
||||||
for (key, value) in args {
|
for (key, value) in args {
|
||||||
match (key, value) {
|
match (key, value) {
|
||||||
("key", Some(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", _) => {
|
("pw", _) => {
|
||||||
needs_password = true;
|
needs_password = true;
|
||||||
}
|
}
|
||||||
("nonce", Some(value)) => {
|
("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::handler::{get, post};
|
||||||
use axum::http::header::EXPIRES;
|
use axum::http::header::EXPIRES;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::Html;
|
||||||
use axum::{service, AddExtensionLayer, Router};
|
use axum::{service, AddExtensionLayer, Router};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use headers::HeaderMap;
|
use headers::HeaderMap;
|
||||||
use omegaupload_common::Expiration;
|
use omegaupload_common::{Expiration, API_ENDPOINT};
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rocksdb::{ColumnFamilyDescriptor, IteratorMode};
|
use rocksdb::{ColumnFamilyDescriptor, IteratorMode};
|
||||||
|
@ -61,11 +62,11 @@ async fn main() -> Result<()> {
|
||||||
.route("/", post(upload::<SHORT_CODE_SIZE>))
|
.route("/", post(upload::<SHORT_CODE_SIZE>))
|
||||||
.route(
|
.route(
|
||||||
"/:code",
|
"/:code",
|
||||||
get(|| async { include_str!("../../dist/index.html") }),
|
get(|| async { Html(include_str!("../../dist/index.html")) }),
|
||||||
)
|
)
|
||||||
.nest("/static", root_service)
|
.nest("/static", root_service)
|
||||||
.route(
|
.route(
|
||||||
"/api/:code",
|
&format!("{}{}", API_ENDPOINT.to_string(), "/:code"),
|
||||||
get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>),
|
get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>),
|
||||||
)
|
)
|
||||||
.layer(AddExtensionLayer::new(db))
|
.layer(AddExtensionLayer::new(db))
|
||||||
|
|
|
@ -27,6 +27,7 @@ zip = { version = "0.5", default-features = false, features = ["deflate"] }
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
features = [
|
features = [
|
||||||
|
"BlobPropertyBag",
|
||||||
"TextDecoder",
|
"TextDecoder",
|
||||||
"IdbFactory",
|
"IdbFactory",
|
||||||
"IdbOpenDbRequest",
|
"IdbOpenDbRequest",
|
||||||
|
|
|
@ -5,9 +5,9 @@ use std::sync::Arc;
|
||||||
use gloo_console::log;
|
use gloo_console::log;
|
||||||
use js_sys::{Array, Uint8Array};
|
use js_sys::{Array, Uint8Array};
|
||||||
use omegaupload_common::crypto::{open_in_place, Key, Nonce};
|
use omegaupload_common::crypto::{open_in_place, Key, Nonce};
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
use web_sys::Blob;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::{Blob, BlobPropertyBag};
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
pub struct ArchiveMeta {
|
pub struct ArchiveMeta {
|
||||||
|
@ -66,6 +66,9 @@ pub fn decrypt(
|
||||||
if let Ok(decrypted) = std::str::from_utf8(container) {
|
if let Ok(decrypted) = std::str::from_utf8(container) {
|
||||||
Ok(DecryptedData::String(Arc::new(decrypted.to_owned())))
|
Ok(DecryptedData::String(Arc::new(decrypted.to_owned())))
|
||||||
} else {
|
} else {
|
||||||
|
let mime_type = tree_magic_mini::from_u8(container);
|
||||||
|
log!("Mimetype: ", mime_type);
|
||||||
|
|
||||||
log!("Blob conversion started.");
|
log!("Blob conversion started.");
|
||||||
let start = now();
|
let start = now();
|
||||||
let blob_chunks = Array::new_with_length(container.chunks(65536).len().try_into().unwrap());
|
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);
|
array.copy_from(chunk);
|
||||||
blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap());
|
blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap());
|
||||||
}
|
}
|
||||||
let blob =
|
let mut blob_props = BlobPropertyBag::new();
|
||||||
Arc::new(Blob::new_with_u8_array_sequence(blob_chunks.dyn_ref().unwrap()).unwrap());
|
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));
|
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" {
|
if mime_type.starts_with("image/") || mime_type == "application/x-riff" {
|
||||||
Ok(DecryptedData::Image(blob, container.len()))
|
Ok(DecryptedData::Image(blob, container.len()))
|
||||||
} else if mime_type.starts_with("audio/") {
|
} else if mime_type.starts_with("audio/") {
|
||||||
|
@ -93,15 +100,18 @@ pub fn decrypt(
|
||||||
if let Ok(mut zip) = zip::ZipArchive::new(cursor) {
|
if let Ok(mut zip) = zip::ZipArchive::new(cursor) {
|
||||||
for i in 0..zip.len() {
|
for i in 0..zip.len() {
|
||||||
match zip.by_index(i) {
|
match zip.by_index(i) {
|
||||||
Ok(file) => {
|
Ok(file) => entries.push(ArchiveMeta {
|
||||||
entries.push(ArchiveMeta{name: file.name().to_string(), file_size: file.size() as usize})
|
name: file.name().to_string(),
|
||||||
},
|
file_size: file.size() as usize,
|
||||||
Err(err) => {
|
}),
|
||||||
match err {
|
Err(err) => match err {
|
||||||
zip::result::ZipError::UnsupportedArchive(s) => { log!("Unsupported: ", s.to_string()); }
|
zip::result::ZipError::UnsupportedArchive(s) => {
|
||||||
_ => { log!(format!("Error: {}", err)); }
|
log!("Unsupported: ", s.to_string());
|
||||||
}
|
}
|
||||||
}
|
_ => {
|
||||||
|
log!(format!("Error: {}", err));
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,12 @@ main {
|
||||||
font-family: 'Mplus Code', sans-serif;
|
font-family: 'Mplus Code', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-ln td.hljs-ln-numbers {
|
.align-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-ln td.hljs-ln-numbers {
|
||||||
|
@extend .align-right;
|
||||||
padding-right: $padding;
|
padding-right: $padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,4 +77,15 @@ img, audio, video {
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
@extend .hljs;
|
@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");
|
let preEle = document.createElement("pre");
|
||||||
preEle.classList.add("paste");
|
preEle.classList.add("paste");
|
||||||
|
|
||||||
let headerEle = document.createElement("header");
|
let headerEle = document.createElement("p");
|
||||||
headerEle.classList.add("unselectable");
|
headerEle.classList.add("unselectable");
|
||||||
|
headerEle.classList.add("centered");
|
||||||
headerEle.textContent = data.expiration;
|
headerEle.textContent = data.expiration;
|
||||||
preEle.appendChild(headerEle);
|
preEle.appendChild(headerEle);
|
||||||
|
|
||||||
|
@ -141,41 +142,58 @@ function createArchivePasteUi({ expiration, data, entries }) {
|
||||||
bodyEle.textContent = '';
|
bodyEle.textContent = '';
|
||||||
|
|
||||||
let mainEle = document.createElement("main");
|
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");
|
let expirationEle = document.createElement("p");
|
||||||
expirationEle.textContent = expiration;
|
expirationEle.textContent = expiration;
|
||||||
mainEle.appendChild(expirationEle);
|
expirationEle.classList.add("centered");
|
||||||
|
sectionEle.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);
|
|
||||||
|
|
||||||
let downloadEle = document.createElement("a");
|
let downloadEle = document.createElement("a");
|
||||||
downloadEle.href = downloadLink;
|
downloadEle.href = URL.createObjectURL(data);
|
||||||
downloadEle.download = window.location.pathname;
|
downloadEle.download = window.location.pathname;
|
||||||
downloadEle.classList.add("hljs-meta");
|
|
||||||
mainEle.appendChild(downloadEle);
|
|
||||||
|
|
||||||
bodyEle.appendChild(mainEle);
|
|
||||||
|
|
||||||
downloadEle.textContent = "Download";
|
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?) {
|
function createMultiMediaPasteUi(tag, expiration, data, on_create?) {
|
||||||
|
@ -198,6 +216,7 @@ function createMultiMediaPasteUi(tag, expiration, data, on_create?) {
|
||||||
mediaEle.controls = true;
|
mediaEle.controls = true;
|
||||||
mainEle.appendChild(mediaEle);
|
mainEle.appendChild(mediaEle);
|
||||||
|
|
||||||
|
|
||||||
let downloadEle = document.createElement("a");
|
let downloadEle = document.createElement("a");
|
||||||
downloadEle.href = downloadLink;
|
downloadEle.href = downloadLink;
|
||||||
downloadEle.download = window.location.pathname;
|
downloadEle.download = window.location.pathname;
|
||||||
|
|
Loading…
Reference in a new issue