diff --git a/core/dsa_common/src/logging.rs b/core/dsa_common/src/logging.rs index b29147f..7fc0155 100644 --- a/core/dsa_common/src/logging.rs +++ b/core/dsa_common/src/logging.rs @@ -21,7 +21,7 @@ impl Logger { #[must_use] pub fn new(logs_tx: mpsc::Sender, use_stdio: bool) -> Self { Self { - use_stdio: true, + use_stdio, logs_tx: Arc::new(logs_tx), } } diff --git a/dsx/dsx/Cargo.toml b/dsx/dsx/Cargo.toml index 2fe2c45..f1c9fe1 100644 --- a/dsx/dsx/Cargo.toml +++ b/dsx/dsx/Cargo.toml @@ -15,3 +15,6 @@ common = { path = "../../core/dsa_common" } dsx_common = { path = "../dsx_common" } toml = "1.0.3" chrono = "0.4.44" +reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "native-tls"] } +tar = "0.4.44" +flate2 = "1.1.9" diff --git a/dsx/dsx/src/main.rs b/dsx/dsx/src/main.rs index e0bf7fc..0029181 100644 --- a/dsx/dsx/src/main.rs +++ b/dsx/dsx/src/main.rs @@ -1,8 +1,9 @@ use std::{env, fs, path::PathBuf, process::Command}; -use dsx_common::builder; +use dsx_common::builder::{self, BuildContext}; pub mod new; +pub mod repo; pub mod templates; fn main() { @@ -16,7 +17,13 @@ fn main() { "new" => new::new_project(&args[2..]), "build" => { if let Some(dir) = find_project_root() { - builder::build_project(&dir).expect("Build failed!"); + let ctx = BuildContext { + project_dir: dir.clone(), + build_dir: dir.join("build"), + artifact_dir: dir.join("artifacts"), + }; + + builder::build_project(ctx).expect("Build failed!"); } else { eprintln!("No Dsx.toml found"); std::process::exit(1); @@ -37,7 +44,13 @@ fn main() { } "run" => { if let Some(dir) = find_project_root() { - builder::build_project(&dir).expect("Run failed!"); + let ctx = BuildContext { + project_dir: dir.clone(), + build_dir: dir.join("build"), + artifact_dir: dir.join("artifacts"), + }; + + builder::build_project(ctx).expect("Run failed!"); // start process and call emulator let mut child = Command::new("dsa") @@ -54,6 +67,22 @@ fn main() { std::process::exit(1); } } + "push" => { + if let Some(dir) = find_project_root() { + repo::push(&dir).expect("Failed to push repository"); + } else { + eprintln!("No Dsx.toml found"); + std::process::exit(1); + } + } + "pull" => { + if let Some(dir) = find_project_root() { + repo::pull(&dir).expect("Failed to pull repository"); + } else { + eprintln!("No Dsx.toml found"); + std::process::exit(1); + } + } "package" => todo!("Package manager stub – not implemented yet."), _ => { eprintln!("Unknown command: {}", args[1]); diff --git a/dsx/dsx/src/repo.rs b/dsx/dsx/src/repo.rs new file mode 100644 index 0000000..279f1fd --- /dev/null +++ b/dsx/dsx/src/repo.rs @@ -0,0 +1,137 @@ +use dsx_common::config::DsxConfig; +use flate2::read::GzDecoder; +use flate2::{Compression, write::GzEncoder}; +use reqwest::Url; +use std::fs::{self, File}; +use std::io::Read; +use std::path::{Path, PathBuf}; +use tar::Builder; + +pub fn push(repo_dir: &Path) -> Result<(), DsxError> { + let config_file = fs::read_to_string(repo_dir.join("Dsx.toml"))?; + let config: DsxConfig = toml::from_str(&config_file).expect("Failed to parse config"); + + let mut repo_url = + Url::parse(&config.remote_url.expect( + "Repository URL is not set in Dsx.toml, set it with the key 'remote'", + )) + .unwrap(); + repo_url.path_segments_mut().unwrap().push("push"); + + let client = reqwest::blocking::Client::new(); + let response = client + .post(repo_url) + .body(pack_tarball(repo_dir)?) + .header("Content-Type", "application/octet-stream") + .send() + .map_err(|e| { + DsxError::with_context( + format!("failed to stream to client: {}", e), + ErrorType::IoError, + ) + })?; + + if response.status().is_success() { + Ok(()) + } else { + Err(DsxError::with_context( + response.text().unwrap(), + ErrorType::IoError, + )) + } +} + +pub fn pull(repo_dir: &Path) -> Result<(), DsxError> { + let config_file = fs::read_to_string(repo_dir.join("Dsx.toml"))?; + let config: DsxConfig = toml::from_str(&config_file).expect("Failed to parse config"); + + let mut repo_url = + Url::parse(&config.remote_url.expect( + "Repository URL is not set in Dsx.toml, set it with the key 'remote'", + )) + .unwrap(); + repo_url.path_segments_mut().unwrap().push("pull"); + + let client = reqwest::blocking::Client::new(); + let response = client.get(repo_url).send().map_err(|e| { + DsxError::with_context("failed to stream from client", ErrorType::IoError) + })?; + + if !response.status().is_success() { + return Err(DsxError::with_context( + response.text().unwrap(), + ErrorType::IoError, + )); + } + + let tmp_file = std::env::temp_dir().join("dsx-pull.tar.gz"); + std::fs::write(&tmp_file, response.bytes().unwrap()) + .map_err(|e| DsxError::with_context(e.to_string(), ErrorType::IoError))?; + unpack_tarball(&tmp_file, repo_dir)?; + fs::remove_file(tmp_file)?; + Ok(()) +} + +fn unpack_tarball( + archive: &std::path::Path, + dest: &std::path::Path, +) -> Result<(), DsxError> { + let file = File::open(archive) + .map_err(|e| DsxError::with_context(e.to_string(), ErrorType::IoError))?; + + fs::create_dir_all(dest)?; + + let gz = GzDecoder::new(file); + let mut tar = tar::Archive::new(gz); + tar.unpack(dest) + .map_err(|e| DsxError::with_context(e.to_string(), ErrorType::TarError))?; + + Ok(()) +} + +fn pack_tarball(src_dir: &std::path::Path) -> Result, DsxError> { + let buf = Vec::new(); + let gz = GzEncoder::new(buf, Compression::default()); + let mut tar = Builder::new(gz); + tar.append_dir_all(".", src_dir)?; + let gz = tar.into_inner()?; + Ok(gz.finish()?) +} + +#[derive(Debug)] +pub struct DsxError { + pub message: String, + pub r#type: ErrorType, +} + +impl DsxError { + pub fn new(message: impl AsRef) -> Self { + DsxError { + message: message.as_ref().to_string(), + r#type: ErrorType::Generic, + } + } + + pub fn with_context(message: impl AsRef, r#type: ErrorType) -> Self { + DsxError { + message: message.as_ref().to_string(), + r#type, + } + } +} + +impl From for DsxError { + fn from(err: std::io::Error) -> Self { + Self::with_context(err.to_string(), ErrorType::IoError) + } +} + +#[derive(Default, Debug)] +pub enum ErrorType { + BuildFailed, + IoError, + + #[default] + Generic, + TarError, +} diff --git a/dsx/dsx_common/src/builder.rs b/dsx/dsx_common/src/builder.rs index 986b0d0..5145a84 100644 --- a/dsx/dsx_common/src/builder.rs +++ b/dsx/dsx_common/src/builder.rs @@ -10,14 +10,26 @@ use assembler::prelude::Assembler; use common::build::{BuildError, Builder}; use compiler::Compiler; +pub struct BuildContext { + pub project_dir: PathBuf, + pub build_dir: PathBuf, + pub artifact_dir: PathBuf, +} + // ---------- build ---------------------------------------------------------- -pub fn build_project(cwd: &Path) -> Result<(), BuildError> { - let config: DsxConfig = toml::from_str(&fs::read_to_string(cwd.join("Dsx.toml"))?) - .map_err(|deser_err| { +pub fn build_project(ctx: BuildContext) -> Result<(), BuildError> { + // variables + let binary_path = ctx.artifact_dir.join("out.dsb"); + let main_path = ctx.build_dir.join("main.dsa"); + let config_path = ctx.project_dir.join("Dsx.toml"); + + let src_dir = ctx.project_dir.join("src"); + + let config: DsxConfig = + toml::from_str(&fs::read_to_string(&config_path)?).map_err(|deser_err| { io::Error::new(io::ErrorKind::InvalidData, deser_err.to_string()) })?; - let src_dir = cwd.join("src"); if !src_dir.exists() { return Err(BuildError::Generic(String::from( "Source Directory does not exist", @@ -35,36 +47,38 @@ pub fn build_project(cwd: &Path) -> Result<(), BuildError> { let (has_dsa, has_dsc) = detect_source_language(&src_dir); // create a build dir and copy all files across - let build_dir = cwd.join("build"); - fs::create_dir_all(&build_dir)?; - env::set_current_dir(&build_dir)?; - - copy_recursively(&src_dir, &build_dir)?; + fs::create_dir_all(&ctx.build_dir)?; + copy_recursively(&src_dir, &ctx.build_dir)?; if has_dsc { - build_all_dsc(&build_dir)?; + build_all_dsc(&ctx.build_dir)?; } // Replace .dsc with .dsa only in include statements, recursively for each file. - let mut sed_cmd = Command::new("bash"); - sed_cmd.args([ + + let status = Command::new("bash").args([ "-c", &format!( "find \"{}\" -type f -name '*.dsa' -exec sed -i '/^include/ s/\\.dsc/.dsa/g' {{}} +", - build_dir.display() + ctx.build_dir.display() ), - ]); - run(&mut sed_cmd); + ]).status()?; + + if !status.success() { + return Err(BuildError::IoError(String::from( + "Failed to execute build command command", + ))); + } // assemble result { - fs::create_dir_all(cwd.join("artifacts"))?; - let mut asm = Assembler::new("./main.dsa"); + fs::create_dir_all(&ctx.artifact_dir)?; + let mut asm = Assembler::new(&main_path); asm.start(()); - asm.write_result("../artifacts/out.dsb")?; + asm.write_result(&binary_path)?; } - println!("Build finished. Binary at {}/main.dsb", build_dir.display()); + println!("Build finished. Binary at {}", binary_path.display()); Ok(()) } @@ -152,11 +166,3 @@ fn build_all_dsc(path: &Path) -> Result<(), BuildError> { Ok(()) } - -/// Run a command and exit on failure. -fn run(cmd: &mut Command) { - let status = cmd.status().expect("failed to execute command"); - if !status.success() { - std::process::exit(1); - } -} diff --git a/dsx/dsx_common/src/config.rs b/dsx/dsx_common/src/config.rs index 32be942..6219f34 100644 --- a/dsx/dsx_common/src/config.rs +++ b/dsx/dsx_common/src/config.rs @@ -8,6 +8,7 @@ pub struct DsxConfig { pub description: Option, #[serde(default)] + #[serde(rename = "remote")] pub remote_url: Option, #[serde(default)] diff --git a/dsx/dsx_repo_data/repos/example/Package.toml b/dsx/dsx_repo_data/repos/example/Package.toml deleted file mode 100644 index a2727f9..0000000 --- a/dsx/dsx_repo_data/repos/example/Package.toml +++ /dev/null @@ -1 +0,0 @@ -id="example" diff --git a/dsx/dsx_repo_data/repos/example/repo/Dsx.toml b/dsx/dsx_repo_data/repos/example/repo/Dsx.toml deleted file mode 100644 index da5c772..0000000 --- a/dsx/dsx_repo_data/repos/example/repo/Dsx.toml +++ /dev/null @@ -1 +0,0 @@ -name = "example" diff --git a/dsx/dsx_repo_data/repos/example/repo/src/main.dsa b/dsx/dsx_repo_data/repos/example/repo/src/main.dsa deleted file mode 100644 index 9b60951..0000000 --- a/dsx/dsx_repo_data/repos/example/repo/src/main.dsa +++ /dev/null @@ -1,39 +0,0 @@ - -// GENERATED BY DSX-BUILD -// Generated at: 2026-02-21 02:50:14 -// Project name: example - -// Imports -include print: "./lib/print.dsa" - -// Globals & Reserved Memory -dw stack: 0x10000 -db message: "Process Exited with code:" - -// Entry Point -_init: - ldw stack, bpr - mov bpr, spr - push zero - call main - call print::print_newline - lwi message, rg0 - push rg0 - call print::print - pop zero - call print::print_hex_word - pop zero - hlt - -main: - push bpr - mov spr, bpr - - // Your code goes here - - // Return zero - stw zero, bpr, 8 - - mov bpr, spr - pop bpr - return \ No newline at end of file diff --git a/dsx/dsx_repo_data/repos/test/Package.toml b/dsx/dsx_repo_data/repos/test/Package.toml new file mode 100644 index 0000000..54b71ee --- /dev/null +++ b/dsx/dsx_repo_data/repos/test/Package.toml @@ -0,0 +1,4 @@ +id = "test" +latest_build_date = "2026-02-25 14:39:49" +latest_build_status = "success" +latest_build_id = "test" diff --git a/dsx/dsx_repo_data/repos/test/repo/Dsx.toml b/dsx/dsx_repo_data/repos/test/repo/Dsx.toml new file mode 100644 index 0000000..77d9b05 --- /dev/null +++ b/dsx/dsx_repo_data/repos/test/repo/Dsx.toml @@ -0,0 +1,3 @@ +name = "test" +binaries = [] +remote = "http://localhost:8000/api/pkg/test" diff --git a/dsx/dsx_repo_data/repos/test/repo/src/lib/arena_alloc.dsc b/dsx/dsx_repo_data/repos/test/repo/src/lib/arena_alloc.dsc new file mode 100644 index 0000000..94e12b7 --- /dev/null +++ b/dsx/dsx_repo_data/repos/test/repo/src/lib/arena_alloc.dsc @@ -0,0 +1,77 @@ +// Arena Allocator +// Supports multiple arenas that can be destroyed independently +// Much more practical than a simple bump allocator + +// Global heap management +static heap_start: u32 = 0x30000; +static heap_end: u32 = 0x40000; +static heap_current: u32 = 0x30000; + +// Arena structure (stored at the start of each arena): +// [0-3]: start_address (u32) +// [4-7]: current_position (u32) +// [8-11]: end_address (u32) +// Total header size: 12 bytes + +// Create a new arena with given size +// Returns pointer to arena handle (or 0 if failed) +fn new(size: u32) -> u32 { + let total_size: u32 = size + 12; + let arena_ptr: u32 = heap_current; + let new_current: u32 = arena_ptr + total_size; + + // Check if we have space + if new_current > heap_end { + return 0; + } + + // Calculate arena data region + let data_start: u32 = arena_ptr + 12; + let data_end: u32 = arena_ptr + total_size; + + // Initialize arena header + // Note: In real implementation, you'd use pointer writes here + // For now, using placeholder comments: + *arena_ptr = data_start; // start_address + *(arena_ptr + 4) = data_start; // current_position + *(arena_ptr + 8) = data_end; // end_address + + heap_current = new_current; + + return arena_ptr; +} + +// Allocate from an arena +// Returns pointer to allocated memory (or 0 if failed) +fn alloc(arena: u32, size: u32) -> u32 { + // Read current position from arena + let current: u32 = *(arena + 4); + let end: u32 = *(arena + 8); + + let new_current: u32 = current + size; + + // Check if arena has space + if new_current > end { + return 0; + } + + // Update current position in arena + *(arena + 4) = new_current; + + return current; +} + +// Destroy an arena (in bump allocator, this is a no-op) +// In a real allocator, you'd mark the memory as free +fn destroy(arena: u32) { + // In a true allocator, mark memory as reusable + // For bump allocator, we can't reclaim memory + // unless we destroy ALL arenas and reset + return 0; +} + +// Reset entire heap (destroys ALL arenas) +fn reset_all() { + heap_current = heap_start; + return 0; +} diff --git a/dsx/dsx_repo_data/repos/example/repo/src/lib/maths.dsa b/dsx/dsx_repo_data/repos/test/repo/src/lib/maths.dsa similarity index 99% rename from dsx/dsx_repo_data/repos/example/repo/src/lib/maths.dsa rename to dsx/dsx_repo_data/repos/test/repo/src/lib/maths.dsa index 687ad5d..7640c04 100644 --- a/dsx/dsx_repo_data/repos/example/repo/src/lib/maths.dsa +++ b/dsx/dsx_repo_data/repos/test/repo/src/lib/maths.dsa @@ -1,4 +1,3 @@ - // multiply.dsa // usage: // diff --git a/dsx/dsx_repo_data/repos/example/repo/src/lib/print.dsa b/dsx/dsx_repo_data/repos/test/repo/src/lib/print.dsa similarity index 99% rename from dsx/dsx_repo_data/repos/example/repo/src/lib/print.dsa rename to dsx/dsx_repo_data/repos/test/repo/src/lib/print.dsa index ead1d5c..f66d4b7 100644 --- a/dsx/dsx_repo_data/repos/example/repo/src/lib/print.dsa +++ b/dsx/dsx_repo_data/repos/test/repo/src/lib/print.dsa @@ -1,4 +1,3 @@ - // lib: // print.dsa diff --git a/dsx/dsx_repo_data/repos/test/repo/src/lib/serial.dsa b/dsx/dsx_repo_data/repos/test/repo/src/lib/serial.dsa new file mode 100644 index 0000000..1e66a62 --- /dev/null +++ b/dsx/dsx_repo_data/repos/test/repo/src/lib/serial.dsa @@ -0,0 +1,274 @@ +// lib: +// print_serial.dsa + +// usage: +// +// include print_serial "" +// +// usage for print: +// push (register containing address of string) +// push pcx +// jmp print_serial::print +// +// usage for print_byte: +// push (register containing byte) +// push pcx +// jmp print_serial::print_byte +// +// usage for print_word: +// push (register containing word) +// push pcx +// jmp print_serial::print_word +// +// usage for print_hex_byte: +// push (register containing byte) +// push pcx +// jmp print_serial::print_hex_byte +// +// usage for print_hex_word: +// push (register containing word) +// push pcx +// jmp print_serial::print_hex_word +// +// usage for print_whitespace: +// push pcx +// jmp print_serial::print_whitespace +// +// usage for print_newline: +// push pcx +// jmp print_serial::print_newline +// +// usage for print_num: +// push (register containing number to print in decimal) +// push pcx +// jmp print_serial::print_num +// +// usage for println: +// push (register containing address of string) +// push pcx +// jmp print_serial::println +// + +include maths "./maths.dsa" + +dw serial: 0x207D0 // 0x20000 + 2000 + +// ------------------------------------------ +// prints the string at addr(arg[0]) to the serial port. +print: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + lwi 0x207D0, rg1 + +_print_loop: + ldb rg0, acc + cmp acc, zero + jeq _end + stb acc, rg1 + + addi rg0, 1 + jmp _print_loop + +// ------------------------------------------ +// prints the string at addr(arg[0]) followed by a newline to the serial port. +println: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + lwi 0x207D0, rg1 + +_println_loop: + ldb rg0, acc + cmp acc, zero + jeq _println_end + stb acc, rg1 + + addi rg0, 1 + jmp _println_loop + +_println_end: + lli 0x0A, rg2 // newline character + stb rg2, rg1 + jmp _end + +// ------------------------------------------ +// prints the word in arg[0] as 4 raw bytes to the serial port. +print_word: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + lwi 0x207D0, rg1 + + stb rg0, rg1 + shr rg0, 8 + stb rg0, rg1 + shr rg0, 8 + stb rg0, rg1 + shr rg0, 8 + stb rg0, rg1 + jmp _end + +// ------------------------------------------ +// prints the last byte of arg[0] to the serial port. +print_byte: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + lwi 0x207D0, rg1 + + stb rg0, rg1 + jmp _end + +// ------------------------------------------ +// prints the value of arg[0] to the serial port in hex. +print_hex_word: + push bpr + mov spr, bpr + + lwi 0x207D0, rg1 + + ldb bpr, rg0, 8 + push rg0 + call _print_hex_byte + addi spr, 4 + + ldb bpr, rg0, 9 + push rg0 + call _print_hex_byte + addi spr, 4 + + ldb bpr, rg0, 10 + push rg0 + call _print_hex_byte + addi spr, 4 + + ldb bpr, rg0, 11 + push rg0 + call _print_hex_byte + addi spr, 4 + + jmp _end + +// ------------------------------------------ +// prints the last byte of arg[0] to the serial port in hex. +print_hex_byte: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + lwi 0x207D0, rg1 + + call _print_hex_byte + jmp _end + +// function body +_print_hex_byte: + lli 0xF, rg2 + push rg0 + + shr rg0, 4 + and rg0, rg2, rg0 + call _print_hex_nibble + pop rg0 + + and rg0, rg2, rg0 + call _print_hex_nibble + return + +// print a hex digit +_print_hex_nibble: + lli 10, rg3 + cmp rg0, rg3 + jlt _print_hex_nibble_number + addi rg0, 0x37, rg0 + stb rg0, rg1 + return + +_print_hex_nibble_number: + addi rg0, 0x30, rg0 + stb rg0, rg1 + return + +// print a single space +print_whitespace: + push bpr + mov spr, bpr + + lli 0x20, rg0 + ldw serial, rg1 + stb rg0, rg1 + + jmp _end + +// print a single space +print_newline: + push bpr + mov spr, bpr + + lli 0x0A, rg0 + ldw serial, rg1 + stb rg0, rg1 + + jmp _end + +// ------------------------------------------ +// prints arg[0] as a decimal number to the serial port. +print_num: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + lli 0, rg5 + + cmp rg0, zero + jne _print_num_extract_digits + + lli 0x30, rg6 + push rg6 + lli 1, rg5 + jmp _print_num_output + +_print_num_extract_digits: + cmp rg0, zero + jeq _print_num_output + + push rg0 + lli 10, rg1 + push rg1 + call maths::divmod + pop rg0 + pop rg1 + + addi rg1, 0x30, rg6 + push rg6 + inc rg5 + + jmp _print_num_extract_digits + +_print_num_output: + lwi 0x207D0, rg1 + +_print_num_output_loop: + cmp rg5, zero + jeq _print_num_done + + pop rg6 + stb rg6, rg1 + dec rg5 + + jmp _print_num_output_loop + +_print_num_done: + jmp _end + +// ------------------------------------------ +// return +_end: + mov bpr, spr + pop bpr + return diff --git a/dsx/dsx_repo_data/repos/test/repo/src/main.dsc b/dsx/dsx_repo_data/repos/test/repo/src/main.dsc new file mode 100644 index 0000000..368d3ad --- /dev/null +++ b/dsx/dsx_repo_data/repos/test/repo/src/main.dsc @@ -0,0 +1,30 @@ +include serial: "./lib/serial.dsa"; +include print: "./lib/print.dsa"; +include arena: "./lib/arena_alloc.dsc"; + +fn main() { + let x: u32 = 0; + let y: u32 = &x; + + let alloc: u32 = arena::new(512); + let ptr1: u32 = arena::alloc(alloc, 32); + let ptr2: u32 = arena::alloc(alloc, 32); + + serial::print_hex_word(alloc); + serial::print_newline(); + serial::print_hex_word(ptr1); + serial::print_newline(); + serial::print_hex_word(ptr2); + serial::print_newline(); + serial::print_num(*ptr2); + serial::print_newline(); + *ptr2 = 42; + + serial::print_hex_word(ptr2); + serial::print_whitespace(); + serial::print_num(*ptr2); + serial::print_newline(); + serial::println("end"); + + return 0; +} diff --git a/dsx/dsx_server/Cargo.toml b/dsx/dsx_server/Cargo.toml index fa92bb2..baf595f 100644 --- a/dsx/dsx_server/Cargo.toml +++ b/dsx/dsx_server/Cargo.toml @@ -25,3 +25,4 @@ chrono = "0.4.43" tar = "0.4.44" flate2 = "1.1.9" walkdir = "2.5.0" +tokio = { version = "1.49.0", features = ["rt"] } diff --git a/dsx/dsx_server/bacon.toml b/dsx/dsx_server/bacon.toml index 66d1030..c486658 100644 --- a/dsx/dsx_server/bacon.toml +++ b/dsx/dsx_server/bacon.toml @@ -59,7 +59,7 @@ on_success = "back" # so that we don't open the browser at each change # if it makes sense for this crate. [jobs.run] command = [ - "cargo", "run", "--bin", "dsx_server" + "cargo", "run", "--bin", "dsx-server" # put launch parameters for your program behind a `--` separator ] need_stdout = true diff --git a/dsx/dsx_server/doc/endpoints.md b/dsx/dsx_server/doc/endpoints.md index efa1cbc..b82f8df 100644 --- a/dsx/dsx_server/doc/endpoints.md +++ b/dsx/dsx_server/doc/endpoints.md @@ -12,6 +12,7 @@ GET /packages//~repo?q= # search within a package's files GET /packages//~artifact/ # page listing repo artifacts by date GET /packages//~artifact/ # page for a specific artifact and status/logs +## API POST /api/pkg # create repo GET /api/pkg/ # repo status/metadata POST /api/pkg//push # upload source tarball diff --git a/dsx/dsx_server/src/error.rs b/dsx/dsx_server/src/error.rs index d7cdd0f..e64bec6 100644 --- a/dsx/dsx_server/src/error.rs +++ b/dsx/dsx_server/src/error.rs @@ -36,3 +36,47 @@ impl From for ApiError { } } } + +impl From for ApiError { + fn from(err: DsxError) -> Self { + ApiError::ServerError(format!("{:?}: {}", err.r#type, err.message)) + } +} + +#[derive(Debug)] +pub struct DsxError { + pub message: String, + pub r#type: ErrorType, +} + +impl DsxError { + pub fn new(message: impl AsRef) -> Self { + DsxError { + message: message.as_ref().to_string(), + r#type: ErrorType::Generic, + } + } + + pub fn with_context(message: impl AsRef, r#type: ErrorType) -> Self { + DsxError { + message: message.as_ref().to_string(), + r#type, + } + } +} + +impl From for DsxError { + fn from(err: std::io::Error) -> Self { + Self::with_context(err.to_string(), ErrorType::IoError) + } +} + +#[derive(Default, Debug)] +pub enum ErrorType { + BuildFailed, + IoError, + + #[default] + Generic, + TarError, +} diff --git a/dsx/dsx_server/src/main.rs b/dsx/dsx_server/src/main.rs index eb61fe6..8afaa38 100644 --- a/dsx/dsx_server/src/main.rs +++ b/dsx/dsx_server/src/main.rs @@ -10,7 +10,8 @@ use rocket::data::ToByteUnit; use rocket::serde::json::Json; use rocket::{fs::FileServer, serde::Deserialize}; -use rocket_dyn_templates::{Template, context}; +use rocket_dyn_templates::tera::{Function, Value}; +use rocket_dyn_templates::{Template, context, tera}; use dotenv::dotenv; use serde::Serialize; @@ -23,247 +24,57 @@ use dsx_common::config::DsxConfig; mod error; mod model; -static DATA_DIR: LazyLock = LazyLock::new(|| { - PathBuf::from(std::env::var("$DATA_DIR").unwrap_or("./data".to_string())) -}); +mod routes; -// Search for a package -#[get("/?")] -fn search_packages(q: Option) -> Result { - #[derive(Serialize)] - struct Package { - name: String, - description: String, - updated_at: String, - } - - let mut packages = Vec::new(); - - let dir = match fs::read_dir(DATA_DIR.join("repos")) { - Ok(dir) => dir, - Err(e) => { - warn!("failed to read repos directory: {}", e); - return Err(ApiError::InternalServerError(())); - } - }; - - for entry in dir { - let entry = entry.map_err(|e| { - warn!("failed to read entry: {}", e); - ApiError::InternalServerError(()) - })?; - - let config_path = entry.path().join("repo").join("Dsx.toml"); - if config_path.exists() { - let text = fs::read_to_string(&config_path).map_err(|e| { - warn!("failed to read config file: {}", e); - ApiError::InternalServerError(()) - })?; - let config: DsxConfig = toml::from_str(&text).map_err(|e| { - warn!("failed to parse config file: {}", e); - ApiError::InternalServerError(()) - })?; - - error!("{}", config.description.clone().unwrap_or_default()); - - // skip repo if it doesnt match query params - if let Some(query) = &q - && !(config.name.contains(query) - || config - .description - .clone() - .unwrap_or_default() - .contains(query)) - { - continue; +fn language_colour() -> impl Function { + Box::new( + move |args: &std::collections::HashMap| -> tera::Result { + match args.get("lang") { + Some(Value::String(lang)) => match lang.as_str() { + // language syntax colour + "dsc" => Ok(Value::String("#000000".to_string())), + "dsa" => Ok(Value::String("#3776AB".to_string())), + _ => Ok(Value::String("#FFFFFF".to_string())), + }, + Some(_) => Err(tera::Error::msg("Invalid argument type")), + None => Ok(Value::String("#FFFFFF".to_string())), } - - packages.push(Package { - name: config.name, - description: config.description.unwrap_or_default(), - updated_at: String::from("0:00"), - }) - } - } - - Ok(Template::render( - "packages", - context! { - packages, - query: q.clone().unwrap_or_default() }, - )) -} - -// Main page for a repository, shows status, files, name etc. -#[get("/")] -fn package_main(name: &str) -> Result { - // get package info - let package = Package::load(name)?; - - println!("{}", package.config.name); - - Ok(Template::render( - "package_home", - context! { - parent_path: String::new(), - current_path: String::from("/"), - package: package, - }, - )) -} - -// Path for a file within a repo -#[get("//~repo/")] -fn repo_file(name: &str, path: std::path::PathBuf) -> Result { - let package = Package::load(name)?; - - Ok(Template::render( - "file", - context! { - package: package, - }, - )) -} - -// Search within a package's files -#[get("//~repo?")] -fn search_repo_files(name: &str, q: Option) -> String { - format!("Search within {} for {:?}", name, q) -} - -// Page listing repo artifacts by date -#[get("//artifacts")] -fn list_artifacts(name: &str) -> String { - format!("Artifacts for package {}", name) -} - -// Page for a specific artifact and status/logs -#[get("//artifacts/")] -fn artifact_detail(name: &str, id: u64) -> String { - format!("Artifact {} details for package {}", id, name) -} - -#[derive(Deserialize)] -#[serde(crate = "rocket::serde")] -struct NewRepo<'r> { - name: &'r str, -} - -// Create repo -#[post("/pkg", data = "")] -fn create_repo(repo: Json>) -> Result<(), &'static str> { - let path = DATA_DIR.join("repos").join(repo.name); - - if repo.name.is_empty() { - return Err("Repository name cannot be empty!"); - } - - if path.exists() { - tracing::info!( - "Attempt to create repository '{}' which already exists.", - repo.name - ); - return Err("This repository already exists!"); - } - - if let Err(e) = fs::create_dir_all(path) { - tracing::error!( - "Attempted to create package with name {} - Error: {e},", - repo.name - ); - return Err("Internal server error"); - } - - Ok(()) -} - -// Repo status/metadata -#[get("/pkg/")] -fn get_pkg(name: &str) -> Result, ApiError> { - let package = Package::load(name).map_err(|e| { - ApiError::NotFound(String::from("repo with name {name} does not exist")) - })?; - - Ok(Json((package.meta, package.config))) -} - -// Upload source tarball -#[post("/pkg//push", data = "")] -async fn push_tarball(name: &str, data: Data<'_>) -> Result<(), ApiError> { - let repo_dir = DATA_DIR.join("repos").join(name); - let tmp_path = repo_dir.join("upload.tar.gz"); - let stream = data - .open(256.mebibytes()) - .into_file(&tmp_path) - .await - .map_err(|e| ApiError::InternalServerError(()))?; - - if !stream.is_complete() { - return Err(ApiError::BadRequest("Incomplete upload".to_string())); - } - - // Unpack over the existing repo dir. - if repo_dir.exists() { - fs::remove_dir_all(&repo_dir).map_err(|e| ApiError::InternalServerError(()))?; - } - fs::create_dir_all(&repo_dir).map_err(|e| ApiError::InternalServerError(()))?; - - let _ = Package::unpack(&tmp_path)?; - fs::remove_file(&tmp_path).ok(); - Ok(()) -} - -// Download source tarball -#[get("/pkg//pull")] -fn pull_tarball( - name: &str, -) -> Result<(rocket::http::ContentType, Vec), Json> { - if let Ok(package) = Package::load(name) { - let tarball = package.tarball()?; - Ok(( - rocket::http::ContentType::new("application", "octet-stream"), - tarball, - )) - } else { - Err(Json(ApiError::NotFound(format!( - "repo with name {name} does not exist" - )))) - } -} - -// Download compiled binary -#[get("/pkg//artifact")] -fn download_artifact(name: &str) -> &'static str { - "Download artifact" + ) } #[launch] fn rocket() -> _ { dotenv().unwrap(); + use routes::api; + use routes::pages; + rocket::build() .mount( "/packages", routes![ - search_packages, - package_main, - repo_file, - search_repo_files, - list_artifacts, - artifact_detail, + pages::search_packages, + pages::package_main, + pages::repo_file, + pages::search_repo_files, + pages::list_artifacts, + pages::artifact_detail, ], ) .mount( "/api", routes![ - create_repo, - get_pkg, - push_tarball, - pull_tarball, - download_artifact + api::create_repo, + api::get_pkg, + api::push_tarball, + api::pull_tarball, + api::download_artifact ], ) - .attach(Template::fairing()) + .attach(Template::custom(|tera| { + tera.tera + .register_function("language_colour", language_colour()); + })) .mount("/static", FileServer::from("./static")) } diff --git a/dsx/dsx_server/src/model.rs b/dsx/dsx_server/src/model.rs index 1407d43..d2b22d6 100644 --- a/dsx/dsx_server/src/model.rs +++ b/dsx/dsx_server/src/model.rs @@ -1,12 +1,21 @@ use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::sync::LazyLock; +use chrono::Utc; +use dsx_common::builder::{self, BuildContext}; use dsx_common::config::DsxConfig; use serde::{Deserialize, Serialize}; +use walkdir::WalkDir; -use crate::{DATA_DIR, error::ApiError}; +pub static DATA_DIR: LazyLock = LazyLock::new(|| { + PathBuf::from(std::env::var("DATA_DIR").unwrap_or("./data".to_string())) +}); -// stored as a Config.toml above the repository root. +use crate::error::{ApiError, DsxError, ErrorType}; + +// stored as a Package.toml above the repository root. +// not directly modifiable by the user/maintainer. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PackageMeta { pub id: String, @@ -17,11 +26,15 @@ pub struct PackageMeta { pub latest_build_status: Option, #[serde(default)] pub latest_build_id: Option, + + #[serde(default)] + pub detected_languages: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Package { pub config: DsxConfig, + pub language: String, pub meta: PackageMeta, pub files: Vec, } @@ -35,47 +48,217 @@ pub struct FileObj { pub extension: String, } -impl Package { - pub fn load(name: &str) -> Result { - let repo_path = DATA_DIR.join("repos").join(name); +enum BuildStatus { + // if we don't know the build state, or no build has run before. + None, - let config_contents = fs::read_to_string(repo_path.join("repo/Dsx.toml")) - .map_err(|e| { - warn!("unable to read config for repo, {e}"); - ApiError::InternalServerError(()) - })?; + // waiting for build + Pending, - let config: DsxConfig = toml::from_str(&config_contents).map_err(|e| { - warn!("Invalid config file for repo! {e}"); - ApiError::InternalServerError(()) + // build + Building, + + // build result + Success, + Failure, + Error, +} + +pub struct PackageHandle { + // id of the package + pub package_id: String, + + config: Option, + meta: Option, + + // build info + build_state: BuildStatus, +} + +impl PackageHandle { + const META_PATH: &'static str = "Package.toml"; + const CONFIG_PATH: &'static str = "repo/Dsx.toml"; + const ARTIFACTS_DIR: &'static str = "artifacts"; + + pub fn new(package_id: impl Into) -> Self { + PackageHandle { + package_id: package_id.into(), + config: None, + meta: None, + build_state: BuildStatus::None, + } + } + + pub fn load(&mut self) -> Result<&mut Self, DsxError> { + self.get_config()?; + self.get_meta()?; + Ok(self) + } + + pub fn tarball(&self) -> Result, DsxError> { + let src_dir = self.path().join("repo"); + pack_tarball(&src_dir) + } + + pub fn unpack(&mut self, archive: &Path) -> Result<&mut Self, DsxError> { + let dest = DATA_DIR.join("repos").join(&self.package_id).join("repo"); + unpack_tarball(archive, &dest)?; + self.load() + } + + pub fn path(&self) -> PathBuf { + DATA_DIR.join("repos").join(&self.package_id) + } + + pub fn build(&mut self) -> Result<&mut Self, DsxError> { + let ctx = BuildContext { + project_dir: self.path().join("repo"), + build_dir: self.path().join("build"), + artifact_dir: self.path().join("artifacts"), + }; + + self.build_state = BuildStatus::Building; + + let id = self.package_id.clone(); + tokio::task::spawn_blocking(move || { + let id = id; + + let res = match builder::build_project(ctx) { + Ok(_) => "success", + Err(_) => "failure", + }; + + let mut handle = PackageHandle::new(&id); + let mut meta = handle.get_meta().unwrap(); + meta.latest_build_date = + Some(Utc::now().format("%Y-%m-%d %H:%M:%S").to_string()); + meta.latest_build_status = Some(res.to_string()); + meta.latest_build_id = Some(id); + handle.set_meta(meta) + }); + + Ok(self) + } + + pub fn get_config(&mut self) -> Result { + let config_path = self.path().join(Self::CONFIG_PATH); + + let config = fs::read_to_string(&config_path).map_err(|e| { + warn!("unable to read Dsx.toml, {e}"); + DsxError::with_context("Unable to read Dsx.toml", ErrorType::IoError) })?; - let meta_contents = - fs::read_to_string(repo_path.join("Package.toml")).map_err(|e| { - warn!("unable to read config for repo, {e}"); - ApiError::InternalServerError(()) - })?; - - let meta: PackageMeta = toml::from_str(&meta_contents).map_err(|e| { - warn!("Invalid meta file for repo! {e}"); - ApiError::InternalServerError(()) + let config: DsxConfig = toml::from_str(&config).map_err(|e| { + warn!("unable to parse Dsx.toml: {e}"); + DsxError::new("Unable to parse Dsx.toml") })?; - let dir = fs::read_dir(repo_path.join("repo")).map_err(|e| { + self.config = Some(config.clone()); + Ok(config) + } + + pub fn get_meta(&mut self) -> Result { + let meta_path = self.path().join(Self::META_PATH); + + let meta = fs::read_to_string(&meta_path).map_err(|e| { + warn!("unable to read Package.toml, {e}"); + DsxError::with_context("Unable to read Package.toml", ErrorType::IoError) + })?; + + let meta: PackageMeta = toml::from_str(&meta).map_err(|e| { + warn!("unable to parse Package.toml: {e}"); + DsxError::new("Unable to parse Package.toml") + })?; + + self.meta = Some(meta.clone()); + Ok(meta) + } + + pub fn set_meta(&mut self, meta: PackageMeta) -> Result<(), DsxError> { + self.meta = Some(meta); + self.save_meta() + } + + fn save_meta(&self) -> Result<(), DsxError> { + let meta_path = self.path().join(Self::META_PATH); + let str = toml::to_string(&self.meta.as_ref().unwrap()).unwrap(); + + fs::write(&meta_path, str).map_err(|e| { + warn!("unable to write Package.toml, {e}"); + DsxError::with_context("Unable to write Package.toml", ErrorType::IoError) + })?; + Ok(()) + } + + pub fn get_languages(&mut self) -> Result, DsxError> { + self.get_meta()?; + + if let Some(languages) = &self.meta.as_ref().unwrap().detected_languages { + return Ok(languages.clone()); + } + + let mut langs = Vec::new(); + + for entry in WalkDir::new(self.path().join("repo/src")) + .into_iter() + .flatten() + { + if entry.file_type().is_file() { + let path = entry.path(); + let extension = path.extension().and_then(|ext| ext.to_str()); + if let Some(ext) = extension { + match ext { + "dsa" | "dsc" => langs.push(ext.to_string()), + _ => {} + } + } + } + } + + self.meta.as_mut().unwrap().detected_languages = Some(langs.clone()); + + Ok(langs) + } + + pub fn get_artifact(&self) -> Result, DsxError> { + println!( + "{}", + self.path() + .join(Self::ARTIFACTS_DIR) + .join("out.dsb") + .display() + ); + + let artifact_path = self.path().join(Self::ARTIFACTS_DIR).join("out.dsb"); + fs::read(artifact_path).map_err(|e| { + warn!("unable to read artifact for repo: {e}"); + DsxError::new("Unable to read artifact") + }) + } + + pub fn file_tree(&self, subpath: impl AsRef) -> Result, DsxError> { + let repo = self.path().join("repo").join(subpath); + + let dir = fs::read_dir(repo).map_err(|e| { warn!("unable to read files for repo, {e}"); - ApiError::InternalServerError(()) + DsxError::new("Unable to read files in repository") })?; let mut files = Vec::new(); + + // for entry in WalkDir::new(repo_path.join("repo")).max_depth(1) { for entry in dir { let entry = entry.map_err(|e| { warn!("unable to read file entry for repo, {e}"); - ApiError::InternalServerError(()) + DsxError::new("Internal error") })?; + + // remove root of DATA_DIR/repos//repo let path = entry.path(); + let metadata = fs::metadata(&path).map_err(|e| { warn!("unable to read file metadata for repo, {e}"); - ApiError::InternalServerError(()) + DsxError::new("Internal error") })?; let is_dir = metadata.is_dir(); let size = metadata.len(); @@ -83,7 +266,11 @@ impl Package { .extension() .map_or(String::new(), |ext| ext.to_string_lossy().to_string()); files.push(FileObj { - path: path.to_string_lossy().to_string(), + path: path + .strip_prefix(self.path().join("repo")) + .unwrap() + .to_string_lossy() + .to_string(), name: path.file_name().unwrap().to_string_lossy().to_string(), is_dir, size, @@ -91,28 +278,7 @@ impl Package { }); } - Ok(Self { - config, - meta, - files, - }) - } - - pub fn unpack(archive: &std::path::Path) -> Result { - let repo_name = archive.file_name().unwrap().to_str().unwrap(); - let dest = DATA_DIR.join("repos").join(repo_name).join("repo"); - unpack_tarball(archive, &dest)?; - let package = Self::load(repo_name)?; - Ok(package) - } - - pub fn tarball(&self) -> Result, ApiError> { - let src_dir = self.path().join("repo"); - pack_tarball(&src_dir) - } - - pub fn path(&self) -> PathBuf { - DATA_DIR.join("repos").join(&self.meta.id) + Ok(files) } } @@ -126,15 +292,21 @@ use tar::Builder; fn unpack_tarball( archive: &std::path::Path, dest: &std::path::Path, -) -> Result<(), ApiError> { - let file = File::open(archive)?; +) -> Result<(), DsxError> { + let file = File::open(archive) + .map_err(|e| DsxError::with_context(e.to_string(), ErrorType::IoError))?; + + fs::create_dir_all(dest)?; + let gz = GzDecoder::new(file); let mut tar = tar::Archive::new(gz); - tar.unpack(dest)?; + tar.unpack(dest) + .map_err(|e| DsxError::with_context(e.to_string(), ErrorType::TarError))?; + Ok(()) } -fn pack_tarball(src_dir: &std::path::Path) -> Result, ApiError> { +fn pack_tarball(src_dir: &std::path::Path) -> Result, DsxError> { let buf = Vec::new(); let gz = GzEncoder::new(buf, Compression::default()); let mut tar = Builder::new(gz); diff --git a/dsx/dsx_server/src/routes/api.rs b/dsx/dsx_server/src/routes/api.rs new file mode 100644 index 0000000..a02575b --- /dev/null +++ b/dsx/dsx_server/src/routes/api.rs @@ -0,0 +1,119 @@ +use std::fs; + +use dsx_common::config::DsxConfig; +use rocket::{Data, data::ToByteUnit, serde::json::Json}; +use serde::Deserialize; + +use crate::{ + error::ApiError, + model::{DATA_DIR, PackageHandle, PackageMeta}, +}; + +#[derive(Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct NewRepo<'r> { + name: &'r str, +} + +// Create repo +#[post("/pkg", data = "")] +pub fn create_repo(repo: Json>) -> Result<(), &'static str> { + let path = DATA_DIR.join("repos").join(repo.name); + + if repo.name.is_empty() { + return Err("Repository name cannot be empty!"); + } + + if path.exists() { + tracing::info!( + "Attempt to create repository '{}' which already exists.", + repo.name + ); + return Err("This repository already exists!"); + } + + if let Err(e) = fs::create_dir_all(path) { + tracing::error!( + "Attempted to create package with name {} - Error: {e},", + repo.name + ); + return Err("Internal server error"); + } + + Ok(()) +} + +// Repo status/metadata +#[get("/pkg/")] +pub fn get_pkg(id: &str) -> Result, ApiError> { + let mut handle = PackageHandle::new(id); + let meta = handle.get_meta()?; + let config = handle.get_config()?; + Ok(Json((meta, config))) +} + +// Upload source tarball +#[post("/pkg//push", data = "")] +pub async fn push_tarball(id: &str, data: Data<'_>) -> Result<(), ApiError> { + let mut handle = PackageHandle::new(id); + let repo = handle.path(); + + let tmp_path = repo.join("upload.tar.gz"); + let stream = data + .open(256.mebibytes()) + .into_file(&tmp_path) + .await + .map_err(|e| ApiError::ServerError(e.to_string()))?; + + if !stream.is_complete() { + return Err(ApiError::BadRequest("Incomplete upload".to_string())); + } + + // Unpack over the existing repo dir. + // if the repo is already deleted that's fine so ignore err + if handle.path().exists() { + let _ = fs::remove_dir_all(repo.join("repo")); + } + + handle.unpack(&tmp_path)?; + fs::remove_file(&tmp_path).ok(); + + // we don't care if there's an error in the build. + let _ = handle.build(); + + Ok(()) +} + +// Download source tarball +#[get("/pkg//pull")] +pub fn pull_tarball( + id: &str, +) -> Result<(rocket::http::ContentType, Vec), Json> { + if let Ok(tarball) = PackageHandle::new(id).tarball() { + Ok(( + rocket::http::ContentType::new("application", "octet-stream"), + tarball, + )) + } else { + Err(Json(ApiError::NotFound(format!( + "repo with id {id} does not exist" + )))) + } +} + +// Download compiled binary +#[get("/pkg//artifact")] +pub fn download_artifact( + id: &str, +) -> Result<(rocket::http::ContentType, Vec), Json> { + if let Ok(artifact) = PackageHandle::new(id).get_artifact() { + Ok(( + rocket::http::ContentType::new("application", "octet-stream"), + artifact, + )) + } else { + Err(Json(ApiError::NotFound(format!( + "repo with id {id} does not have a valid artifact" + )))) + } +} diff --git a/dsx/dsx_server/src/routes/mod.rs b/dsx/dsx_server/src/routes/mod.rs new file mode 100644 index 0000000..22bfe40 --- /dev/null +++ b/dsx/dsx_server/src/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod pages; diff --git a/dsx/dsx_server/src/routes/pages.rs b/dsx/dsx_server/src/routes/pages.rs new file mode 100644 index 0000000..d3ca360 --- /dev/null +++ b/dsx/dsx_server/src/routes/pages.rs @@ -0,0 +1,145 @@ +use std::{fs, path::Path}; + +use dsx_common::config::DsxConfig; +use rocket_dyn_templates::{Template, context}; +use serde::Serialize; + +use crate::{ + error::ApiError, + model::{DATA_DIR, PackageHandle}, +}; + +// Search for a package +#[get("/?")] +pub fn search_packages(q: Option) -> Result { + #[derive(Serialize)] + struct Package { + name: String, + description: String, + updated_at: String, + } + + let mut packages = Vec::new(); + + let dir = match fs::read_dir(DATA_DIR.join("repos")) { + Ok(dir) => dir, + Err(e) => { + warn!("failed to read repos directory: {}", e); + return Err(ApiError::InternalServerError(())); + } + }; + + for entry in dir { + let entry = entry.map_err(|e| { + warn!("failed to read entry: {}", e); + ApiError::InternalServerError(()) + })?; + + let config_path = entry.path().join("repo").join("Dsx.toml"); + if config_path.exists() { + let text = fs::read_to_string(&config_path).map_err(|e| { + warn!("failed to read config file: {}", e); + ApiError::InternalServerError(()) + })?; + let config: DsxConfig = toml::from_str(&text).map_err(|e| { + warn!("failed to parse config file: {}", e); + ApiError::InternalServerError(()) + })?; + + error!("{}", config.description.clone().unwrap_or_default()); + + // skip repo if it doesnt match query params + if let Some(query) = &q + && !(config.name.contains(query) + || config + .description + .clone() + .unwrap_or_default() + .contains(query)) + { + continue; + } + + packages.push(Package { + name: config.name, + description: config.description.unwrap_or_default(), + updated_at: String::from("0:00"), + }) + } + } + + Ok(Template::render( + "packages", + context! { + packages, + query: q.clone().unwrap_or_default() + }, + )) +} + +// Main page for a repository, shows status, files, name etc. +#[get("/")] +pub fn package_main(id: &str) -> Result { + // get package info + + let mut handle = PackageHandle::new(id); + let meta = handle.get_meta()?; + let config = handle.get_config()?; + let files = handle.file_tree("")?; + + let parent_path: Option = None; + let current_path: Option = None; + + Ok(Template::render( + "package_home", + context! { + parent_path, + current_path, + meta: meta, + config: config, + files: files, + }, + )) +} + +// Path for a file within a repo +#[get("//~repo/", rank = 1)] +pub fn repo_file(id: &str, path: std::path::PathBuf) -> Result { + // get package info + let mut handle = PackageHandle::new(id); + let meta = handle.get_meta()?; + let config = handle.get_config()?; + let files = handle.file_tree(&path)?; + + let parent_path = path.parent().unwrap_or(Path::new("")); + let current_path = &path; + + Ok(Template::render( + "package_home", + context! { + parent_path, + current_path, + meta: meta, + config: config, + files: files, + }, + )) +} + +// Search within a package's files +#[get("//~repo?", rank = 2)] +pub fn search_repo_files(name: &str, q: Option) -> String { + format!("Search within {} for {:?}", name, q) +} + +// Page listing repo artifacts by date +#[get("//artifacts")] +pub fn list_artifacts(name: &str) -> String { + format!("Artifacts for package {}", name) +} + +// Page for a specific artifact and status/logs +#[get("//artifacts/")] +pub fn artifact_detail(name: &str, id: u64) -> String { + format!("Artifact {} details for package {}", id, name) +} diff --git a/dsx/dsx_server/templates/components/file_tree.html.tera b/dsx/dsx_server/templates/components/file_tree.html.tera index 1757b06..0b3d3c5 100644 --- a/dsx/dsx_server/templates/components/file_tree.html.tera +++ b/dsx/dsx_server/templates/components/file_tree.html.tera @@ -4,17 +4,17 @@
{{ current_path | default(value="/") }}
- {% if package.files and package.files | length > 0 %} + {% if files and files | length > 0 %} {% else %} diff --git a/dsx/dsx_server/templates/components/sidebar.html.tera b/dsx/dsx_server/templates/components/sidebar.html.tera index 21e300c..aaea47f 100644 --- a/dsx/dsx_server/templates/components/sidebar.html.tera +++ b/dsx/dsx_server/templates/components/sidebar.html.tera @@ -6,25 +6,28 @@ @@ -67,11 +70,11 @@