diff --git a/CHANGELOG.md b/CHANGELOG.md index 55defc6a0..a2f4141f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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 +- Search endpoint throws error for websocket requests #1047 +- Fix search in CLI / atomic_lib #958 + ## [v0.40.2] - fix property sort order when importing + add tests #980 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/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 756432f5c..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,18 +98,27 @@ 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, /// Validates the store #[command(hide = true)] Validate, + /// Print the current agent + Agent, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum SerializeOptions { Pretty, Json, + JsonAd, NTriples, } @@ -118,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, } } @@ -151,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 } } @@ -238,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); @@ -257,12 +268,16 @@ 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); } + Commands::Agent => { + let agent = context.read_config(); + println!("{}", agent.agent); + } }; Ok(()) } diff --git a/cli/src/path.rs b/cli/src/path.rs deleted file mode 100644 index 9d7a5fdce..000000000 --- a/cli/src/path.rs +++ /dev/null @@ -1,36 +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(" "); - - // Returns a URL or Value - let store = &mut context.store; - let path = store.get_path( - &path_string, - Some(&context.mapping.lock().unwrap()), - &ForAgent::Sudo, - )?; - 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 fcf5facae..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(), }) @@ -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..c1d221863 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 fetching {}: Status: {}. Body: {}", + url, status, body + ) + .into()); + } + Err(e) => return Err(format!("Error when fetching {}: {}", url, e).into()), + }; let status = resp.status(); let body = resp .into_string() 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/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(), + ); +} 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/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/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 71d2a2ca3..24cf50bdb 100644 --- a/server/src/handlers/download.rs +++ b/server/src/handlers/download.rs @@ -2,10 +2,9 @@ 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}; +use std::{collections::HashSet, path::PathBuf}; #[serde_with::serde_as] #[serde_with::skip_serializing_none] @@ -71,12 +70,17 @@ 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")] + { + 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()); + } + let format = get_format(params)?; + process_image(&file_path, &processed_file_path, params, &format)?; } - process_image(&file_path, &processed_file_path, params)?; - let file = NamedFile::open(processed_file_path)?; Ok(file.into_response(req)) } @@ -110,57 +114,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..8dd3d2d4d --- /dev/null +++ b/server/src/handlers/image.rs @@ -0,0 +1,60 @@ +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); + } + false +} + +pub fn process_image( + file_path: &PathBuf, + new_path: &PathBuf, + params: &DownloadParams, + format: &str, +) -> AtomicServerResult<()> { + 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()) +} 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;