This commit is contained in:
Julian 2023-03-04 19:29:56 -05:00
commit 92e2dd410f
Signed by: NotNite
GPG key ID: BD91A5402CCEB08A
19 changed files with 1935 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
target/
files/
.env
u.db*

1554
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

2
Cargo.toml Normal file
View file

@ -0,0 +1,2 @@
[workspace]
members = ["client", "server"]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 NotNite
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# u
blazingly-fast super-concurrent webscale file server

7
client/Cargo.lock generated Normal file
View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "client"
version = "0.1.0"

8
client/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

3
client/src/main.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

19
server/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.69"
async-trait = "0.1.65"
axum = { version = "0.6.10", features = ["multipart"] }
dotenvy = "0.15.6"
hyper = "0.14.24"
nanoid = "0.4.0"
serde = { version = "1.0.152", features = ["derive"] }
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
tokio = { version = "1.26.0", features = ["full"] }
tokio-stream = { version = "0.1.12", features = ["net"] }
tokio-util = { version = "0.7.7", features = ["io"] }

View file

@ -0,0 +1,9 @@
create table api_keys (
key varchar(64) primary key not null,
admin boolean not null default false
);
create table files (
id varchar(8) primary key not null,
revocation_key varchar(128) not null
);

37
server/src/auth.rs Normal file
View file

@ -0,0 +1,37 @@
use crate::state::ArcState;
use async_trait::async_trait;
use axum::{extract::FromRequestParts, http::request::Parts};
use hyper::StatusCode;
pub struct AuthState {
pub api_key: String,
pub admin: bool,
}
pub struct Auth(pub AuthState);
#[async_trait]
impl FromRequestParts<ArcState> for Auth {
type Rejection = StatusCode;
async fn from_request_parts(
parts: &mut Parts,
state: &ArcState,
) -> Result<Self, Self::Rejection> {
let header = parts
.headers
.get("Authorization")
.ok_or(StatusCode::UNAUTHORIZED)?;
let header = header.to_str().unwrap();
let row = sqlx::query!("select admin from api_keys where key = $1", header)
.fetch_one(&state.db)
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
return Ok(Auth(AuthState {
api_key: header.to_string(),
admin: row.admin,
}));
}
}

58
server/src/main.rs Normal file
View file

@ -0,0 +1,58 @@
use axum::{
routing::{delete, get, post},
Router, Server,
};
use sqlx::SqlitePool;
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
use tokio::net::TcpListener;
use tokio_stream::wrappers::TcpListenerStream;
mod auth;
mod routes;
mod state;
mod utils;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
println!("connecting to database");
let pool = SqlitePool::connect("u.db").await?;
println!("running migrations");
sqlx::migrate!().run(&pool).await?;
utils::check_admin_key(&pool).await?;
let port = std::env::var("U_PORT").unwrap_or_else(|_| "8075".to_string());
let files_dir = std::env::var("U_FILES_DIR").unwrap_or_else(|_| "files".to_string());
let files_dir = PathBuf::from(files_dir);
if !files_dir.exists() {
std::fs::create_dir(&files_dir)?;
}
let state = Arc::new(state::State {
db: pool,
files_dir,
});
let router = Router::default()
.route("/i/:id", get(routes::i))
.route("/i/:id", delete(routes::delete))
.route("/api/upload", post(routes::upload))
.route("/api/new_key", post(routes::new_key))
.with_state(Arc::clone(&state));
let addr: SocketAddr = format!("0.0.0.0:{}", port).parse()?;
println!("listening on {}", addr);
let listener = TcpListener::bind(addr).await?;
let stream = TcpListenerStream::new(listener);
let acceptor = hyper::server::accept::from_stream(stream);
Server::builder(acceptor)
.serve(router.into_make_service())
.await?;
Ok(())
}

View file

@ -0,0 +1,48 @@
use crate::state::ReqState;
use axum::{
body::Empty,
extract::{Path, Query, State},
response::IntoResponse,
};
use hyper::StatusCode;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct UploadQuery {
revocation_key: String,
}
pub async fn delete(
State(state): ReqState,
Query(query): Query<UploadQuery>,
Path(id): Path<String>,
) -> impl IntoResponse {
if id.len() < 8 {
return Err(StatusCode::NOT_FOUND);
}
let id = id[..8].to_string();
let row = sqlx::query!("select revocation_key from files where id = $1", id)
.fetch_one(&state.db)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
if row.revocation_key != query.revocation_key {
return Err(StatusCode::UNAUTHORIZED);
}
let path = state.files_dir.join(id.clone());
if path.exists() {
tokio::fs::remove_file(path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
sqlx::query!("delete from files where id = $1", id)
.execute(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok((StatusCode::NO_CONTENT, Empty::new()))
}

29
server/src/routes/i.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::state::ReqState;
use axum::{
body::StreamBody,
extract::{Path, State},
response::IntoResponse,
};
use hyper::StatusCode;
use tokio_util::io::ReaderStream;
pub async fn get(State(state): ReqState, Path(id): Path<String>) -> impl IntoResponse {
if id.len() < 8 {
return Err(StatusCode::NOT_FOUND);
}
let id = id[..8].to_string();
let path = state.files_dir.join(id.clone());
if path.exists() {
let file = tokio::fs::File::open(path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let stream = ReaderStream::new(file);
let body = StreamBody::new(stream);
Ok(body)
} else {
Err(StatusCode::NOT_FOUND)
}
}

9
server/src/routes/mod.rs Normal file
View file

@ -0,0 +1,9 @@
mod delete;
mod i;
mod new_key;
mod upload;
pub use delete::delete;
pub use i::get as i;
pub use new_key::post as new_key;
pub use upload::post as upload;

View file

@ -0,0 +1,18 @@
use crate::{auth::Auth, state::ReqState};
use axum::{extract::State, response::IntoResponse};
use hyper::StatusCode;
pub async fn post(State(state): ReqState, Auth(auth): Auth) -> impl IntoResponse {
if !auth.admin {
return Err(StatusCode::UNAUTHORIZED);
}
let key = crate::utils::gen_key();
sqlx::query!("insert into api_keys (key, admin) values ($1, false)", key)
.execute(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok((StatusCode::CREATED, key))
}

View file

@ -0,0 +1,59 @@
use crate::{auth::Auth, state::ReqState};
use axum::{
extract::{Multipart, State},
response::IntoResponse,
Json,
};
use hyper::StatusCode;
use serde::Serialize;
#[derive(Serialize)]
struct UploadResponse {
id: String,
revocation_key: String,
}
pub async fn post(
State(state): ReqState,
Auth(_): Auth,
mut multipart: Multipart,
) -> impl IntoResponse {
let mut file = None;
while let Some(field) = multipart.next_field().await.unwrap() {
file = Some(
field
.bytes()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
);
}
if file.is_none() {
return Err(StatusCode::BAD_REQUEST);
}
let file = file.unwrap();
let mut id = crate::utils::gen_filename();
while state.files_dir.join(&id).exists() {
id = crate::utils::gen_filename();
}
let path = state.files_dir.join(id.clone());
tokio::fs::write(path, file)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let revocation_key = crate::utils::gen_revocation_key();
sqlx::query!(
"insert into files (id, revocation_key) values ($1, $2)",
id,
revocation_key
)
.execute(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(UploadResponse { id, revocation_key }))
}

9
server/src/state.rs Normal file
View file

@ -0,0 +1,9 @@
use std::sync::Arc;
pub type ArcState = Arc<State>;
pub type ReqState = axum::extract::State<ArcState>;
pub struct State {
pub db: sqlx::SqlitePool,
pub files_dir: std::path::PathBuf,
}

37
server/src/utils.rs Normal file
View file

@ -0,0 +1,37 @@
use sqlx::SqlitePool;
const API_KEY_ALPHABET: &str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const FILENAME_ALPHABET: &str = "0123456789abcdefghijklmnopqrstuvwxyz";
pub fn gen_filename() -> String {
let alphabet = &FILENAME_ALPHABET.chars().collect::<Vec<_>>()[..];
nanoid::nanoid!(8, alphabet)
}
pub fn gen_key() -> String {
let alphabet = &API_KEY_ALPHABET.chars().collect::<Vec<_>>()[..];
nanoid::nanoid!(64, alphabet)
}
pub fn gen_revocation_key() -> String {
let alphabet = &API_KEY_ALPHABET.chars().collect::<Vec<_>>()[..];
nanoid::nanoid!(128, alphabet)
}
pub async fn check_admin_key(pool: &SqlitePool) -> anyhow::Result<()> {
let query = sqlx::query!("select admin from api_keys where admin = true")
.fetch_all(pool)
.await?;
if query.is_empty() {
let key = gen_key();
sqlx::query!("insert into api_keys (key, admin) values ($1, true)", key)
.execute(pool)
.await?;
println!("created admin key: {}", key);
}
Ok(())
}