1
0

working proof of concept

This commit is contained in:
2024-02-03 00:36:56 +00:00
parent 5bdf7a7250
commit a8076f726f
13 changed files with 2539 additions and 0 deletions

9
.editorconfig Normal file
View 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

3
.env Normal file
View File

@@ -0,0 +1,3 @@
ADDRESS=127.0.0.1
PORT=8080
SCHEM_DIR=schematics

5
.gitignore vendored
View File

@@ -8,3 +8,8 @@ target/
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Added by cargo
/target

2291
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
Cargo.toml Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

2
src/public/mod.rs Normal file
View 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
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

38
src/server.rs Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
pub const INDEX_HTML: &'static str = include_str!("index.html");