From bf4f89029800f0aeddd22395e783ba79931f8c6f Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Tue, 18 Feb 2025 13:54:34 +0100 Subject: [PATCH] - 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;