[WIP] A simple wake-on-lan service
at main 211 lines 6.2 kB view raw
1use std::{ 2 env::{self, VarError}, 3 fs::{self, File}, 4 io::{self, Read, Write}, 5 path::{Path, PathBuf, StripPrefixError}, 6 process::Command, 7}; 8 9use serde::Deserialize; 10use thiserror::Error; 11 12const DIST_SRC: &str = "./web/dist"; 13const WEB_SRC: &str = "./web"; 14const PACKAGE_JSON: &str = "./web/package.json"; 15 16#[derive(Deserialize, Debug)] 17struct Package { 18 scripts: Scripts, 19} 20 21#[derive(Deserialize, Debug)] 22struct Scripts { 23 #[serde(rename = "wol.build")] 24 build: String, 25} 26 27#[derive(Error, Debug)] 28enum BuildError { 29 #[error("IO Error: {}", .0)] 30 Io(#[from] io::Error), 31 #[error("Parse Error: {}", .0)] 32 Parse(#[from] serde_json::Error), 33 #[error("Command failed with error {}\nSTDOUT: {}\nSTDERR: {}", .0.map(|x| x.to_string()).unwrap_or(String::from("N/A")), .1, .2)] 34 Command(Option<i32>, String, String), 35} 36 37fn build() -> Result<(), BuildError> { 38 let mut file = File::open(PACKAGE_JSON)?; 39 let mut package = String::new(); 40 file.read_to_string(&mut package)?; 41 42 let package = serde_json::from_str::<Package>(&package)?; 43 let sh = package.scripts.build; 44 45 let cmd = Command::new("/bin/sh") 46 .arg("-c") 47 .arg(sh) 48 .current_dir(WEB_SRC) 49 .output()?; 50 51 if !cmd.status.success() { 52 return Err(BuildError::Command( 53 cmd.status.code(), 54 String::from_utf8(cmd.stdout).unwrap_or(String::from("Could not parse STDOUT")), 55 String::from_utf8(cmd.stderr).unwrap_or(String::from("Could not parse STDERR")), 56 )); 57 } 58 59 Ok(()) 60} 61 62#[derive(Error, Debug)] 63#[error("{}", .0)] 64struct CopyError(#[from] io::Error); 65 66fn copy_dir(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<Vec<PathBuf>, CopyError> { 67 fs::create_dir_all(&dst)?; 68 let mut paths = Vec::new(); 69 70 for entry in fs::read_dir(&src)? { 71 let entry = entry?; 72 let r#type = entry.file_type()?; 73 if r#type.is_dir() { 74 let mut res = copy_dir(entry.path(), dst.as_ref().join(entry.file_name()))?; 75 paths.append(&mut res); 76 } else { 77 fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; 78 paths.push(entry.path()); 79 } 80 } 81 82 Ok(paths) 83} 84 85#[derive(Error, Debug)] 86enum CodegenError { 87 #[error("Could not strip path: {}", .0)] 88 StripPath(#[from] StripPrefixError), 89 #[error("Error parsing non UTF8 string")] 90 Utf8, 91} 92 93fn codegen(paths: Vec<PathBuf>) -> Result<String, CodegenError> { 94 // idx is a unique id for the current route 95 // this is easier than converting the path to a rust variable and avoiding collisons 96 let mut idx = 0; 97 // source path, route, id 98 let paths = paths 99 .into_iter() 100 .map(|x| { 101 idx += 1; 102 103 let route = Path::new("/").join(x.strip_prefix(DIST_SRC)?); 104 let route = if route.ends_with("index.html") { 105 route.parent().unwrap_or(&route) 106 } else { 107 &route 108 }; 109 let route = route.to_str().ok_or(CodegenError::Utf8)?; 110 let route = String::from(route); 111 112 let path = x.strip_prefix(WEB_SRC).map(|x| x.to_owned())?; 113 114 Ok((path, route, idx)) 115 }) 116 .collect::<Result<Vec<_>, CodegenError>>()?; 117 118 let routes = &paths 119 .iter() 120 .map(|(path, _, idx)| { 121 let ext = path 122 .extension() 123 .and_then(|x| x.to_str()) 124 .and_then(|x| match x { 125 // text 126 "html" | "htm" => Some("text/html"), 127 "css" => Some("text/css"), 128 "js" => Some("text/javascript"), 129 "json" => Some("application/json"), 130 "webmanifest" => Some("application/manifest+json"), 131 "txt" => Some("text/plain"), 132 133 // image 134 "png" => Some("image/png"), 135 "jpeg" | "jpg" => Some("image/jpeg"), 136 "svg" => Some("image/svg+xml"), 137 "webp" => Some("image/webp"), 138 "gif" => Some("image/gif"), 139 "ico" => Some("image/vnc.microsoft.icon"), 140 141 _ => None, 142 }) 143 .unwrap_or("application/octet-stream"); 144 145 Ok(format!( 146 r#"async fn route_{idx}() -> impl axum::response::IntoResponse {{ 147 let mut headers = axum::http::HeaderMap::new(); 148 headers.insert(axum::http::header::CONTENT_TYPE, axum::http::HeaderValue::from_static("{}")); 149 (headers, include_bytes!("{}")) 150 }}"#, 151 ext, 152 path.to_str().ok_or(CodegenError::Utf8)? 153 )) 154 }) 155 .collect::<Result<Vec<_>, CodegenError>>()? 156 .join("\n"); 157 158 let main = format!( 159 r#" 160 pub fn main() -> axum::Router {{ 161 axum::Router::new(){} 162 }} 163 "#, 164 paths 165 .into_iter() 166 .map(|(_, route, idx)| format!(r#".route("{route}", axum::routing::get(route_{idx}))"#)) 167 .collect::<Vec<_>>() 168 .join("") 169 ); 170 171 Ok(format!("mod dist {{ {routes} {main} }}")) 172} 173 174#[derive(Error, Debug)] 175enum Error { 176 #[error("OUT_DIR env variable was not defined. Is this build.rs? {}", .0)] 177 OutDir(#[from] VarError), 178 #[error("Failed to compile ./web: {}", .0)] 179 Web(#[from] BuildError), 180 #[error("Failed to copy ./web/dist {}", .0)] 181 Copy(#[from] CopyError), 182 #[error("Codegen error: {}", .0)] 183 Codegen(#[from] CodegenError), 184 #[error("IO error: {}", .0)] 185 Io(#[from] io::Error), 186} 187 188fn main() -> Result<(), ()> { 189 fn main() -> Result<(), Error> { 190 let out_dir = env::var("OUT_DIR")?; 191 let dist_dst = Path::new(&out_dir).join("dist"); 192 193 build()?; 194 let paths = copy_dir(DIST_SRC, dist_dst)?; 195 let rust = codegen(paths)?; 196 let mut rust_file = fs::File::create(Path::new(&out_dir).join("mod.rs"))?; 197 rust_file.write(rust.as_bytes())?; 198 199 println!("cargo::rerun-if-changed=web/"); 200 201 Ok(()) 202 } 203 204 let res = main(); 205 if let Err(err) = res { 206 eprintln!("{}", err); 207 Err(()) 208 } else { 209 Ok(()) 210 } 211}