Compare commits

...

4 commits

Author SHA1 Message Date
d1d12bdfb5
chore: Cleanup clippy warnings 2022-07-05 11:41:08 +01:00
88aa3093dd
chore: Run cargo fmt 2022-07-05 11:37:37 +01:00
4d1c5b69c1
chore: Update gitignore 2022-07-05 11:37:12 +01:00
256aca663c
feat: Serve index.html at root 2022-07-05 11:36:57 +01:00
4 changed files with 91 additions and 39 deletions

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
/target
/upload-temp
/test
/project_archives
/project-archives

View file

@ -1,7 +1,7 @@
use std::{path::Path, fs};
use std::{fs, path::Path};
use axum::Extension;
use color_eyre::{Result, eyre::eyre};
use color_eyre::{eyre::eyre, Result};
use projects::AuthToken;
use tracing_subscriber::prelude::*;
@ -19,8 +19,8 @@ async fn main() -> Result<()> {
.with(tracing_subscriber::fmt::layer())
.init();
let upload_token = std::env::var("PROJECTOR_TOKEN")
.map_err(|_| eyre!("please set a PROJECTOR_TOKEN"))?;
let upload_token =
std::env::var("PROJECTOR_TOKEN").map_err(|_| eyre!("please set a PROJECTOR_TOKEN"))?;
let tempdir = Path::new("upload-temp");
if !tempdir.exists() {
@ -32,9 +32,8 @@ async fn main() -> Result<()> {
fs::create_dir_all(archive_dir)?;
}
let app = routes::router()
.layer(Extension(AuthToken(upload_token)));
let app = routes::router().layer(Extension(AuthToken(upload_token)));
let port: u16 = std::env::var("PORT")
.map(|s| s.parse().unwrap())
.unwrap_or(3000);

View file

@ -1,18 +1,25 @@
use axum::{extract::FromRequest, http::StatusCode, Extension, async_trait};
use axum::{async_trait, extract::FromRequest, http::StatusCode, Extension};
#[derive(Clone)]
pub struct AuthToken(pub String);
pub struct Authed(());
#[async_trait]
impl<B> FromRequest<B> for Authed where B: Send {
impl<B> FromRequest<B> for Authed
where
B: Send,
{
type Rejection = StatusCode;
async fn from_request(req: &mut axum::extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
let Extension(token) = Extension::<AuthToken>::from_request(req).await.expect("missing auth token extension");
async fn from_request(
req: &mut axum::extract::RequestParts<B>,
) -> Result<Self, Self::Rejection> {
let Extension(token) = Extension::<AuthToken>::from_request(req)
.await
.expect("missing auth token extension");
if let Some(auth) = req.headers().get("authorization") {
if let Ok(auth) = auth.to_str() {
if auth == &token.0 {
if auth == token.0 {
return Ok(Self(()));
}
}

View file

@ -1,41 +1,80 @@
use std::{io::{self, Read}, path::PathBuf, path, fs::File};
use std::{
fs::File,
io::{self, Read},
path,
path::PathBuf,
};
use axum::{http::{StatusCode, HeaderMap, HeaderValue}, extract::{Multipart, Path}, body::Bytes, BoxError, Router, routing::{post, get}};
use axum::{
body::Bytes,
extract::{Multipart, Path},
http::{HeaderMap, HeaderValue, StatusCode},
response::Redirect,
routing::{get, post},
BoxError, Router,
};
use futures::{Stream, TryStreamExt};
use tokio::io::{BufWriter, AsyncWriteExt};
use tokio::io::{AsyncWriteExt, BufWriter};
use tokio_util::io::StreamReader;
use zip::{ZipArchive, result::ZipError};
use zip::{result::ZipError, ZipArchive};
use crate::projects::Authed;
pub fn router() -> Router {
Router::new()
.route("/:project", post(upload_project))
.route("/:project", get(project_redirect))
.route("/:project/*path", get(get_asset))
}
async fn get_asset(Path((project, filename)): Path<(String, String)>) -> Result<(HeaderMap, Vec<u8>), StatusCode> {
let path = path::Path::new("project-archives")
.join(format!("{project}.zip"));
async fn project_redirect(Path(path): Path<String>) -> Redirect {
Redirect::to(&format!("/{}/", path))
}
async fn get_asset(
Path((project, filename)): Path<(String, String)>,
) -> Result<(HeaderMap, Vec<u8>), StatusCode> {
let path = path::Path::new("project-archives").join(format!("{project}.zip"));
if path.exists() {
let data = {
let asset = {
let filename = filename.clone();
move || {
let zip = ZipArchive::new(File::open(path)?)?;
let asset = find_asset(zip, &filename.clone())?;
let mut zip = ZipArchive::new(File::open(path)?)?;
let asset = find_asset(&mut zip, &filename)?;
let asset = if let Some(asset) = asset {
Some((
mime_guess::from_path(filename).first_or_octet_stream(),
asset,
))
} else if filename.ends_with('/') {
let changed_name = format!("{filename}index.html");
find_asset(&mut zip, &changed_name)?.map(|data| {
(
mime_guess::from_path(changed_name).first_or_octet_stream(),
data,
)
})
} else {
None
};
Ok::<_, color_eyre::Report>(asset)
}
};
let data = tokio::task::spawn_blocking(data).await
let asset = tokio::task::spawn_blocking(asset)
.await
.unwrap()
.map_err(|e| {
tracing::error!(%e, "failed to find asset in zip");
StatusCode::INTERNAL_SERVER_ERROR
})?;
if let Some(data) = data {
let mime = mime_guess::from_path(filename).first_or_octet_stream();
if let Some((mime, data)) = asset {
let mut headers = HeaderMap::new();
headers.append("content-type", HeaderValue::from_str(mime.essence_str()).unwrap());
headers.append(
"content-type",
HeaderValue::from_str(mime.essence_str()).unwrap(),
);
Ok((headers, data))
} else {
Err(StatusCode::NOT_FOUND)
@ -45,12 +84,11 @@ async fn get_asset(Path((project, filename)): Path<(String, String)>) -> Result<
}
}
fn find_asset(mut zip: ZipArchive<impl std::io::Read + std::io::Seek>, filename: &str) -> color_eyre::Result<Option<Vec<u8>>> {
let filename = if filename.starts_with("/") {
&filename[1..]
} else {
filename
};
fn find_asset(
zip: &mut ZipArchive<impl std::io::Read + std::io::Seek>,
filename: &str,
) -> color_eyre::Result<Option<Vec<u8>>> {
let filename = filename.strip_prefix('/').unwrap_or(filename);
let entry = zip.by_name(filename);
match entry {
Ok(mut entry) => {
@ -67,18 +105,26 @@ fn find_asset(mut zip: ZipArchive<impl std::io::Read + std::io::Seek>, filename:
}
}
async fn upload_project(mut multipart: Multipart, _auth: Authed, Path(project): Path<String>) -> Result<(), StatusCode> {
while let Some(field) = multipart.next_field().await.map_err(|_| StatusCode::BAD_REQUEST)? {
let path = path::Path::new("project-archives")
.join(format!("{project}.zip"));
return if let Err(e) = stream_to_file(path, field).await {
async fn upload_project(
mut multipart: Multipart,
_auth: Authed,
Path(project): Path<String>,
) -> Result<(), StatusCode> {
if let Some(field) = multipart
.next_field()
.await
.map_err(|_| StatusCode::BAD_REQUEST)?
{
let path = path::Path::new("project-archives").join(format!("{project}.zip"));
if let Err(e) = stream_to_file(path, field).await {
tracing::error!(%e, "failed to upload project archive");
Err(StatusCode::INTERNAL_SERVER_ERROR)
} else {
Ok(())
}
} else {
Err(StatusCode::BAD_REQUEST)
}
Err(StatusCode::BAD_REQUEST)
}
async fn stream_to_file<S, E>(target: PathBuf, stream: S) -> color_eyre::Result<()>