Compare commits
4 commits
f42924cd3b
...
d1d12bdfb5
Author | SHA1 | Date | |
---|---|---|---|
d1d12bdfb5 | |||
88aa3093dd | |||
4d1c5b69c1 | |||
256aca663c |
4 changed files with 91 additions and 39 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
/target
|
||||
/upload-temp
|
||||
/test
|
||||
/project_archives
|
||||
/project-archives
|
||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -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);
|
||||
|
|
|
@ -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(()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<()>
|
||||
|
|
Loading…
Reference in a new issue