working proof of concept
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,3 +8,8 @@ target/
|
|||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
|
|
||||||
|
# Added by cargo
|
||||||
|
|
||||||
|
/target
|
||||||
|
|||||||
2291
Cargo.lock
generated
Normal file
2291
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "minecraft_schematics_web"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-files = "0.6.5"
|
||||||
|
actix-multipart = "0.6.1"
|
||||||
|
actix-web = "4.4.1"
|
||||||
|
clap = { version = "4.4.18", features = ["env", "derive"] }
|
||||||
|
dotenv = { version = "0.15.0", features = ["clap"] }
|
||||||
|
futures-util = "0.3.30"
|
||||||
|
serde = { version = "1.0.196", features = ["derive"] }
|
||||||
|
tera = "1.19.1"
|
||||||
|
thiserror = "1.0.56"
|
||||||
|
tokio = { version = "1.35.1", features = ["full"] }
|
||||||
113
src/handlers.rs
Normal file
113
src/handlers.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
use std::{io::{self, Read}, path::PathBuf};
|
||||||
|
|
||||||
|
use crate::{public, Args};
|
||||||
|
use actix_files::NamedFile;
|
||||||
|
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
|
||||||
|
use actix_web::{http::header, web::Data, HttpRequest, HttpResponse};
|
||||||
|
use serde::Serialize;
|
||||||
|
use tera::{Context, Tera};
|
||||||
|
|
||||||
|
pub async fn robots_txt() -> HttpResponse {
|
||||||
|
HttpResponse::Ok().body(public::ROBOTS)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn favicon_ico() -> HttpResponse {
|
||||||
|
HttpResponse::Ok().body(public::FAVICON)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SchematicLink {
|
||||||
|
filename: String,
|
||||||
|
href: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn index(args: Data<Args>, tera: Data<Tera>) -> HttpResponse {
|
||||||
|
let schem_dir = match std::fs::read_dir(&args.schem_dir_path) {
|
||||||
|
Ok(dir) => dir,
|
||||||
|
Err(err) => return HttpResponse::InternalServerError().body(err.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut schematic_links = Vec::new();
|
||||||
|
|
||||||
|
for dir_entry in schem_dir {
|
||||||
|
let entry = match dir_entry {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(err) => return HttpResponse::InternalServerError().body(err.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if entry.path().is_file() {
|
||||||
|
let filename = entry.file_name().to_string_lossy().to_string();
|
||||||
|
let schematic_link = SchematicLink {
|
||||||
|
href: format!("/download/{}", filename),
|
||||||
|
filename,
|
||||||
|
};
|
||||||
|
schematic_links.push(schematic_link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("schematic_links", &schematic_links);
|
||||||
|
let rendered = match tera.render("index.html", &context) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(err) => return HttpResponse::InternalServerError().body(err.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpResponse::Ok().body(rendered)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download(req: HttpRequest, args: Data<Args>) -> actix_web::Result<NamedFile> {
|
||||||
|
let filename: PathBuf = req.match_info().query("filename").parse().unwrap();
|
||||||
|
let path = args.schem_dir_path.join(filename);
|
||||||
|
Ok(NamedFile::open(path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, MultipartForm)]
|
||||||
|
pub struct UploadForm {
|
||||||
|
#[multipart(rename = "file")]
|
||||||
|
pub files: Vec<TempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload(
|
||||||
|
MultipartForm(form): MultipartForm<UploadForm>,
|
||||||
|
args: Data<Args>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
match save_files(form, &args).await {
|
||||||
|
Ok(()) => HttpResponse::SeeOther().append_header((header::LOCATION, "/")).finish(),
|
||||||
|
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_files(form: UploadForm, args: &Args) -> Result<(), SaveError> {
|
||||||
|
struct File { name: String, contents: Vec<u8> }
|
||||||
|
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for file in form.files {
|
||||||
|
let name = file.file_name.ok_or(SaveError::NameError)?;
|
||||||
|
|
||||||
|
let mut contents = Vec::new();
|
||||||
|
let mut bytes = file.file.bytes();
|
||||||
|
|
||||||
|
while let Some(byte_or_error) = bytes.next() {
|
||||||
|
let byte = byte_or_error?;
|
||||||
|
contents.push(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push(File { name, contents })
|
||||||
|
}
|
||||||
|
|
||||||
|
for File { name, contents } in files {
|
||||||
|
let path = args.schem_dir_path.join(name);
|
||||||
|
tokio::fs::write(path, contents).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum SaveError {
|
||||||
|
#[error("failed to get name of file")]
|
||||||
|
NameError,
|
||||||
|
#[error(transparent)]
|
||||||
|
IoError(#[from] io::Error),
|
||||||
|
}
|
||||||
35
src/main.rs
Normal file
35
src/main.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use server::ServerError;
|
||||||
|
|
||||||
|
mod handlers;
|
||||||
|
mod server;
|
||||||
|
mod public;
|
||||||
|
mod templates;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), MainError> {
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
server::run(&args).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
pub struct Args {
|
||||||
|
#[arg(short = 'd', long = "schem_dir", env = "SCHEM_DIR")]
|
||||||
|
schem_dir_path: PathBuf,
|
||||||
|
#[arg(short = 'a', long = "address", env = "ADDRESS")]
|
||||||
|
address: String,
|
||||||
|
#[arg(short = 'p', long = "port", env = "PORT", default_value_t = 80)]
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum MainError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Server(#[from] ServerError),
|
||||||
|
}
|
||||||
BIN
src/public/favicon.ico
Normal file
BIN
src/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
2
src/public/mod.rs
Normal file
2
src/public/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub const FAVICON: &'static [u8] = include_bytes!("favicon.ico");
|
||||||
|
pub const ROBOTS: &'static str = include_str!("robots.txt");
|
||||||
2
src/public/robots.txt
Normal file
2
src/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
38
src/server.rs
Normal file
38
src/server.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use actix_web::{web::{self, Data}, App, HttpServer};
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
|
use crate::{handlers, templates, Args};
|
||||||
|
|
||||||
|
const ADDRESS: &str = "127.0.0.1";
|
||||||
|
|
||||||
|
pub async fn run(args: &Args) -> Result<(), ServerError> {
|
||||||
|
std::fs::create_dir_all(&args.schem_dir_path)?;
|
||||||
|
|
||||||
|
let cloned_args = args.clone();
|
||||||
|
let mut tera = Tera::default();
|
||||||
|
tera.add_raw_template("index.html", templates::INDEX_HTML)?;
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(Data::new(cloned_args.clone()))
|
||||||
|
.app_data(Data::new(tera.clone()))
|
||||||
|
.route("/robots.txt", web::get().to(handlers::robots_txt))
|
||||||
|
.route("/favicon.ico", web::get().to(handlers::favicon_ico))
|
||||||
|
.route("/", web::get().to(handlers::index))
|
||||||
|
.route("/download/{filename:.*}", web::get().to(handlers::download))
|
||||||
|
.route("/upload", web::post().to(handlers::upload))
|
||||||
|
})
|
||||||
|
.bind((ADDRESS, args.port))?
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ServerError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Tera(#[from] tera::Error),
|
||||||
|
}
|
||||||
22
src/templates/index.html
Normal file
22
src/templates/index.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Schematics</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Schematics</h1>
|
||||||
|
<h2>Upload</h2>
|
||||||
|
<form action="/upload" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="file" multiple name="file"/>
|
||||||
|
<button type="submit">Upload</button>
|
||||||
|
</form>
|
||||||
|
<h2>Download</h2>
|
||||||
|
<ul>
|
||||||
|
{% for schematic_link in schematic_links %}
|
||||||
|
<li><a href="{{ schematic_link.href }}" download>{{ schematic_link.filename }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
src/templates/mod.rs
Normal file
1
src/templates/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub const INDEX_HTML: &'static str = include_str!("index.html");
|
||||||
Reference in New Issue
Block a user