From ab2d74cc4cf2ef3e7229c76e955c3e4ae2909e50 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Thu, 13 Feb 2025 14:39:06 +0100 Subject: [PATCH 01/12] CLI should use Agent in requests - get #986 --- CHANGELOG.md | 4 ++++ cli/src/main.rs | 6 ++++++ cli/src/path.rs | 4 +++- lib/src/agents.rs | 13 +++++++++++++ lib/src/client/helpers.rs | 33 ++++++++++++++++++++++++--------- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55defc6a0..f74841dc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ By far most changes relate to `atomic-server`, so if not specified, assume the c **Changes to JS assets (including the front-end and JS libraries) are not shown here**, but in [`/browser/CHANGELOG`](/browser/CHANGELOG.md). See [STATUS.md](server/STATUS.md) to learn more about which features will remain stable. +## UNRELEASED + +- CLI should use Agent in requests - get #986 + ## [v0.40.2] - fix property sort order when importing + add tests #980 diff --git a/cli/src/main.rs b/cli/src/main.rs index 756432f5c..646828705 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -104,6 +104,8 @@ enum Commands { /// Validates the store #[command(hide = true)] Validate, + /// Print the current agent + Agent, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] @@ -263,6 +265,10 @@ fn exec_command(context: &mut Context) -> AtomicResult<()> { Commands::Validate => { validate(context); } + Commands::Agent => { + let agent = context.read_config(); + println!("{}", agent.agent); + } }; Ok(()) } diff --git a/cli/src/path.rs b/cli/src/path.rs index 9d7a5fdce..1b824a6d3 100644 --- a/cli/src/path.rs +++ b/cli/src/path.rs @@ -10,12 +10,14 @@ pub fn get_path( // let subcommand_matches = context.matches.subcommand_matches("get").unwrap(); let path_string: String = path_vec.join(" "); + context.read_config(); + // Returns a URL or Value let store = &mut context.store; let path = store.get_path( &path_string, Some(&context.mapping.lock().unwrap()), - &ForAgent::Sudo, + &store.get_default_agent()?.into(), )?; let out = match path { storelike::PathReturn::Subject(subject) => { diff --git a/lib/src/agents.rs b/lib/src/agents.rs index fcf5facae..3631527bf 100644 --- a/lib/src/agents.rs +++ b/lib/src/agents.rs @@ -211,6 +211,19 @@ pub fn verify_public_key(public_key: &str) -> AtomicResult<()> { Ok(()) } +impl From for ForAgent { + fn from(agent: Agent) -> Self { + agent.subject.into() + } +} + +impl<'a> From<&'a Agent> for ForAgent { + fn from(agent: &'a Agent) -> Self { + let subject: String = agent.subject.clone(); + subject.into() + } +} + #[cfg(test)] mod test { #[cfg(test)] diff --git a/lib/src/client/helpers.rs b/lib/src/client/helpers.rs index 5122cf2d3..ec9c8a96c 100644 --- a/lib/src/client/helpers.rs +++ b/lib/src/client/helpers.rs @@ -54,18 +54,33 @@ pub fn fetch_body( if !url.starts_with("http") { return Err(format!("Could not fetch url '{}', must start with http.", url).into()); } - if let Some(agent) = client_agent { - get_authentication_headers(url, agent)?; - } - let agent = ureq::builder() + let client = ureq::builder() .timeout(std::time::Duration::from_secs(2)) .build(); - let resp = agent - .get(url) - .set("Accept", content_type) - .call() - .map_err(|e| format!("Error when server tried fetching {} : {}", url, e))?; + + let mut req = client.get(url); + if let Some(agent) = client_agent { + let headers = get_authentication_headers(url, agent)?; + for (key, value) in headers { + req = req.set(key.as_str(), value.as_str()); + } + } + + let resp = match req.set("Accept", content_type).call() { + Ok(response) => response, + Err(ureq::Error::Status(status, response)) => { + let body = response + .into_string() + .unwrap_or_else(|_| "".to_string()); + return Err(format!( + "Error when server tried fetching {}: Status: {}. Body: {}", + url, status, body + ) + .into()); + } + Err(e) => return Err(format!("Error when server tried fetching {}: {}", url, e).into()), + }; let status = resp.status(); let body = resp .into_string() From f91abe03d9bcbfc3ee0a2f51a54318e67c9263ea Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Thu, 13 Feb 2025 17:28:28 +0100 Subject: [PATCH 02/12] Make img processing optional #1044 --- Cargo.lock | 14 ------- server/Cargo.toml | 14 +++++-- server/src/handlers/download.rs | 68 ++++++--------------------------- server/src/handlers/image.rs | 53 +++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 75 deletions(-) create mode 100644 server/src/handlers/image.rs diff --git a/Cargo.lock b/Cargo.lock index 1ede40ce3..f285cf475 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -595,7 +595,6 @@ dependencies = [ "opentelemetry-jaeger", "percent-encoding", "rand 0.8.5", - "ravif", "rcgen", "regex", "rio_api", @@ -2412,7 +2411,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", - "rayon", ] [[package]] @@ -2515,15 +2513,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" -[[package]] -name = "nasm-rs" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4d98d0065f4b1daf164b3eafb11974c94662e5e2396cf03f32d0bb5c17da51" -dependencies = [ - "rayon", -] - [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -3324,7 +3313,6 @@ dependencies = [ "av1-grain", "bitstream-io", "built", - "cc", "cfg-if", "interpolate_name", "itertools 0.12.1", @@ -3332,7 +3320,6 @@ dependencies = [ "libfuzzer-sys", "log", "maybe-rayon", - "nasm-rs", "new_debug_unreachable", "noop_proc_macro", "num-derive", @@ -3360,7 +3347,6 @@ dependencies = [ "loop9", "quick-error", "rav1e", - "rayon", "rgb", ] diff --git a/server/Cargo.toml b/server/Cargo.toml index bebe64467..8672c3678 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -52,9 +52,6 @@ tracing-chrome = "0.7" tracing-log = "0.2" ureq = "2" urlencoding = "2" -image = "0.25.2" -webp = "0.3" -ravif = "0.11.8" [dependencies.instant-acme] optional = true @@ -111,6 +108,14 @@ version = "1" features = ["time"] version = "1" +[dependencies.image] +optional = true +version = "0.25.2" + +[dependencies.webp] +optional = true +version = "0.3" + [dependencies.tracing-subscriber] features = ["env-filter"] version = "0.3" @@ -120,9 +125,10 @@ actix-rt = "2" assert_cmd = "2" [features] -default = ["https", "telemetry"] +default = ["https", "telemetry", "img"] https = ["rustls", "instant-acme", "rcgen", "rustls-pemfile"] telemetry = ["tracing-opentelemetry", "opentelemetry", "opentelemetry-jaeger"] +img = ["webp", "image"] [lib] name = "atomic_server_lib" diff --git a/server/src/handlers/download.rs b/server/src/handlers/download.rs index 71d2a2ca3..09b8d0ac8 100644 --- a/server/src/handlers/download.rs +++ b/server/src/handlers/download.rs @@ -2,11 +2,13 @@ use crate::{appstate::AppState, errors::AtomicServerResult, helpers::get_client_ use actix_files::NamedFile; use actix_web::{web, HttpRequest, HttpResponse}; use atomic_lib::{urls, Resource, Storelike}; -use image::GenericImageView; -use image::{codecs::avif::AvifEncoder, ImageReader}; + use serde::Deserialize; use std::{collections::HashSet, io::Write, path::PathBuf}; +#[cfg(feature = "image")] +use crate::handlers::image::{is_image, process_image}; + #[serde_with::serde_as] #[serde_with::skip_serializing_none] #[derive(Deserialize, Debug)] @@ -71,12 +73,15 @@ pub fn download_file_handler_partial( return Ok(file.into_response(req)); } - if !is_image(&file_path) { - return Err("Quality or with parameter are not supported for non image files".into()); + // only if image feature flag is on + #[cfg(feature = "image")] + { + if !is_image(&file_path) { + return Err("Quality or with parameter are not supported for non image files".into()); + } + process_image(&file_path, &processed_file_path, params)?; } - process_image(&file_path, &processed_file_path, params)?; - let file = NamedFile::open(processed_file_path)?; Ok(file.into_response(req)) } @@ -110,57 +115,6 @@ pub fn build_prossesed_file_path( Ok(processed_file_path) } -fn is_image(file_path: &PathBuf) -> bool { - if let Ok(img) = image::open(file_path) { - return img.dimensions() > (0, 0); - } - false -} - -fn process_image( - file_path: &PathBuf, - new_path: &PathBuf, - params: &DownloadParams, -) -> AtomicServerResult<()> { - let format = get_format(params)?; - let quality = params.q.unwrap_or(100.0).clamp(0.0, 100.0); - - let mut img = ImageReader::open(file_path)? - .with_guessed_format()? - .decode() - .map_err(|e| format!("Failed to decode image: {}", e))?; - - if let Some(width) = ¶ms.w { - if *width < img.dimensions().0 { - img = img.resize(*width, 10000, image::imageops::FilterType::Lanczos3); - } - } - - if format == "webp" { - let encoder = webp::Encoder::from_image(&img)?; - let webp_image = match params.q { - Some(quality) => encoder.encode(quality), - None => encoder.encode(75.0), - }; - - let mut file = std::fs::File::create(new_path)?; - file.write_all(&webp_image)?; - - return Ok(()); - } - - if format == "avif" { - let mut file = std::fs::File::create(new_path)?; - let encoder = AvifEncoder::new_with_speed_quality(&mut file, 8, quality as u8); - img.write_with_encoder(encoder) - .map_err(|e| format!("Failed to encode image: {}", e))?; - - return Ok(()); - } - - Err(format!("Unsupported format: {}", format).into()) -} - fn create_processed_folder_if_not_exists(base_path: &PathBuf) -> AtomicServerResult<()> { let mut processed_folder = base_path.clone(); processed_folder.push("processed"); diff --git a/server/src/handlers/image.rs b/server/src/handlers/image.rs new file mode 100644 index 000000000..ad75e3df9 --- /dev/null +++ b/server/src/handlers/image.rs @@ -0,0 +1,53 @@ +use image::GenericImageView; +use image::{codecs::avif::AvifEncoder, ImageReader}; + +pub fn is_image(file_path: &PathBuf) -> bool { + if let Ok(img) = image::open(file_path) { + return img.dimensions() > (0, 0); + } + false +} + +pub fn process_image( + file_path: &PathBuf, + new_path: &PathBuf, + params: &DownloadParams, +) -> AtomicServerResult<()> { + let format = get_format(params)?; + let quality = params.q.unwrap_or(100.0).clamp(0.0, 100.0); + + let mut img = ImageReader::open(file_path)? + .with_guessed_format()? + .decode() + .map_err(|e| format!("Failed to decode image: {}", e))?; + + if let Some(width) = ¶ms.w { + if *width < img.dimensions().0 { + img = img.resize(*width, 10000, image::imageops::FilterType::Lanczos3); + } + } + + if format == "webp" { + let encoder = webp::Encoder::from_image(&img)?; + let webp_image = match params.q { + Some(quality) => encoder.encode(quality), + None => encoder.encode(75.0), + }; + + let mut file = std::fs::File::create(new_path)?; + file.write_all(&webp_image)?; + + return Ok(()); + } + + if format == "avif" { + let mut file = std::fs::File::create(new_path)?; + let encoder = AvifEncoder::new_with_speed_quality(&mut file, 8, quality as u8); + img.write_with_encoder(encoder) + .map_err(|e| format!("Failed to encode image: {}", e))?; + + return Ok(()); + } + + Err(format!("Unsupported format: {}", format).into()) +} From 0950cf761aab779e8b3637157fe8adb7eeebe8a1 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Tue, 18 Feb 2025 13:53:12 +0100 Subject: [PATCH 03/12] Search endpoint doesn't work over websockets, should err #1047 --- lib/src/plugins/search.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/src/plugins/search.rs b/lib/src/plugins/search.rs index 00ca4b65b..ce3cf8a8a 100644 --- a/lib/src/plugins/search.rs +++ b/lib/src/plugins/search.rs @@ -1,4 +1,8 @@ -use crate::{endpoints::Endpoint, urls}; +use crate::{ + endpoints::{Endpoint, HandleGetContext}, + errors::AtomicResult, + urls, Resource, +}; // Note that the actual logic of this endpoint resides in `atomic-server`, as it depends on the Actix runtime. pub fn search_endpoint() -> Endpoint { @@ -11,7 +15,22 @@ pub fn search_endpoint() -> Endpoint { ], description: "Full text-search endpoint. You can use the keyword `AND` and `OR`, or use `\"` for advanced searches. ".to_string(), shortname: "search".to_string(), - handle: None, + handle: Some(handle_search), handle_post: None, } } + +fn handle_search(context: HandleGetContext) -> AtomicResult { + let HandleGetContext { + subject, + store, + for_agent: _for_agent, + } = context; + let params = subject.query_pairs(); + if params.into_iter().next().is_none() { + return search_endpoint().to_resource(store); + } + return Err( + "Search endpoint is only available through HTTP requests, not through webhooks".into(), + ); +} From bf4f89029800f0aeddd22395e783ba79931f8c6f Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Tue, 18 Feb 2025 13:54:34 +0100 Subject: [PATCH 04/12] - Fix search in CLI / atomic_lib #958 --- CHANGELOG.md | 2 ++ cli/src/get.rs | 16 ++++++++++++++ cli/src/main.rs | 25 +++++++++++++++------- cli/src/path.rs | 38 --------------------------------- cli/src/search.rs | 34 ++++++++++++++--------------- lib/defaults/default_store.json | 2 +- lib/src/agents.rs | 12 +++++------ lib/src/client/helpers.rs | 4 ++-- lib/src/client/search.rs | 5 +++++ lib/src/collections.rs | 12 +++++------ lib/src/commit.rs | 4 ++-- lib/src/parse.rs | 6 +++--- lib/src/plugins/importer.rs | 8 ++----- lib/src/populate.rs | 11 +++++----- lib/src/resources.rs | 26 +++++++++++----------- lib/src/store.rs | 20 ++++++++++++----- lib/src/storelike.rs | 26 +++++++++++++++++++++- lib/src/values.rs | 12 +++++++++++ server/build.rs | 2 +- server/src/handlers/download.rs | 9 ++++---- server/src/handlers/image.rs | 9 +++++++- server/src/handlers/mod.rs | 2 ++ 22 files changed, 164 insertions(+), 121 deletions(-) create mode 100644 cli/src/get.rs delete mode 100644 cli/src/path.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f74841dc3..a2f4141f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain ## UNRELEASED - CLI should use Agent in requests - get #986 +- Search endpoint throws error for websocket requests #1047 +- Fix search in CLI / atomic_lib #958 ## [v0.40.2] diff --git a/cli/src/get.rs b/cli/src/get.rs new file mode 100644 index 000000000..d1511c324 --- /dev/null +++ b/cli/src/get.rs @@ -0,0 +1,16 @@ +use crate::{print::print_resource, Context, SerializeOptions}; +use atomic_lib::{errors::AtomicResult, Storelike}; + +pub fn get_resource( + context: &mut Context, + subject: &str, + serialize: &SerializeOptions, +) -> AtomicResult<()> { + context.read_config(); + + let store = &mut context.store; + let fetched = store.fetch_resource(subject, store.get_default_agent().ok().as_ref())?; + print_resource(context, &fetched, serialize)?; + + Ok(()) +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 646828705..5e7a60ddf 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -8,8 +8,8 @@ use dirs::home_dir; use std::{cell::RefCell, path::PathBuf, sync::Mutex}; mod commit; +mod get; mod new; -mod path; mod print; mod search; @@ -45,9 +45,9 @@ enum Commands { Visit https://docs.atomicdata.dev/core/paths.html for more info about paths. \ ")] Get { - /// The subject URL, shortname or path to be fetched - #[arg(required = true, num_args = 1..)] - path: Vec, + /// The subject URL + #[arg(required = true)] + subject: String, /// Serialization format #[arg(long, value_enum, default_value = "pretty")] @@ -98,6 +98,12 @@ enum Commands { /// The search query #[arg(required = true)] query: String, + /// Subject URL of the parent Resource to filter by + #[arg(long)] + parent: Option, + /// Serialization format + #[arg(long, value_enum, default_value = "pretty")] + as_: SerializeOptions, }, /// List all bookmarks List, @@ -112,6 +118,7 @@ enum Commands { pub enum SerializeOptions { Pretty, Json, + JsonAd, NTriples, } @@ -120,6 +127,7 @@ impl Into for SerializeOptions { match self { SerializeOptions::Pretty => Format::Pretty, SerializeOptions::Json => Format::Json, + SerializeOptions::JsonAd => Format::JsonAd, SerializeOptions::NTriples => Format::NTriples, } } @@ -153,6 +161,7 @@ impl Context { name: None, public_key: generate_public_key(&write_ctx.private_key).public, }); + self.store.set_server_url(&write_ctx.server); write_ctx } } @@ -240,8 +249,8 @@ fn exec_command(context: &mut Context) -> AtomicResult<()> { return Err("Feature not available. Compile with `native` feature.".into()); } } - Commands::Get { path, as_ } => { - path::get_path(context, &path, &as_)?; + Commands::Get { subject, as_ } => { + get::get_resource(context, &subject, &as_)?; } Commands::List => { list(context); @@ -259,8 +268,8 @@ fn exec_command(context: &mut Context) -> AtomicResult<()> { } => { commit::set(context, &subject, &property, &value)?; } - Commands::Search { query } => { - search::search(context, query)?; + Commands::Search { query, parent, as_ } => { + search::search(context, query, parent, &as_)?; } Commands::Validate => { validate(context); diff --git a/cli/src/path.rs b/cli/src/path.rs deleted file mode 100644 index 1b824a6d3..000000000 --- a/cli/src/path.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::{print::print_resource, Context, SerializeOptions}; -use atomic_lib::{agents::ForAgent, errors::AtomicResult, serialize, storelike, Atom, Storelike}; - -/// Resolves an Atomic Path query -pub fn get_path( - context: &mut Context, - path_vec: &Vec, - serialize: &SerializeOptions, -) -> AtomicResult<()> { - // let subcommand_matches = context.matches.subcommand_matches("get").unwrap(); - let path_string: String = path_vec.join(" "); - - context.read_config(); - - // Returns a URL or Value - let store = &mut context.store; - let path = store.get_path( - &path_string, - Some(&context.mapping.lock().unwrap()), - &store.get_default_agent()?.into(), - )?; - let out = match path { - storelike::PathReturn::Subject(subject) => { - let resource = store.get_resource_extended(&subject, false, &ForAgent::Sudo)?; - print_resource(context, &resource, serialize)?; - return Ok(()); - } - storelike::PathReturn::Atom(atom) => match serialize { - SerializeOptions::NTriples => { - let atoms: Vec = vec![*atom]; - serialize::atoms_to_ntriples(atoms, store)? - } - _other => atom.value.to_string(), - }, - }; - println!("{}", out); - Ok(()) -} diff --git a/cli/src/search.rs b/cli/src/search.rs index b8c774093..81b4d9008 100644 --- a/cli/src/search.rs +++ b/cli/src/search.rs @@ -1,29 +1,27 @@ -use atomic_lib::{errors::AtomicResult, urls, Storelike}; +use atomic_lib::{errors::AtomicResult, Storelike}; -pub fn search(context: &crate::Context, query: String) -> AtomicResult<()> { +use crate::print::print_resource; + +pub fn search( + context: &crate::Context, + query: String, + parent: Option, + serialize: &crate::SerializeOptions, +) -> AtomicResult<()> { + context.read_config(); let opts = atomic_lib::client::search::SearchOpts { limit: Some(10), include: Some(true), + parents: Some(vec![parent.unwrap_or_default()]), ..Default::default() }; - let subject = atomic_lib::client::search::build_search_subject( - &context.read_config().server, - &query, - opts, - ); - let resource = context.store.get_resource(&subject)?; - let members = resource - .get(urls::ENDPOINT_RESULTS) - .expect("No members?") - .to_subjects(None) - .unwrap(); - if members.is_empty() { - println!("No results found."); - println!("URL: {}", subject); + let resources = context.store.search(&query, opts)?; + if resources.is_empty() { + println!("No results found for query: {}", query); return Ok(()); } else { - for member in members { - println!("{}", member); + for member in resources { + print_resource(context, &member, serialize)?; } } Ok(()) diff --git a/lib/defaults/default_store.json b/lib/defaults/default_store.json index d2e25c1b1..13d2d6e14 100644 --- a/lib/defaults/default_store.json +++ b/lib/defaults/default_store.json @@ -710,7 +710,7 @@ }, { "@id": "https://atomicdata.dev/properties/published-at", - "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/timestamp", + "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/date", "https://atomicdata.dev/properties/description": "DateTime at which an item is made public.", "https://atomicdata.dev/properties/isA": [ "https://atomicdata.dev/classes/Property" diff --git a/lib/src/agents.rs b/lib/src/agents.rs index 3631527bf..e9b1d0f20 100644 --- a/lib/src/agents.rs +++ b/lib/src/agents.rs @@ -85,7 +85,7 @@ impl Agent { pub fn new(name: Option<&str>, store: &impl Storelike) -> AtomicResult { let keypair = generate_keypair()?; - Ok(Agent::new_from_private_key(name, store, &keypair.private)) + Agent::new_from_private_key(name, store, &keypair.private) } /// Creates a new Agent on this server, using the server's Server URL. @@ -94,16 +94,16 @@ impl Agent { name: Option<&str>, store: &impl Storelike, private_key: &str, - ) -> Agent { + ) -> AtomicResult { let keypair = generate_public_key(private_key); - Agent { + Ok(Agent { private_key: Some(keypair.private), public_key: keypair.public.clone(), - subject: format!("{}/agents/{}", store.get_server_url(), keypair.public), + subject: format!("{}/agents/{}", store.get_server_url()?, keypair.public), name: name.map(|x| x.to_owned()), created_at: crate::utils::now(), - } + }) } /// Creates a new Agent on this server, using the server's Server URL. @@ -114,7 +114,7 @@ impl Agent { Ok(Agent { private_key: None, public_key: public_key.into(), - subject: format!("{}/agents/{}", store.get_server_url(), public_key), + subject: format!("{}/agents/{}", store.get_server_url()?, public_key), name: None, created_at: crate::utils::now(), }) diff --git a/lib/src/client/helpers.rs b/lib/src/client/helpers.rs index ec9c8a96c..c1d221863 100644 --- a/lib/src/client/helpers.rs +++ b/lib/src/client/helpers.rs @@ -74,12 +74,12 @@ pub fn fetch_body( .into_string() .unwrap_or_else(|_| "".to_string()); return Err(format!( - "Error when server tried fetching {}: Status: {}. Body: {}", + "Error when fetching {}: Status: {}. Body: {}", url, status, body ) .into()); } - Err(e) => return Err(format!("Error when server tried fetching {}: {}", url, e).into()), + Err(e) => return Err(format!("Error when fetching {}: {}", url, e).into()), }; let status = resp.status(); let body = resp diff --git a/lib/src/client/search.rs b/lib/src/client/search.rs index de627facb..8dc4c620a 100644 --- a/lib/src/client/search.rs +++ b/lib/src/client/search.rs @@ -7,6 +7,8 @@ Use the `/search` endpoint from AtomicServer to perform full-text search. use std::collections::HashMap; use url::Url; +use crate::agents::Agent; + // Define the SearchOpts struct with optional fields #[derive(Debug, Default)] pub struct SearchOpts { @@ -14,6 +16,8 @@ pub struct SearchOpts { pub limit: Option, pub parents: Option>, pub filters: Option>, + /// The agent to use for authentication + pub agent: Option, } // Function to build the base URL for search @@ -126,6 +130,7 @@ mod tests { filters }), parents: Some(vec!["https://test.com/parent".to_string()]), + agent: None, }; let expected_search_url = "https://test.com/search?q=test&include=true&limit=30&filters=age%3A%2210%22&parents=https%3A%2F%2Ftest.com%2Fparent"; assert_eq!( diff --git a/lib/src/collections.rs b/lib/src/collections.rs index fa52713c0..d906a237c 100644 --- a/lib/src/collections.rs +++ b/lib/src/collections.rs @@ -86,9 +86,9 @@ impl CollectionBuilder { class_url: &str, path: &str, store: &impl Storelike, - ) -> CollectionBuilder { - CollectionBuilder { - subject: format!("{}/{}", store.get_server_url(), path), + ) -> AtomicResult { + Ok(CollectionBuilder { + subject: format!("{}/{}", store.get_server_url()?, path), property: Some(urls::IS_A.into()), value: Some(class_url.into()), sort_by: None, @@ -98,7 +98,7 @@ impl CollectionBuilder { name: Some(format!("{} collection", path)), include_nested: true, include_external: false, - } + }) } /// Converts the CollectionBuilder into a collection, with Members @@ -394,7 +394,7 @@ pub fn create_collection_resource_for_class( other => format!("{}s", other), }; - let mut collection = CollectionBuilder::class_collection(&class.subject, &pluralized, store); + let mut collection = CollectionBuilder::class_collection(&class.subject, &pluralized, store)?; collection.sort_by = match class_subject { urls::COMMIT => Some(urls::CREATED_AT.to_string()), @@ -524,7 +524,7 @@ mod test { println!("{:?}", subjects); let collections_collection = store .get_resource_extended( - &format!("{}/collections", store.get_server_url()), + &format!("{}/collections", store.get_server_url().unwrap()), false, &ForAgent::Public, ) diff --git a/lib/src/commit.rs b/lib/src/commit.rs index 9470bdf0d..9ff8eef2f 100644 --- a/lib/src/commit.rs +++ b/lib/src/commit.rs @@ -429,10 +429,10 @@ impl Commit { #[tracing::instrument(skip(store))] pub fn into_resource(&self, store: &impl Storelike) -> AtomicResult { let commit_subject = match self.signature.as_ref() { - Some(sig) => format!("{}/commits/{}", store.get_server_url(), sig), + Some(sig) => format!("{}/commits/{}", store.get_server_url()?, sig), None => { let now = crate::utils::now(); - format!("{}/commitsUnsigned/{}", store.get_server_url(), now) + format!("{}/commitsUnsigned/{}", store.get_server_url()?, now) } }; let mut resource = Resource::new_instance(urls::COMMIT, store)?; diff --git a/lib/src/parse.rs b/lib/src/parse.rs index b746504e6..8fcfd0d6a 100644 --- a/lib/src/parse.rs +++ b/lib/src/parse.rs @@ -221,7 +221,7 @@ pub fn parse_json_ad_commit_resource( .get(urls::SUBJECT) .ok_or("No subject field in Commit.")? .to_string(); - let subject = format!("{}/commits/{}", store.get_server_url(), signature); + let subject = format!("{}/commits/{}", store.get_server_url()?, signature); let mut resource = Resource::new(subject); let propvals = match parse_json_ad_map_to_resource(json, store, &ParseOpts::default())? { SubResource::Resource(r) => r.into_propvals(), @@ -303,7 +303,7 @@ fn parse_json_ad_map_to_resource( } serde_json::Value::String(str) => { // LocalIDs are mapped to @ids by appending the `localId` to the `importer`'s `parent`. - if prop == urls::LOCAL_ID { + if prop == urls::LOCAL_ID && parse_opts.importer.is_some() { let parent = parse_opts.importer.as_ref() .ok_or_else(|| AtomicError::parse_error( "Encountered `localId`, which means we need a `parent` in the parsing options.", @@ -672,7 +672,7 @@ mod test { // Try to overwrite the main drive with some malicious data let agent = store.get_default_agent().unwrap(); - let mut resource = Resource::new_generate_subject(&store); + let mut resource = Resource::new_generate_subject(&store).unwrap(); resource .set( urls::WRITE.into(), diff --git a/lib/src/plugins/importer.rs b/lib/src/plugins/importer.rs index f72e6e053..e333e9295 100644 --- a/lib/src/plugins/importer.rs +++ b/lib/src/plugins/importer.rs @@ -4,7 +4,7 @@ Importers allow users to (periodically) import JSON-AD files from a remote sourc use crate::{ agents::ForAgent, - endpoints::{Endpoint, HandleGetContext, HandlePostContext}, + endpoints::{Endpoint, HandlePostContext}, errors::AtomicResult, urls, Resource, Storelike, }; @@ -20,15 +20,11 @@ pub fn import_endpoint() -> Endpoint { description: "Imports one or more Resources to some parent. POST your JSON-AD and add a `parent` query param to the URL. See https://docs.atomicdata.dev/create-json-ad.html".to_string(), shortname: "path".to_string(), // Not sure if we need this, or if we should derive it from `None` here. - handle: Some(handle_get), + handle: None, handle_post: Some(handle_post), } } -pub fn handle_get(context: HandleGetContext) -> AtomicResult { - import_endpoint().to_resource(context.store) -} - /// When an importer is shown, we list a bunch of Parameters and a list of previously imported items. #[tracing::instrument] pub fn handle_post(context: HandlePostContext) -> AtomicResult { diff --git a/lib/src/populate.rs b/lib/src/populate.rs index 829850830..a33f54e0d 100644 --- a/lib/src/populate.rs +++ b/lib/src/populate.rs @@ -160,7 +160,7 @@ pub fn create_drive(store: &impl Storelike) -> AtomicResult<()> { .ok_or("No self_url set, cannot populate store with Drive")?; let mut drive = store.get_resource_new(&self_url); drive.set_class(urls::DRIVE); - let server_url = url::Url::parse(store.get_server_url())?; + let server_url = url::Url::parse(&store.get_server_url()?)?; drive.set_string( urls::NAME.into(), server_url.host_str().ok_or("Can't use current base URL")?, @@ -172,7 +172,8 @@ pub fn create_drive(store: &impl Storelike) -> AtomicResult<()> { } pub fn create_default_ontology(store: &impl Storelike) -> AtomicResult<()> { - let mut drive = store.get_resource(store.get_server_url())?; + let server_url = store.get_server_url()?; + let mut drive = store.get_resource(&server_url).unwrap(); let ontology_subject = format!("{}/{}", drive.get_subject(), DEFAULT_ONTOLOGY_PATH); @@ -209,7 +210,7 @@ pub fn create_default_ontology(store: &impl Storelike) -> AtomicResult<()> { /// Adds rights to the default agent to the Drive resource (at the base URL). Optionally give Public Read rights. pub fn set_drive_rights(store: &impl Storelike, public_read: bool) -> AtomicResult<()> { // Now let's add the agent as the Root user and provide write access - let mut drive = store.get_resource(store.get_server_url())?; + let mut drive = store.get_resource(&store.get_server_url()?)?; let write_agent = store.get_default_agent()?.subject; let read_agent = write_agent.clone(); @@ -234,7 +235,7 @@ You can create folders to organise your resources. To use the data in your web apps checkout our client libraries: [@tomic/lib](https://docs.atomicdata.dev/js), [@tomic/react](https://docs.atomicdata.dev/usecases/react) and [@tomic/svelte](https://docs.atomicdata.dev/svelte) Use [@tomic/cli](https://docs.atomicdata.dev/js-cli) to generate typed ontologies inside your code. -"#, store.get_server_url(), &format!("{}/{}", drive.get_subject(), DEFAULT_ONTOLOGY_PATH)), store)?; +"#, store.get_server_url()?, &format!("{}/{}", drive.get_subject(), DEFAULT_ONTOLOGY_PATH)), store)?; } drive.save_locally(store)?; Ok(()) @@ -290,7 +291,7 @@ pub fn populate_collections(store: &impl Storelike) -> AtomicResult<()> { /// Makes sure they are fetchable pub fn populate_endpoints(store: &crate::Db) -> AtomicResult<()> { let endpoints = crate::endpoints::default_endpoints(); - let endpoints_collection = format!("{}/endpoints", store.get_server_url()); + let endpoints_collection = format!("{}/endpoints", store.get_server_url()?); for endpoint in endpoints { let mut resource = endpoint.to_resource(store)?; resource.set( diff --git a/lib/src/resources.rs b/lib/src/resources.rs index ee1001fa1..be6a67e4a 100644 --- a/lib/src/resources.rs +++ b/lib/src/resources.rs @@ -210,15 +210,15 @@ impl Resource { } } - pub fn random_subject(store: &impl Storelike) -> String { - format!("{}/{}", store.get_server_url(), Ulid::new().to_string()) + pub fn random_subject(store: &impl Storelike) -> AtomicResult { + let server_url = store.get_server_url()?; + Ok(format!("{}/{}", server_url, Ulid::new().to_string())) } /// Create a new resource with a generated Subject - pub fn new_generate_subject(store: &impl Storelike) -> Resource { - let subject = Resource::random_subject(store); - - Resource::new(subject) + pub fn new_generate_subject(store: &impl Storelike) -> AtomicResult { + let subject = Resource::random_subject(store)?; + Ok(Resource::new(subject)) } /// Create a new instance of some Class. @@ -229,7 +229,7 @@ impl Resource { let class = store.get_class(class_url)?; let subject = format!( "{}/{}/{}", - store.get_server_url(), + store.get_server_url()?, &class.shortname, random_string(10) ); @@ -781,7 +781,7 @@ mod test { let store = init_store(); let property: String = urls::CHILDREN.into(); let append_value = "http://localhost/someURL"; - let mut resource = Resource::new_generate_subject(&store); + let mut resource = Resource::new_generate_subject(&store).unwrap(); resource .push(&property, append_value.into(), false) .unwrap(); @@ -807,11 +807,11 @@ mod test { #[test] fn get_children() { let store = init_store(); - let mut resource1 = Resource::new_generate_subject(&store); + let mut resource1 = Resource::new_generate_subject(&store).unwrap(); let subject1 = resource1.get_subject().to_string(); resource1.save_locally(&store).unwrap(); - let mut resource2 = Resource::new_generate_subject(&store); + let mut resource2 = Resource::new_generate_subject(&store).unwrap(); resource2 .set(urls::PARENT.into(), Value::AtomicUrl(subject1), &store) .unwrap(); @@ -829,11 +829,11 @@ mod test { let store = init_store(); // Create 3 resources in a tree structure. - let mut resource1 = Resource::new_generate_subject(&store); + let mut resource1 = Resource::new_generate_subject(&store).unwrap(); let subject1 = resource1.get_subject().to_string(); resource1.save_locally(&store).unwrap(); - let mut resource2 = Resource::new_generate_subject(&store); + let mut resource2 = Resource::new_generate_subject(&store).unwrap(); resource2 .set( urls::PARENT.into(), @@ -844,7 +844,7 @@ mod test { let subject2 = resource2.get_subject().to_string(); resource2.save_locally(&store).unwrap(); - let mut resource3 = Resource::new_generate_subject(&store); + let mut resource3 = Resource::new_generate_subject(&store).unwrap(); let resource3_subject = resource3.get_subject().to_string(); resource3 diff --git a/lib/src/store.rs b/lib/src/store.rs index 8aeb82913..7f32652b6 100644 --- a/lib/src/store.rs +++ b/lib/src/store.rs @@ -15,6 +15,7 @@ pub struct Store { // The store currently holds two stores - that is not ideal hashmap: Arc>>, default_agent: Arc>>, + server_url: Arc>>, } impl Store { @@ -24,11 +25,18 @@ impl Store { let store = Store { hashmap: Arc::new(Mutex::new(HashMap::new())), default_agent: Arc::new(Mutex::new(None)), + server_url: Arc::new(Mutex::new(None)), }; crate::populate::populate_base_models(&store)?; Ok(store) } + /// Set the URL of the server which endpoint we are using. + /// This is needed for generating correct URLs for Commits, Search, etc. + pub fn set_server_url(&self, server_url: &str) { + self.server_url.lock().unwrap().replace(server_url.into()); + } + /// Triple Pattern Fragments interface. /// Use this for most queries, e.g. finding all items with some property / value combination. /// Returns an empty array if nothing is found. @@ -158,14 +166,16 @@ impl Storelike for Store { Box::new(self.hashmap.lock().unwrap().clone().into_values()) } - fn get_server_url(&self) -> &str { - // TODO Should be implemented later when companion functionality is here - // https://github.com/atomicdata-dev/atomic-server/issues/6 - "local:store" + fn get_server_url(&self) -> AtomicResult { + self.server_url + .lock() + .unwrap() + .clone() + .ok_or("No server URL found. Set it using `set_server_url`.".into()) } fn get_self_url(&self) -> Option { - Some(self.get_server_url().into()) + None } fn get_default_agent(&self) -> AtomicResult { diff --git a/lib/src/storelike.rs b/lib/src/storelike.rs index 54705bfca..c337f2150 100644 --- a/lib/src/storelike.rs +++ b/lib/src/storelike.rs @@ -98,7 +98,9 @@ pub trait Storelike: Sized { /// E.g. `https://example.com` /// This is where deltas should be sent to. /// Also useful for Subject URL generation. - fn get_server_url(&self) -> &str; + fn get_server_url(&self) -> AtomicResult { + Err("No server URL found. Set it using `set_server_url`.".into()) + } /// Returns the root URL where this instance of the store is hosted. /// Should return `None` if this is simply a client and not a server. @@ -155,6 +157,28 @@ pub trait Storelike: Sized { Ok(resource) } + /// Performs a full-text search on the Server's /search endpoint. + /// Requires a server URL to be set. + fn search( + &self, + query: &str, + opts: crate::client::search::SearchOpts, + ) -> AtomicResult> { + let server_url = self.get_server_url()?; + let subject = crate::client::search::build_search_subject(&server_url, query, opts); + println!("subject: {:?}", subject); + // let resource = self.fetch_resource(&subject, self.get_default_agent().ok().as_ref())?; + let resource = self.fetch_resource("https://atomicdata.dev/search?q=a&include=true&limit=30&parents=https%3A%2F%2Fatomicdata.dev%2Fdrive%2Fxzpv34r5ibr", self.get_default_agent().ok().as_ref())?; + let results: Vec = match resource.get(urls::ENDPOINT_RESULTS) { + Ok(Value::ResourceArray(vec)) => { + println!("members: {:?}", vec); + vec.iter().cloned().map(|r| r.try_into().unwrap()).collect() + } + _ => return Err("No 'ENDPOINT_RESULTS' in response from server.".into()), + }; + Ok(results) + } + /// Returns a full Resource with native Values. /// Note that this does _not_ construct dynamic Resources, such as collections. /// If you're not sure what to use, use `get_resource_extended`. diff --git a/lib/src/values.rs b/lib/src/values.rs index b6c75ace9..e177701e2 100644 --- a/lib/src/values.rs +++ b/lib/src/values.rs @@ -39,6 +39,18 @@ pub enum SubResource { Subject(String), } +// try convert subresource into resource +impl TryInto for SubResource { + type Error = String; + + fn try_into(self) -> Result { + match self { + SubResource::Resource(r) => Ok(*r.clone()), + _ => Err("SubResource is not a Resource".into()), + } + } +} + /// When the Datatype of a Value is not handled by this library #[derive(Clone, Debug, Serialize, Deserialize)] pub struct UnsupportedValue { diff --git a/server/build.rs b/server/build.rs index a3d3234b8..ff6b58a7b 100644 --- a/server/build.rs +++ b/server/build.rs @@ -19,7 +19,7 @@ struct Dirs { fn main() -> std::io::Result<()> { // Uncomment this line if you want faster builds during development - // return Ok(()); + return Ok(()); const BROWSER_ROOT: &str = "../browser/"; let dirs: Dirs = { Dirs { diff --git a/server/src/handlers/download.rs b/server/src/handlers/download.rs index 09b8d0ac8..24cf50bdb 100644 --- a/server/src/handlers/download.rs +++ b/server/src/handlers/download.rs @@ -4,10 +4,7 @@ use actix_web::{web, HttpRequest, HttpResponse}; use atomic_lib::{urls, Resource, Storelike}; use serde::Deserialize; -use std::{collections::HashSet, io::Write, path::PathBuf}; - -#[cfg(feature = "image")] -use crate::handlers::image::{is_image, process_image}; +use std::{collections::HashSet, path::PathBuf}; #[serde_with::serde_as] #[serde_with::skip_serializing_none] @@ -76,10 +73,12 @@ pub fn download_file_handler_partial( // only if image feature flag is on #[cfg(feature = "image")] { + use crate::handlers::image::{is_image, process_image}; if !is_image(&file_path) { return Err("Quality or with parameter are not supported for non image files".into()); } - process_image(&file_path, &processed_file_path, params)?; + let format = get_format(params)?; + process_image(&file_path, &processed_file_path, params, &format)?; } let file = NamedFile::open(processed_file_path)?; diff --git a/server/src/handlers/image.rs b/server/src/handlers/image.rs index ad75e3df9..8dd3d2d4d 100644 --- a/server/src/handlers/image.rs +++ b/server/src/handlers/image.rs @@ -1,6 +1,13 @@ +use std::io::Write; +use std::path::PathBuf; + use image::GenericImageView; use image::{codecs::avif::AvifEncoder, ImageReader}; +use crate::errors::AtomicServerResult; + +use super::download::DownloadParams; + pub fn is_image(file_path: &PathBuf) -> bool { if let Ok(img) = image::open(file_path) { return img.dimensions() > (0, 0); @@ -12,8 +19,8 @@ pub fn process_image( file_path: &PathBuf, new_path: &PathBuf, params: &DownloadParams, + format: &str, ) -> AtomicServerResult<()> { - let format = get_format(params)?; let quality = params.q.unwrap_or(100.0).clamp(0.0, 100.0); let mut img = ImageReader::open(file_path)? diff --git a/server/src/handlers/mod.rs b/server/src/handlers/mod.rs index 69dcddec5..636b4d424 100644 --- a/server/src/handlers/mod.rs +++ b/server/src/handlers/mod.rs @@ -9,6 +9,8 @@ pub mod commit; pub mod download; pub mod export; pub mod get_resource; +#[cfg(feature = "image")] +pub mod image; pub mod post_resource; pub mod search; pub mod single_page_app; From a59b78e698d9b712cae6dff2eb55c66b7ae906c3 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Wed, 26 Mar 2025 19:41:48 +0100 Subject: [PATCH 05/12] Gets server url can err --- cli/tests/tests.rs | 25 ++++++------------------- lib/src/commit.rs | 24 +++++++++++++----------- lib/src/db.rs | 6 +++--- lib/src/db/test.rs | 12 ++++++------ lib/src/endpoints.rs | 2 +- lib/src/parse.rs | 2 ++ lib/src/plugins/prunetests.rs | 9 ++++----- lib/src/plugins/versioning.rs | 17 ++++++++++------- server/build.rs | 2 +- server/src/appstate.rs | 8 ++++---- server/src/handlers/single_page_app.rs | 2 +- server/src/handlers/upload.rs | 6 +++--- server/src/tests.rs | 2 +- 13 files changed, 55 insertions(+), 62 deletions(-) diff --git a/cli/tests/tests.rs b/cli/tests/tests.rs index 87fc87f73..e823c3ebb 100644 --- a/cli/tests/tests.rs +++ b/cli/tests/tests.rs @@ -13,12 +13,6 @@ mod test { .failure(); } - #[test] - fn get_shortname() { - let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); - cmd.args(["get", "shortname"]).assert().success(); - } - #[test] fn get_url() { let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); @@ -26,29 +20,22 @@ mod test { } #[test] - fn get_path() { + fn get_path_array_non_existent() { let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); - cmd.args(["get", &format!("{TEST_URL} name")]) + cmd.args(["get", &format!("{TEST_URL} is-a 1")]) .assert() - .success(); + .failure(); } #[test] - fn get_path_array() { + fn search() { + let parent = "https://atomicdata.dev/ontology/core"; let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); - cmd.args(["get", &format!("{TEST_URL} is-a 0")]) + cmd.args(["search", "a", "--parent", parent]) .assert() .success(); } - #[test] - fn get_path_array_non_existent() { - let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); - cmd.args(["get", &format!("{TEST_URL} is-a 1")]) - .assert() - .failure(); - } - #[ignore] #[test] fn set_and_get() { diff --git a/lib/src/commit.rs b/lib/src/commit.rs index 9ff8eef2f..bd439982e 100644 --- a/lib/src/commit.rs +++ b/lib/src/commit.rs @@ -675,11 +675,12 @@ mod test { } use super::*; - use crate::{agents::Agent, Storelike}; + use crate::{agents::Agent, Store, Storelike}; #[test] fn agent_and_commit() { - let store = crate::Store::init().unwrap(); + let store = Store::init().unwrap(); + store.set_server_url("http://localhost:9883"); store.populate().unwrap(); let agent = store.create_agent(Some("test_actor")).unwrap(); let subject = "https://localhost/new_thing"; @@ -711,7 +712,8 @@ mod test { #[test] fn serialize_commit() { - let store = crate::Store::init().unwrap(); + let store = Store::init().unwrap(); + store.set_server_url("http://localhost:9883"); store.populate().unwrap(); let mut set: HashMap = HashMap::new(); let shortname = Value::new("shortname", &DataType::String).unwrap(); @@ -739,13 +741,13 @@ mod test { #[test] fn signature_matches() { + let store = Store::init().unwrap(); + store.set_server_url("http://localhost:9883"); let private_key = "CapMWIhFUT+w7ANv9oCPqrHrwZpkP2JhzF9JnyT6WcI="; - let store = crate::Store::init().unwrap(); - store.populate().unwrap(); - let agent = Agent::new_from_private_key(None, &store, private_key); + let agent = Agent::new_from_private_key(None, &store, private_key).unwrap(); assert_eq!( &agent.subject, - "local:store/agents/7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U=" + "http://localhost:9883/agents/7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U=" ); store.add_resource(&agent.to_resource().unwrap()).unwrap(); let subject = "https://localhost/new_thing"; @@ -760,8 +762,8 @@ mod test { let signature = commit.signature.clone().unwrap(); let serialized = commit.serialize_deterministically_json_ad(&store).unwrap(); - assert_eq!(serialized, "{\"https://atomicdata.dev/properties/createdAt\":0,\"https://atomicdata.dev/properties/isA\":[\"https://atomicdata.dev/classes/Commit\"],\"https://atomicdata.dev/properties/set\":{\"https://atomicdata.dev/properties/description\":\"Some value\",\"https://atomicdata.dev/properties/shortname\":\"someval\"},\"https://atomicdata.dev/properties/signer\":\"local:store/agents/7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U=\",\"https://atomicdata.dev/properties/subject\":\"https://localhost/new_thing\"}"); - assert_eq!(signature, "JOGRyp1NCulc0RNuuNozgIagQPRoZy0Y5+mbSpHY2DKiN3vqUNYLjXbAPYT6Cga6vSG9zztEIa/ZcbQPo7wgBg=="); + assert_eq!(serialized, "{\"https://atomicdata.dev/properties/createdAt\":0,\"https://atomicdata.dev/properties/isA\":[\"https://atomicdata.dev/classes/Commit\"],\"https://atomicdata.dev/properties/set\":{\"https://atomicdata.dev/properties/description\":\"Some value\",\"https://atomicdata.dev/properties/shortname\":\"someval\"},\"https://atomicdata.dev/properties/signer\":\"http://localhost:9883/agents/7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U=\",\"https://atomicdata.dev/properties/subject\":\"https://localhost/new_thing\"}"); + assert_eq!(signature, "pYkM6dC4qFGGh6EXbys6NwmhaPIA6Z7Ij//rPejo5mnBOvs1EFxP0iErfJiUXZgJDi5yK4QOBMb2nf2FIKcUCA=="); } #[test] @@ -775,9 +777,9 @@ mod test { } #[test] - fn invalid_subjects() { - let store = crate::Store::init().unwrap(); + let store = Store::init().unwrap(); + store.set_server_url("http://localhost:9883"); store.populate().unwrap(); let agent = store.create_agent(Some("test_actor")).unwrap(); let resource = Resource::new("https://localhost/test_resource".into()); diff --git a/lib/src/db.rs b/lib/src/db.rs index f64fec2a8..90e6f1979 100644 --- a/lib/src/db.rs +++ b/lib/src/db.rs @@ -660,14 +660,14 @@ impl Storelike for Db { Ok(commit_response) } - fn get_server_url(&self) -> &str { - &self.server_url + fn get_server_url(&self) -> AtomicResult { + Ok(self.server_url.clone()) } // Since the DB is often also the server, this should make sense. // Some edge cases might appear later on (e.g. a slave DB that only stores copies?) fn get_self_url(&self) -> Option { - Some(self.get_server_url().into()) + self.get_server_url().ok() } fn get_default_agent(&self) -> AtomicResult { diff --git a/lib/src/db/test.rs b/lib/src/db/test.rs index f3ec91cd3..7803c6cbf 100644 --- a/lib/src/db/test.rs +++ b/lib/src/db/test.rs @@ -65,7 +65,7 @@ fn populate_collections() { .map(|r| r.get_subject().into()) .collect(); println!("{:?}", subjects); - let collections_collection_url = format!("{}/collections", store.get_server_url()); + let collections_collection_url = format!("{}/collections", store.get_server_url().unwrap()); let collections_resource = store .get_resource_extended(&collections_collection_url, false, &ForAgent::Public) .unwrap(); @@ -91,7 +91,7 @@ fn populate_collections() { fn destroy_resource_and_check_collection_and_commits() { let store = Db::init_temp("counter").unwrap(); let for_agent = &ForAgent::Public; - let agents_url = format!("{}/agents", store.get_server_url()); + let agents_url = format!("{}/agents", store.get_server_url().unwrap()); let agents_collection_1 = store .get_resource_extended(&agents_url, false, for_agent) .unwrap(); @@ -110,7 +110,7 @@ fn destroy_resource_and_check_collection_and_commits() { ); // We will count the commits, and check if they've incremented later on. - let commits_url = format!("{}/commits", store.get_server_url()); + let commits_url = format!("{}/commits", store.get_server_url().unwrap()); let commits_collection_1 = store .get_resource_extended(&commits_url, false, for_agent) .unwrap(); @@ -198,7 +198,7 @@ fn get_extended_resource_pagination() { let store = Db::init_temp("get_extended_resource_pagination").unwrap(); let subject = format!( "{}/commits?current_page=2&page_size=99999", - store.get_server_url() + store.get_server_url().unwrap() ); let for_agent = &ForAgent::Public; if store @@ -244,7 +244,7 @@ fn queries() { let mut subject_to_delete = "".to_string(); for _x in 0..count { - let mut demo_resource = Resource::new_generate_subject(store); + let mut demo_resource = Resource::new_generate_subject(store).unwrap(); // We make one resource public if _x == 1 { demo_resource @@ -489,7 +489,7 @@ fn test_collection_update_value(store: &Db, property_url: &str, old_val: Value, let mut resources: Vec = (0..count) .map(|_num| { - let mut demo_resource = Resource::new_generate_subject(store); + let mut demo_resource = Resource::new_generate_subject(store).unwrap(); demo_resource .set(property_url.into(), old_val.clone(), store) .unwrap(); diff --git a/lib/src/endpoints.rs b/lib/src/endpoints.rs index c3699fd60..859ad737d 100644 --- a/lib/src/endpoints.rs +++ b/lib/src/endpoints.rs @@ -58,7 +58,7 @@ pub struct PostEndpoint { impl Endpoint { /// Converts Endpoint to resource. Does not save it. pub fn to_resource(&self, store: &impl Storelike) -> AtomicResult { - let subject = format!("{}{}", store.get_server_url(), self.path); + let subject = format!("{}{}", store.get_server_url()?, self.path); let mut resource = store.get_resource_new(&subject); resource.set_string(urls::DESCRIPTION.into(), &self.description, store)?; resource.set_string(urls::SHORTNAME.into(), &self.shortname, store)?; diff --git a/lib/src/parse.rs b/lib/src/parse.rs index 8fcfd0d6a..08af5d4c9 100644 --- a/lib/src/parse.rs +++ b/lib/src/parse.rs @@ -591,6 +591,7 @@ mod test { fn create_store_and_importer() -> (crate::Store, String) { let store = crate::Store::init().unwrap(); + store.set_server_url("http://localhost:9883"); store.populate().unwrap(); let agent = store.create_agent(None).unwrap(); store.set_default_agent(agent); @@ -669,6 +670,7 @@ mod test { #[test] fn import_resource_malicious() { let (store, importer) = create_store_and_importer(); + store.set_server_url("http://localhost:9883"); // Try to overwrite the main drive with some malicious data let agent = store.get_default_agent().unwrap(); diff --git a/lib/src/plugins/prunetests.rs b/lib/src/plugins/prunetests.rs index c167d854c..1fed03489 100644 --- a/lib/src/plugins/prunetests.rs +++ b/lib/src/plugins/prunetests.rs @@ -57,14 +57,13 @@ fn handle_prune_tests_request(context: HandlePostContext) -> AtomicResult Resource { - let mut resource = Resource::new_generate_subject(store); +fn build_response(store: &impl Storelike, status: i32, message: String) -> AtomicResult { + let mut resource = Resource::new_generate_subject(store)?; resource.set_class(urls::ENDPOINT_RESPONSE); resource.set_unsafe(urls::STATUS.to_string(), status.into()); resource.set_unsafe(urls::RESPONSE_MESSAGE.to_string(), message.into()); - resource + Ok(resource) } diff --git a/lib/src/plugins/versioning.rs b/lib/src/plugins/versioning.rs index 8ee57fd8b..1e707858f 100644 --- a/lib/src/plugins/versioning.rs +++ b/lib/src/plugins/versioning.rs @@ -80,11 +80,11 @@ fn handle_all_versions_request(context: HandleGetContext) -> AtomicResult = collection .members .iter_mut() .map(|commit_url| construct_version_endpoint_url(store, commit_url)) - .collect(); + .collect::>>()?; collection.members = new_members; collection.to_resource(store) } @@ -148,12 +148,15 @@ pub fn construct_version( } /// Creates the versioning URL for some specific Commit -fn construct_version_endpoint_url(store: &impl Storelike, commit_url: &str) -> String { - format!( +fn construct_version_endpoint_url( + store: &impl Storelike, + commit_url: &str, +) -> AtomicResult { + Ok(format!( "{}/versioning?commit={}", - store.get_server_url(), + store.get_server_url()?, urlencoding::encode(commit_url) - ) + )) } /// Gets a version of a Resource by Commit. @@ -163,7 +166,7 @@ pub fn get_version( store: &impl Storelike, for_agent: &ForAgent, ) -> AtomicResult { - let version_url = construct_version_endpoint_url(store, commit_url); + let version_url = construct_version_endpoint_url(store, commit_url)?; match store.get_resource(&version_url) { Ok(cached) => Ok(cached), Err(_not_cached) => { diff --git a/server/build.rs b/server/build.rs index ff6b58a7b..a3d3234b8 100644 --- a/server/build.rs +++ b/server/build.rs @@ -19,7 +19,7 @@ struct Dirs { fn main() -> std::io::Result<()> { // Uncomment this line if you want faster builds during development - return Ok(()); + // return Ok(()); const BROWSER_ROOT: &str = "../browser/"; let dirs: Dirs = { Dirs { diff --git a/server/src/appstate.rs b/server/src/appstate.rs index fc6ca11ab..6105eac4f 100644 --- a/server/src/appstate.rs +++ b/server/src/appstate.rs @@ -137,7 +137,7 @@ fn set_default_agent(config: &Config, store: &impl Storelike) -> AtomicServerRes "server".into(), store, &agent_config.private_key, - ); + )?; store.add_resource(&recreated_agent.to_resource()?)?; agent_config } else { @@ -179,7 +179,7 @@ fn set_default_agent(config: &Config, store: &impl Storelike) -> AtomicServerRes /// Creates the first Invitation that is opened by the user on the Home page. fn set_up_initial_invite(store: &impl Storelike) -> AtomicServerResult<()> { - let subject = format!("{}/setup", store.get_server_url()); + let subject = format!("{}/setup", store.get_server_url()?); tracing::info!("Creating initial Invite at {}", subject); let mut invite = store.get_resource_new(&subject); invite.set_class(atomic_lib::urls::INVITE); @@ -197,12 +197,12 @@ fn set_up_initial_invite(store: &impl Storelike) -> AtomicServerResult<()> { )?; invite.set( atomic_lib::urls::TARGET.into(), - atomic_lib::Value::AtomicUrl(store.get_server_url().into()), + atomic_lib::Value::AtomicUrl(store.get_server_url()?.into()), store, )?; invite.set( atomic_lib::urls::PARENT.into(), - atomic_lib::Value::AtomicUrl(store.get_server_url().into()), + atomic_lib::Value::AtomicUrl(store.get_server_url()?.into()), store, )?; invite.set( diff --git a/server/src/handlers/single_page_app.rs b/server/src/handlers/single_page_app.rs index 6140a2c3b..6b8cbba0f 100644 --- a/server/src/handlers/single_page_app.rs +++ b/server/src/handlers/single_page_app.rs @@ -11,7 +11,7 @@ pub async fn single_page( path: actix_web::web::Path, ) -> AtomicServerResult { let template = include_str!("../../assets_tmp/index.html"); - let subject = format!("{}/{}", appstate.store.get_server_url(), path); + let subject = format!("{}/{}", appstate.store.get_server_url()?, path); let meta_tags: MetaTags = if let Ok(resource) = appstate .store diff --git a/server/src/handlers/upload.rs b/server/src/handlers/upload.rs index 189332f90..b65a49e72 100644 --- a/server/src/handlers/upload.rs +++ b/server/src/handlers/upload.rs @@ -30,7 +30,7 @@ pub async fn upload_handler( let parent = store.get_resource(&query.parent)?; let subject = format!( "{}{}", - store.get_server_url(), + store.get_server_url()?, req.head() .uri .path_and_query() @@ -93,8 +93,8 @@ async fn save_file_and_create_resource( let mimetype = guess_mime_for_filename(filename); let subject_path = format!("files/{}", urlencoding::encode(&file_id)); - let new_subject = format!("{}/{}", store.get_server_url(), subject_path); - let download_url = format!("{}/download/{}", store.get_server_url(), subject_path); + let new_subject = format!("{}/{}", store.get_server_url()?, subject_path); + let download_url = format!("{}/download/{}", store.get_server_url()?, subject_path); let mut resource = atomic_lib::Resource::new_instance(urls::FILE, store)?; resource diff --git a/server/src/tests.rs b/server/src/tests.rs index ccd9d8424..350aaf411 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -16,7 +16,7 @@ use atomic_lib::{urls, Storelike}; /// Returns the request with signed headers. Also adds a json-ad accept header - overwrite this if you need something else. fn build_request_authenticated(path: &str, appstate: &AppState) -> TestRequest { - let url = format!("{}{}", appstate.store.get_server_url(), path); + let url = format!("{}{}", appstate.store.get_server_url().unwrap(), path); let headers = atomic_lib::client::get_authentication_headers( &url, &appstate.store.get_default_agent().unwrap(), From 13a37d52e6a75025af76081b1333b6587a5afaee Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Thu, 8 May 2025 14:47:01 +0200 Subject: [PATCH 06/12] Fix tests, only run them once --- lib/src/commit.rs | 13 +----- lib/src/plugins/versioning.rs | 1 + lib/src/resources.rs | 78 ----------------------------------- lib/src/store.rs | 2 +- lib/src/storelike.rs | 4 +- lib/src/test_utils.rs | 1 + server/Cargo.toml | 1 + 7 files changed, 7 insertions(+), 93 deletions(-) diff --git a/lib/src/commit.rs b/lib/src/commit.rs index bd439982e..1ef735cb4 100644 --- a/lib/src/commit.rs +++ b/lib/src/commit.rs @@ -206,7 +206,7 @@ impl Commit { commit.check_for_circular_parents()?; let mut is_new = false; // Create a new resource if it doesn't exist yet - let mut resource_old = match store.get_resource(&commit.subject) { + let resource_old = match store.get_resource(&commit.subject) { Ok(rs) => rs, Err(_) => { is_new = true; @@ -233,17 +233,6 @@ impl Commit { if is_new { crate::hierarchy::check_append(store, &applied.resource_new, &validate_for.into())?; } else { - // Set a parent only if the rights checks are to be validated. - // If there is no explicit parent set on the previous resource, use a default. - // Unless it's a Drive! - if resource_old.get(urls::PARENT).is_err() { - let default_parent = store.get_self_url().ok_or("There is no self_url set, and no parent in the Commit. The commit can not be applied.")?; - resource_old.set( - urls::PARENT.into(), - Value::AtomicUrl(default_parent), - store, - )?; - } // This should use the _old_ resource, no the new one, as the new one might maliciously give itself write rights. crate::hierarchy::check_write(store, &resource_old, &validate_for.into())?; } diff --git a/lib/src/plugins/versioning.rs b/lib/src/plugins/versioning.rs index 1e707858f..20692a403 100644 --- a/lib/src/plugins/versioning.rs +++ b/lib/src/plugins/versioning.rs @@ -187,6 +187,7 @@ mod test { fn constructs_versions() { let store = Store::init().unwrap(); store.populate().unwrap(); + store.set_server_url("http://localhost"); let agent = store.create_agent(None).unwrap(); store.set_default_agent(agent.clone()); store.get_resource(&agent.subject).unwrap(); diff --git a/lib/src/resources.rs b/lib/src/resources.rs index be6a67e4a..78c0156e9 100644 --- a/lib/src/resources.rs +++ b/lib/src/resources.rs @@ -823,82 +823,4 @@ mod test { assert_eq!(children.len(), 1); assert_eq!(children[0].get_subject(), &subject2); } - - #[test] - fn destroy() { - let store = init_store(); - // Create 3 resources in a tree structure. - - let mut resource1 = Resource::new_generate_subject(&store).unwrap(); - let subject1 = resource1.get_subject().to_string(); - resource1.save_locally(&store).unwrap(); - - let mut resource2 = Resource::new_generate_subject(&store).unwrap(); - resource2 - .set( - urls::PARENT.into(), - Value::AtomicUrl(subject1.clone()), - &store, - ) - .unwrap(); - let subject2 = resource2.get_subject().to_string(); - resource2.save_locally(&store).unwrap(); - - let mut resource3 = Resource::new_generate_subject(&store).unwrap(); - let resource3_subject = resource3.get_subject().to_string(); - - resource3 - .set( - urls::PARENT.into(), - Value::AtomicUrl(subject2.clone()), - &store, - ) - .unwrap(); - resource3 - .set(urls::NAME.into(), Value::String("resource3".into()), &store) - .unwrap(); - let subject3 = resource3.get_subject().to_string(); - resource3.save_locally(&store).unwrap(); - - // Check if all 3 resources exist in the store. - - assert_eq!( - store.get_resource(&subject1).unwrap().get_subject(), - &subject1 - ); - assert_eq!( - store.get_resource(&subject2).unwrap().get_subject(), - &subject2 - ); - assert_eq!( - store.get_resource(&subject3).unwrap().get_subject(), - &subject3 - ); - - // Destroy the first resource, and check if all 3 resources are gone. - resource1.destroy(&store).unwrap(); - - assert_panics!({ store.get_resource(&subject1).unwrap() }); - assert_panics!({ store.get_resource(&subject2).unwrap() }); - assert_panics!({ store.get_resource(&subject3).unwrap() }); - - // Create a new resource with the same subject as resource 3 to check if there's no old data left. - let mut resource4 = Resource::new(resource3_subject.to_string()); - - resource4 - .set( - urls::DESCRIPTION.into(), - Value::Markdown("description thing".into()), - &store, - ) - .unwrap(); - resource4.save_locally(&store).unwrap(); - - assert_eq!( - resource4.get(urls::DESCRIPTION).unwrap().to_string(), - "description thing" - ); - - assert!(resource4.get(urls::NAME).is_err()); - } } diff --git a/lib/src/store.rs b/lib/src/store.rs index 7f32652b6..fbe4c3640 100644 --- a/lib/src/store.rs +++ b/lib/src/store.rs @@ -171,7 +171,7 @@ impl Storelike for Store { .lock() .unwrap() .clone() - .ok_or("No server URL found. Set it using `set_server_url`.".into()) + .ok_or("No server URL found. Set it using `store.set_server_url`.".into()) } fn get_self_url(&self) -> Option { diff --git a/lib/src/storelike.rs b/lib/src/storelike.rs index c337f2150..a19e53866 100644 --- a/lib/src/storelike.rs +++ b/lib/src/storelike.rs @@ -102,9 +102,9 @@ pub trait Storelike: Sized { Err("No server URL found. Set it using `set_server_url`.".into()) } - /// Returns the root URL where this instance of the store is hosted. - /// Should return `None` if this is simply a client and not a server. + /// Returns the root URL of where this instance of the store is hosted. /// E.g. `https://example.com` + /// Should return `None` if this store is a client and not a server. fn get_self_url(&self) -> Option { None } diff --git a/lib/src/test_utils.rs b/lib/src/test_utils.rs index 3ff592ac2..f33941b11 100644 --- a/lib/src/test_utils.rs +++ b/lib/src/test_utils.rs @@ -5,6 +5,7 @@ pub fn init_store() -> crate::Store { let store = crate::Store::init().unwrap(); store.populate().unwrap(); + store.set_server_url("http://localhost"); let agent = store.create_agent(None).unwrap(); store.set_default_agent(agent); store diff --git a/server/Cargo.toml b/server/Cargo.toml index 8672c3678..f49f0e77f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -17,6 +17,7 @@ lto = true [[bin]] name = "atomic-server" path = "src/bin.rs" +test = false [build-dependencies] dircpy = "0.3.15" From 84b66c5c4e989e166b4fd3e5fda5e03cb6c77c5a Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Mon, 12 May 2025 15:37:05 +0200 Subject: [PATCH 07/12] Fix search CLI #958 --- cli/src/main.rs | 14 ++++++++++++-- cli/src/search.rs | 4 ++++ lib/src/storelike.rs | 5 +---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 5e7a60ddf..0257294cc 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -101,6 +101,11 @@ enum Commands { /// Subject URL of the parent Resource to filter by #[arg(long)] parent: Option, + /// Server URL to search on + /// Will query this + `/search` if provided. + /// Defaults to the server in the config. + #[arg(long)] + server: Option, /// Serialization format #[arg(long, value_enum, default_value = "pretty")] as_: SerializeOptions, @@ -268,8 +273,13 @@ fn exec_command(context: &mut Context) -> AtomicResult<()> { } => { commit::set(context, &subject, &property, &value)?; } - Commands::Search { query, parent, as_ } => { - search::search(context, query, parent, &as_)?; + Commands::Search { + query, + parent, + server, + as_, + } => { + search::search(context, query, parent, server, &as_)?; } Commands::Validate => { validate(context); diff --git a/cli/src/search.rs b/cli/src/search.rs index 81b4d9008..bf852a8ab 100644 --- a/cli/src/search.rs +++ b/cli/src/search.rs @@ -6,6 +6,7 @@ pub fn search( context: &crate::Context, query: String, parent: Option, + server: Option, serialize: &crate::SerializeOptions, ) -> AtomicResult<()> { context.read_config(); @@ -15,6 +16,9 @@ pub fn search( parents: Some(vec![parent.unwrap_or_default()]), ..Default::default() }; + if let Some(server) = server { + context.store.set_server_url(&server); + } let resources = context.store.search(&query, opts)?; if resources.is_empty() { println!("No results found for query: {}", query); diff --git a/lib/src/storelike.rs b/lib/src/storelike.rs index a19e53866..cde832f61 100644 --- a/lib/src/storelike.rs +++ b/lib/src/storelike.rs @@ -166,12 +166,9 @@ pub trait Storelike: Sized { ) -> AtomicResult> { let server_url = self.get_server_url()?; let subject = crate::client::search::build_search_subject(&server_url, query, opts); - println!("subject: {:?}", subject); - // let resource = self.fetch_resource(&subject, self.get_default_agent().ok().as_ref())?; - let resource = self.fetch_resource("https://atomicdata.dev/search?q=a&include=true&limit=30&parents=https%3A%2F%2Fatomicdata.dev%2Fdrive%2Fxzpv34r5ibr", self.get_default_agent().ok().as_ref())?; + let resource = self.fetch_resource(&subject, self.get_default_agent().ok().as_ref())?; let results: Vec = match resource.get(urls::ENDPOINT_RESULTS) { Ok(Value::ResourceArray(vec)) => { - println!("members: {:?}", vec); vec.iter().cloned().map(|r| r.try_into().unwrap()).collect() } _ => return Err("No 'ENDPOINT_RESULTS' in response from server.".into()), From d7767aa2b25a1adf4d6cb1f768c6e81afff648fc Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Tue, 20 May 2025 15:52:29 +0200 Subject: [PATCH 08/12] Improve styling bottom bar --- browser/data-browser/src/components/Button.tsx | 2 +- browser/data-browser/src/components/NavBarSpacer.tsx | 2 +- browser/data-browser/src/components/Navigation.tsx | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/browser/data-browser/src/components/Button.tsx b/browser/data-browser/src/components/Button.tsx index 9eb7ff001..3ca411d73 100644 --- a/browser/data-browser/src/components/Button.tsx +++ b/browser/data-browser/src/components/Button.tsx @@ -153,7 +153,7 @@ export const ButtonBar = styled(ButtonClean)` } padding-left: ${p => (p.leftPadding ? '1.2rem' : '')}; - padding-right: ${p => (p.rightPadding ? '1.2rem' : '')}; + padding-right: ${p => (p.rightPadding ? '1rem' : '')}; `; /** Button with some optional margins around it */ diff --git a/browser/data-browser/src/components/NavBarSpacer.tsx b/browser/data-browser/src/components/NavBarSpacer.tsx index 38273a3ac..ffd863800 100644 --- a/browser/data-browser/src/components/NavBarSpacer.tsx +++ b/browser/data-browser/src/components/NavBarSpacer.tsx @@ -2,8 +2,8 @@ import { styled } from 'styled-components'; import { useSettings } from '../helpers/AppSettings'; import type { JSX } from 'react'; +import { NAVBAR_HEIGHT } from './Navigation'; -const NAVBAR_HEIGHT = '2rem'; const NAVBAR_CALC_PART = ` + ${NAVBAR_HEIGHT}`; export interface NavBarSpacerProps { diff --git a/browser/data-browser/src/components/Navigation.tsx b/browser/data-browser/src/components/Navigation.tsx index 9be4d92bd..8ff570436 100644 --- a/browser/data-browser/src/components/Navigation.tsx +++ b/browser/data-browser/src/components/Navigation.tsx @@ -15,6 +15,8 @@ import { useMediaQuery } from '../hooks/useMediaQuery'; import { useBackForward } from '../hooks/useNavigateWithTransition'; import { NAVBAR_TRANSITION_TAG } from '../helpers/transitionName'; +export const NAVBAR_HEIGHT = '2.5rem'; + interface NavWrapperProps { children: React.ReactNode; } @@ -96,6 +98,7 @@ function NavBar(): JSX.Element { <> setSideBarLocked(!sideBarLocked)} title={`Show / hide sidebar (${shortcuts.sidebarToggle})`} @@ -135,7 +138,7 @@ const NavBarBase = styled.div` /* transition: all 0.2s; */ position: fixed; z-index: ${p => p.theme.zIndex.sidebar}; - height: 2.5rem; + height: ${NAVBAR_HEIGHT}; display: flex; border: solid 1px ${props => props.theme.colors.bg2}; background-color: ${props => props.theme.colors.bg}; @@ -191,7 +194,6 @@ const VerticalDivider = styled.div` width: 1px; background-color: ${props => props.theme.colors.bg2}; height: 100%; - margin-left: ${p => p.theme.size(2)}; `; const SideBarWrapper = styled('div')` display: flex; From 03fa022497482ae98b7c9503e184b0001dca73bc Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Tue, 20 May 2025 15:52:50 +0200 Subject: [PATCH 09/12] Fix e2e-query command --- browser/e2e/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/e2e/package.json b/browser/e2e/package.json index e64b7c9d8..74fccff0b 100644 --- a/browser/e2e/package.json +++ b/browser/e2e/package.json @@ -24,7 +24,7 @@ "test-debug": "PWDEBUG=1 playwright test", "test-update": "playwright test --update-snapshots", "test-new": "playwright codegen http://localhost:5173", - "test-query": "PWDEBUG=1 DELETE_PREVIOUS_TEST_DRIVES=false playwright test \"e2e.spec.ts\" \"tables.spec.ts\" \"documents.spec.ts\" \"ontology.spec.ts\" \"search.spec.ts\" -g" + "test-query": "PWDEBUG=1 DELETE_PREVIOUS_TEST_DRIVES=false playwright test **/*.spec.ts --retries=0 -g" }, "dependencies": {} } From 6069f154271307768859d5cf5e0d3e50ef0afcb4 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Tue, 20 May 2025 15:53:02 +0200 Subject: [PATCH 10/12] Improve logs prune tests --- lib/src/plugins/prunetests.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/plugins/prunetests.rs b/lib/src/plugins/prunetests.rs index 1fed03489..bc93c153f 100644 --- a/lib/src/plugins/prunetests.rs +++ b/lib/src/plugins/prunetests.rs @@ -1,3 +1,5 @@ +use tracing::info; + use crate::{ endpoints::{Endpoint, HandleGetContext, HandlePostContext}, errors::AtomicResult, @@ -29,7 +31,7 @@ fn handle_prune_tests_request(context: HandlePostContext) -> AtomicResult AtomicResult Date: Tue, 20 May 2025 16:44:04 +0200 Subject: [PATCH 11/12] Website Template has description in ontology #1064 --- .../Template/ApplyTemplateDialog.tsx | 6 +-- .../src/components/Template/TemplateList.tsx | 48 +++++++++++++------ .../src/components/Template/template.ts | 10 ++-- .../components/Template/templates/website.tsx | 31 +++++++----- 4 files changed, 60 insertions(+), 35 deletions(-) diff --git a/browser/data-browser/src/components/Template/ApplyTemplateDialog.tsx b/browser/data-browser/src/components/Template/ApplyTemplateDialog.tsx index 28ffca7cc..0c74c2f1a 100644 --- a/browser/data-browser/src/components/Template/ApplyTemplateDialog.tsx +++ b/browser/data-browser/src/components/Template/ApplyTemplateDialog.tsx @@ -10,7 +10,6 @@ import { useDialog, } from '../Dialog'; import { Column } from '../Row'; -import type { Template } from './template'; import { useEffect, useMemo, useState } from 'react'; import Markdown from '../datatypes/Markdown'; import { dataBrowser, useResource, useResources, useStore } from '@tomic/react'; @@ -19,6 +18,7 @@ import { InlineErrMessage } from '../forms/InputStyles'; import { useSettings } from '../../helpers/AppSettings'; import { useNavigateWithTransition } from '../../hooks/useNavigateWithTransition'; import { constructOpenURL } from '../../helpers/navigation'; +import type { Template } from './template'; interface ApplyTemplateDialogProps { template?: Template; @@ -95,9 +95,7 @@ export function ApplyTemplateDialog({ - +
diff --git a/browser/data-browser/src/components/Template/TemplateList.tsx b/browser/data-browser/src/components/Template/TemplateList.tsx index 5bd720576..365c6b4cf 100644 --- a/browser/data-browser/src/components/Template/TemplateList.tsx +++ b/browser/data-browser/src/components/Template/TemplateList.tsx @@ -1,32 +1,50 @@ import { TemplateListItem } from './TemplateListItem'; import { styled } from 'styled-components'; import { website } from './templates/website'; -import type { Template } from './template'; +import type { Template, TemplateFn } from './template'; import { useState } from 'react'; import { ApplyTemplateDialog } from './ApplyTemplateDialog'; +import { useSettings } from '../../helpers/AppSettings'; -const templates: Template[] = [website]; +const templates: TemplateFn[] = [website]; export function TemplateList(): React.JSX.Element { const [dialogOpen, setDialogOpen] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState