diff --git a/Cargo.lock b/Cargo.lock index 2c9b54fbf..fd9adcfad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -582,10 +582,11 @@ dependencies = [ [[package]] name = "atomic_lib" -version = "0.15.0" +version = "0.16.0" dependencies = [ "base64 0.13.0", "bincode", + "dirs", "rand", "regex", "ring", @@ -594,6 +595,7 @@ dependencies = [ "serde", "serde_json", "sled", + "toml", "ureq", "url", ] @@ -2668,6 +2670,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" +dependencies = [ + "serde", +] + [[package]] name = "tracing" version = "0.1.18" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index aeaced001..965789938 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/joepio/atomic" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -atomic_lib = { version = "0.15.0", path = "../lib", features = ["db", "rdf"] } +atomic_lib = { version = "0.16.0", path = "../lib", features = ["config", "db", "rdf"] } promptly = "0.3.0" clap = "2.33.1" colored = "1.9.3" diff --git a/cli/README.md b/cli/README.md index b6c250dd3..6b83e2402 100644 --- a/cli/README.md +++ b/cli/README.md @@ -71,8 +71,11 @@ atomic new class - [x] Basic JSON Serialization - [x] RDF (Turtle / N-Triples / RDF/XML) Serialization - [x] Fetch data from the interwebs with `get` commands -- [ ] Works with [`atomic-server`](../server) (fetches from there, stores there, uses domain etc.) [#6](https://github.com/joepio/atomic/issues/6) -- [x] A `delta` command for manipulating existing resources +- [ ] Works with [`atomic-server`](../server) [#6](https://github.com/joepio/atomic/issues/6) + - [x] fetches data + - [ ] `set`, `remove` and `destroy` commands for commits + - [ ] `new` creates commits +- [x] A `delta` command for manipulating existing local resources - [ ] Tests for the cli - [ ] A `map` command for creating a bookmark and storing a copy diff --git a/cli/src/commit.rs b/cli/src/commit.rs new file mode 100644 index 000000000..23abfc35f --- /dev/null +++ b/cli/src/commit.rs @@ -0,0 +1,39 @@ +use crate::{Context, delta::argument_to_url}; +use atomic_lib::{errors::AtomicResult}; + +/// Apply a Commit using the Set method - create or update a value in a resource +pub fn set(context: &Context) -> AtomicResult<()> { + let subcommand = "set"; + let subcommand_matches = context.matches.subcommand_matches(subcommand).clone().unwrap(); + let subject = argument_to_url(context, subcommand, "subject")?; + let prop = argument_to_url(context, subcommand, "property")?; + let val = subcommand_matches.value_of("value").unwrap(); + let mut commit_builder = builder(context, subject); + commit_builder.set(prop, val.into()); + post(context, commit_builder)?; + Ok(()) +} + +/// Apply a Commit using the Remove method - removes a property from a resource +pub fn remove(context: &Context) -> AtomicResult<()> { + let subcommand = "remove"; + let subject = argument_to_url(context, subcommand, "subject")?; + let prop = argument_to_url(context, subcommand, "property")?; + let mut commit_builder = builder(context, subject); + commit_builder.remove(prop); + post(context, commit_builder)?; + Ok(()) +} + +fn builder(context: &Context, subject: String) -> atomic_lib::commit::CommitBuilder { + let write_ctx = context.get_write_context(); + atomic_lib::commit::CommitBuilder::new(subject, write_ctx.author_subject) +} + +/// Posts the Commit and applies it to the server +fn post(context: &Context , commit_builder: atomic_lib::commit::CommitBuilder) -> AtomicResult<()> { + let write_ctx = context.get_write_context(); + let commit = commit_builder.sign(&write_ctx.author_private_key)?; + atomic_lib::client::post_commit(&format!("{}commit", &write_ctx.base_url), &commit)?; + Ok(()) +} diff --git a/cli/src/delta.rs b/cli/src/delta.rs index a2a1ed656..89088c0b0 100644 --- a/cli/src/delta.rs +++ b/cli/src/delta.rs @@ -4,9 +4,9 @@ use atomic_lib::{delta::DeltaDeprecated, errors::AtomicResult, DeltaLine, Storel /// Processes a singe delta pub fn delta(context: &mut Context) -> AtomicResult<()> { let subcommand_matches = context.matches.subcommand_matches("delta").unwrap(); - let method = subcommand_to_url(context, "method")?; - let subject = subcommand_to_url(context, "subject")?; - let property = match subcommand_to_url(context, "property") { + let method = argument_to_url(context, "delta", "method")?; + let subject = argument_to_url(context, "delta", "subject")?; + let property = match argument_to_url(context, "delta", "property") { // If it's a valid URL, use that. Ok(prop) => Ok(prop), // If it's a shortname available from the Class of the resource, use that; @@ -34,9 +34,9 @@ pub fn delta(context: &mut Context) -> AtomicResult<()> { } /// Parses a single argument (URL or Bookmark), should return a valid URL -pub fn subcommand_to_url(context: &Context, subcommand: &str) -> AtomicResult { - let subcommand_matches = context.matches.subcommand_matches("delta").unwrap(); - let user_arg = subcommand_matches.value_of(subcommand).unwrap(); +pub fn argument_to_url(context: &Context, subcommand: &str, argument: &str) -> AtomicResult { + let subcommand_matches = context.matches.subcommand_matches(subcommand).unwrap(); + let user_arg = subcommand_matches.value_of(argument).ok_or(format!("No argument value for {} found", argument))?; let id_url: String = context .mapping.lock().unwrap() .try_mapping_or_url(&String::from(user_arg)) diff --git a/cli/src/main.rs b/cli/src/main.rs index 51559fbd9..73d6f2ae9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,23 +1,47 @@ -use path::SERIALIZE_OPTIONS; -use atomic_lib::{errors::AtomicResult, Storelike}; use atomic_lib::mapping::Mapping; -use clap::{App, AppSettings, Arg, ArgMatches, SubCommand, crate_version}; +use atomic_lib::{errors::AtomicResult, Storelike}; +use clap::{crate_version, App, AppSettings, Arg, ArgMatches, SubCommand}; use colored::*; use dirs::home_dir; +use path::SERIALIZE_OPTIONS; use std::{path::PathBuf, sync::Mutex}; +mod commit; mod delta; mod new; mod path; #[allow(dead_code)] pub struct Context<'a> { - base_url: String, store: atomic_lib::Store, mapping: Mutex, matches: ArgMatches<'a>, config_folder: PathBuf, user_mapping_path: PathBuf, + write: Option, +} + +impl Context<'_> { + pub fn get_write_context(&self) -> WriteContext { + match self.write { + Some(_) => { + self.write.clone().unwrap() + } + None => { + panic!("No write context set"); + } + } + } +} + +#[derive(Clone)] +pub struct WriteContext { + /// URL of the Atomic Server to write to + base_url: String, + /// URL of the Author of Commits + author_subject: String, + /// Private key of the Author of Commits + author_private_key: String, } fn main() -> AtomicResult<()> { @@ -82,9 +106,37 @@ fn main() -> AtomicResult<()> { .required(true) ) ) + .subcommand( + SubCommand::with_name("set") + .about("Update an Atom's value. Writes a commit to the store using the current Agent.") + .arg(Arg::with_name("subject") + .help("Subject URL or bookmark of the resourece") + .required(true) + ) + .arg(Arg::with_name("property") + .help("Property URL or shortname of the property") + .required(true) + ) + .arg(Arg::with_name("value") + .help("String representation of the Value to be changed") + .required(true) + ) + ) + .subcommand( + SubCommand::with_name("remove") + .about("Remove a single Atom from a Resource. Writes a commit to the store using the current Agent.") + .arg(Arg::with_name("subject") + .help("Subject URL or bookmark of the resource") + .required(true) + ) + .arg(Arg::with_name("property") + .help("Property URL or shortname of the property to be deleted") + .required(true) + ) + ) .subcommand( SubCommand::with_name("delta") - .about("Update the store using an single Delta", + .about("Update the store using an single Delta. Deprecated in favor of `set`, `remove` and `", ) .arg(Arg::with_name("method") .help("Method URL or bookmark, describes how the resource will be changed. Only suppports Insert at the time") @@ -132,22 +184,29 @@ fn main() -> AtomicResult<()> { } let store = atomic_lib::Store::init(); + let agent_config_path = atomic_lib::config::default_path()?; + let agent_config = atomic_lib::config::read_config(&agent_config_path)?; + let write_context = WriteContext { + base_url: agent_config.server, + author_private_key: agent_config.private_key, + author_subject: agent_config.agent, + }; let mut context = Context { // TODO: This should be configurable - base_url: "http://localhost/".into(), mapping: Mutex::new(mapping), store, matches, config_folder, user_mapping_path, + write: Some(write_context), }; exec_command(&mut context)?; Ok(()) } -fn exec_command(context: &mut Context) -> AtomicResult<()>{ +fn exec_command(context: &mut Context) -> AtomicResult<()> { match context.matches.subcommand_name() { Some("new") => { new::new(context)?; @@ -164,6 +223,12 @@ fn exec_command(context: &mut Context) -> AtomicResult<()>{ Some("delta") => { delta::delta(context)?; } + Some("set") => { + commit::set(context)?; + } + Some("remove") => { + commit::remove(context)?; + } Some("populate") => { populate(context)?; } @@ -192,8 +257,7 @@ fn list(context: &mut Context) { /// Prints a resource to the terminal with readble formatting and colors fn pretty_print_resource(url: &str, store: &mut dyn Storelike) -> AtomicResult<()> { let mut output = String::new(); - let resource = store - .get_resource_string(url)?; + let resource = store.get_resource_string(url)?; for (prop_url, val) in resource { let prop_shortname = store.property_url_to_shortname(&prop_url)?; output.push_str(&*format!( @@ -208,7 +272,7 @@ fn pretty_print_resource(url: &str, store: &mut dyn Storelike) -> AtomicResult<( } /// Triple Pattern Fragment Query -fn tpf(context: &mut Context) -> AtomicResult<()>{ +fn tpf(context: &mut Context) -> AtomicResult<()> { let subcommand_matches = context.matches.subcommand_matches("tpf").unwrap(); let subject = tpf_value(subcommand_matches.value_of("subject").unwrap()); let property = tpf_value(subcommand_matches.value_of("property").unwrap()); diff --git a/cli/src/new.rs b/cli/src/new.rs index 6139ffcaa..f318cf7e9 100644 --- a/cli/src/new.rs +++ b/cli/src/new.rs @@ -47,9 +47,11 @@ fn prompt_instance<'a>( // I think URL generation could be better, though. Perhaps use a let path = SystemTime::now().duration_since(UNIX_EPOCH)?.subsec_nanos(); - let mut subject = format!("{}/{}", context.base_url, path); + let write_ctx = context.get_write_context(); + + let mut subject = format!("{}/{}", write_ctx.base_url, path); if preffered_shortname.is_some() { - subject = format!("{}/{}-{}", context.base_url, path, preffered_shortname.clone().unwrap()); + subject = format!("{}/{}-{}", write_ctx.base_url, path, preffered_shortname.clone().unwrap()); } let mut new_resource: Resource = Resource::new(subject.clone(), &context.store); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index ca0a10a2b..7b21946e6 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,27 +1,30 @@ [package] -name = "atomic_lib" -version = "0.15.0" authors = ["Joep Meindertsma "] +description = "Library for creating, storing, querying, validating and converting Atomic Data." edition = "2018" license = "MIT" -description = "Library for creating, storing, querying, validating and converting Atomic Data." +name = "atomic_lib" readme = "README.md" repository = "https://github.com/joepio/atomic" +version = "0.16.0" [dependencies] +base64 = "0.13.0" +bincode = {version = "1.3.1", optional = true} +dirs = {version = "3.0.1", optional = true} +rand = "0.7.3" regex = "1.3.9" +ring = "0.16.15" +rio_api = {version = "0.5.0", optional = true} +rio_turtle = {version = "0.5.0", optional = true} +serde = {version = "1.0.114", features = ["derive"]} serde_json = "1.0.57" -serde = { version = "1.0.114", features = ["derive"] } sled = {version = "0.34.3", optional = true} -bincode = {version = "1.3.1", optional = true} +toml = {version = "0.5.7", optional = true} ureq = "1.4.0" -rio_turtle = { version = "0.5.0", optional = true} -rio_api = { version = "0.5.0", optional = true} -rand = "0.7.3" -ring = "0.16.15" -base64 = "0.13.0" url = "2.1.1" [features] +config = ["dirs", "toml"] db = ["sled", "bincode"] rdf = ["rio_api", "rio_turtle"] diff --git a/lib/defaults/default_store.ad3 b/lib/defaults/default_store.ad3 index 942dfca07..73c646ef7 100644 --- a/lib/defaults/default_store.ad3 +++ b/lib/defaults/default_store.ad3 @@ -26,7 +26,7 @@ ["https://atomicdata.dev/classes/Agent","https://atomicdata.dev/properties/description","An Agent is a user that can create or modify data. It has two keys: a private and a public one. The private key should be kept secret. The publik key is for proving that the "] ["https://atomicdata.dev/classes/Agent","https://atomicdata.dev/properties/shortname","agent"] ["https://atomicdata.dev/classes/Collection","https://atomicdata.dev/properties/isA","[\"https://atomicdata.dev/classes/Class\"]"] -["https://atomicdata.dev/classes/Collection","https://atomicdata.dev/properties/recommends","[\"https://atomicdata.dev/properties/collection/property\"]"] +["https://atomicdata.dev/classes/Collection","https://atomicdata.dev/properties/recommends","[\"https://atomicdata.dev/properties/collection/property\",\"https://atomicdata.dev/properties/collection/value\",\"https://atomicdata.dev/properties/collection/pageSize\",\"https://atomicdata.dev/properties/collection/members\",\"https://atomicdata.dev/properties/collection/totalPages\",\"https://atomicdata.dev/properties/collection/currentPage\"]"] ["https://atomicdata.dev/classes/Collection","https://atomicdata.dev/properties/description","A paginated set of resources that can be sorted. Accepts query parameters for setting the current page number, page size, sort attribute, sort direction"] ["https://atomicdata.dev/classes/Collection","https://atomicdata.dev/properties/shortname","collection"] # Datatypes @@ -142,10 +142,10 @@ ["https://atomicdata.dev/properties/collection/members","https://atomicdata.dev/properties/datatype","https://atomicdata.dev/datatypes/resourceArray"] ["https://atomicdata.dev/properties/collection/members","https://atomicdata.dev/properties/shortname","members"] ["https://atomicdata.dev/properties/collection/members","https://atomicdata.dev/properties/description","The members are the list of resources in a collection."] -["https://atomicdata.dev/properties/collection/itemCount","https://atomicdata.dev/properties/isA","[\"https://atomicdata.dev/classes/Property\"]"] -["https://atomicdata.dev/properties/collection/itemCount","https://atomicdata.dev/properties/datatype","https://atomicdata.dev/datatypes/integer"] -["https://atomicdata.dev/properties/collection/itemCount","https://atomicdata.dev/properties/shortname","item-count"] -["https://atomicdata.dev/properties/collection/itemCount","https://atomicdata.dev/properties/description","The total number of items in the collection."] +["https://atomicdata.dev/properties/collection/totalMembers","https://atomicdata.dev/properties/isA","[\"https://atomicdata.dev/classes/Property\"]"] +["https://atomicdata.dev/properties/collection/totalMembers","https://atomicdata.dev/properties/datatype","https://atomicdata.dev/datatypes/integer"] +["https://atomicdata.dev/properties/collection/totalMembers","https://atomicdata.dev/properties/shortname","total-members"] +["https://atomicdata.dev/properties/collection/totalMembers","https://atomicdata.dev/properties/description","The count of items (members) in the collection."] ["https://atomicdata.dev/properties/collection/totalPages","https://atomicdata.dev/properties/isA","[\"https://atomicdata.dev/classes/Property\"]"] ["https://atomicdata.dev/properties/collection/totalPages","https://atomicdata.dev/properties/datatype","https://atomicdata.dev/datatypes/integer"] ["https://atomicdata.dev/properties/collection/totalPages","https://atomicdata.dev/properties/shortname","total-pages"] @@ -154,6 +154,10 @@ ["https://atomicdata.dev/properties/collection/currentPage","https://atomicdata.dev/properties/datatype","https://atomicdata.dev/datatypes/integer"] ["https://atomicdata.dev/properties/collection/currentPage","https://atomicdata.dev/properties/shortname","current-page"] ["https://atomicdata.dev/properties/collection/currentPage","https://atomicdata.dev/properties/description","The curent page number of the collection. Defaults to 0."] +["https://atomicdata.dev/properties/collection/pageSize","https://atomicdata.dev/properties/isA","[\"https://atomicdata.dev/classes/Property\"]"] +["https://atomicdata.dev/properties/collection/pageSize","https://atomicdata.dev/properties/datatype","https://atomicdata.dev/datatypes/integer"] +["https://atomicdata.dev/properties/collection/pageSize","https://atomicdata.dev/properties/shortname","page-size"] +["https://atomicdata.dev/properties/collection/pageSize","https://atomicdata.dev/properties/description","The amount of members per page."] # Collections ["https://atomicdata.dev/classes","https://atomicdata.dev/properties/isA","[\"https://atomicdata.dev/classes/Collection\"]"] ["https://atomicdata.dev/classes","https://atomicdata.dev/properties/collection/property","https://atomicdata.dev/properties/isA"] diff --git a/lib/src/agents.rs b/lib/src/agents.rs new file mode 100644 index 000000000..6ace8b72d --- /dev/null +++ b/lib/src/agents.rs @@ -0,0 +1,22 @@ +//! Logic for Agents - which are like Users + +/// PKCS#8 keypair, serialized using base64 +pub struct Pair { + pub private: String, + pub public: String, +} + +/// Returns a new random PKCS#8 keypair. +pub fn generate_keypair() -> Pair { + use ring::signature::KeyPair; + let rng = ring::rand::SystemRandom::new(); + let pkcs8_bytes = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng) + .map_err(|_| "Error generating seed").unwrap(); + let key_pair = ring::signature::Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()) + .map_err(|_| "Error generating keypair").unwrap(); + let pair = Pair { + private: base64::encode(pkcs8_bytes.as_ref()), + public: base64::encode(key_pair.public_key().as_ref()), + }; + pair +} diff --git a/lib/src/client.rs b/lib/src/client.rs index 72386b1f3..2e1945e7f 100644 --- a/lib/src/client.rs +++ b/lib/src/client.rs @@ -28,6 +28,7 @@ pub fn fetch_resource(subject: &str) -> AtomicResult { } /// Posts a Commit to an endpoint +/// Default commit endpoint is `https://example.com/commit` pub fn post_commit(endpoint: &str, commit: &crate::Commit) -> AtomicResult<()> { let json = serde_json::to_string(commit)?; @@ -37,7 +38,7 @@ pub fn post_commit(endpoint: &str, commit: &crate::Commit) -> AtomicResult<()> { .send_string(&json); if resp.error() { - Err(format!("Failed sending commit. Status: {} Body: {}", resp.status(), resp.into_string()?).into()) + Err(format!("Failed applying commit. Status: {} Body: {}", resp.status(), resp.into_string()?).into()) } else { Ok(()) } diff --git a/lib/src/collections.rs b/lib/src/collections.rs index 6014a0297..7a4be5d90 100644 --- a/lib/src/collections.rs +++ b/lib/src/collections.rs @@ -128,9 +128,13 @@ impl Collection { resource.set_propval(crate::urls::COLLECTION_VALUE.into(), prop.into())?; } resource.set_propval( - crate::urls::COLLECTION_ITEM_COUNT.into(), + crate::urls::COLLECTION_MEMBER_COUNT.into(), self.total_items.clone().into(), )?; + resource.set_propval_string( + crate::urls::IS_A.into(), + crate::urls::COLLECTION, + )?; resource.set_propval( crate::urls::COLLECTION_TOTAL_PAGES.into(), self.total_pages.clone().into(), @@ -233,13 +237,13 @@ mod test { println!( "Count is {}", collection - .get(urls::COLLECTION_ITEM_COUNT) + .get(urls::COLLECTION_MEMBER_COUNT) .unwrap() .to_string() ); assert!( collection - .get(urls::COLLECTION_ITEM_COUNT) + .get(urls::COLLECTION_MEMBER_COUNT) .unwrap() .to_string() == "6" @@ -271,6 +275,13 @@ mod test { .to_string() == "1" ); + assert!( + collection_page_nr + .get(urls::IS_A) + .unwrap() + .to_string() + == urls::COLLECTION + ); let members_vec = match collection_page_nr.get(urls::COLLECTION_MEMBERS).unwrap() { crate::Value::ResourceArray(vec) => vec, _ => panic!(), diff --git a/lib/src/commit.rs b/lib/src/commit.rs index 8b06c355a..1ea0a5d86 100644 --- a/lib/src/commit.rs +++ b/lib/src/commit.rs @@ -44,6 +44,12 @@ impl Commit { urls::SUBJECT.into(), Value::new(&self.subject, &DataType::AtomicUrl).unwrap(), )?; + let mut classes: Vec = Vec::new(); + classes.push(urls::COMMIT.into()); + resource.set_propval( + urls::IS_A.into(), + classes.into() + )?; resource.set_propval( urls::CREATED_AT.into(), Value::new(&self.created_at.to_string(), &DataType::Timestamp).unwrap(), @@ -141,6 +147,8 @@ pub struct CommitBuilder { } impl CommitBuilder { + /// Use this to start constructing a Commit. + /// The signer is the URL of the Author, which contains the public key. pub fn new(subject: String, signer: String) -> Self { CommitBuilder { subject, diff --git a/lib/src/config.rs b/lib/src/config.rs new file mode 100644 index 000000000..25a6e3f8b --- /dev/null +++ b/lib/src/config.rs @@ -0,0 +1,35 @@ +//! Configuration logic which can be used in both CLI and Server contexts +use std::path::PathBuf; +use serde::{Serialize, Deserialize}; +use crate::errors::AtomicResult; + +/// A set of options that are shared between CLI and Server contexts +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Config { + /// Companion Atomic Server, where data is written by default. + pub server: String, + /// The current Agent (user) URL. Usually lives on the server, but not necessarily so. + pub agent: String, + /// Private key for the Agent, which is used to sign commits. + pub private_key: String, +} + +/// Returns the default path for the config file: `~/.config/atomic/config.toml` +pub fn default_path () -> AtomicResult { + Ok(dirs::home_dir().ok_or("Could not open home dir")?.join(".config/atomic/config.toml")) +} + +/// Reads config file from a specified path +pub fn read_config(path: &PathBuf) -> AtomicResult { + let config_string = std::fs::read_to_string(path)?; + let config: Config = toml::from_str(&config_string).unwrap(); + Ok(config) +} + +/// Writes config file from a specified path +/// Overwrites any existing config +pub fn write_config(path: &PathBuf, config: Config) -> AtomicResult<()> { + let out = toml::to_string_pretty(&config).map_err(|e| format!("Error serializing config. {}", e))?; + std::fs::write(path, out)?; + Ok(()) +} diff --git a/lib/src/db.rs b/lib/src/db.rs index e0211df7f..a331121e4 100644 --- a/lib/src/db.rs +++ b/lib/src/db.rs @@ -35,13 +35,14 @@ impl Db { let resources = db.open_tree("resources")?; let index_props = db.open_tree("index_props")?; let index_vals = db.open_tree("index_vals")?; - Ok(Db { + let store = Db { db, resources, index_vals, index_props, base_url, - }) + }; + Ok(store) } // fn index_value_add(&mut self, atom: Atom) -> AtomicResult<()> { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 6a679c53e..27da6502a 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -34,12 +34,15 @@ assert!(found_atoms.len() == 1); ``` */ +pub mod agents; pub mod atoms; pub mod client; pub mod collections; pub mod commit; #[cfg(feature = "db")] pub mod db; +#[cfg(feature = "config")] +pub mod config; pub mod delta; pub mod datatype; pub mod errors; diff --git a/lib/src/resources.rs b/lib/src/resources.rs index 47bc15951..b65e46194 100644 --- a/lib/src/resources.rs +++ b/lib/src/resources.rs @@ -166,6 +166,7 @@ impl<'a> Resource<'a> { /// Inserts a Property/Value combination. /// Overwrites existing. + /// Does not validate property / datatype combination pub fn set_propval(&mut self, property: String, value: Value) -> AtomicResult<()> { self.propvals.insert(property, value); Ok(()) diff --git a/lib/src/storelike.rs b/lib/src/storelike.rs index 2615652d3..a1006a4d0 100644 --- a/lib/src/storelike.rs +++ b/lib/src/storelike.rs @@ -146,21 +146,14 @@ pub trait Storelike { where Self: std::marker::Sized, { - use ring::signature::KeyPair; let subject = format!("{}agents/{}", self.get_base_url(), name); - let rng = ring::rand::SystemRandom::new(); - let pkcs8_bytes = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng) - .map_err(|_| "Error generating seed")?; - let key_pair = ring::signature::Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()) - .map_err(|_| "Error generating keypair")?; + let keypair = crate::agents::generate_keypair(); let mut agent = Resource::new_instance(urls::AGENT, self)?; - let pubkey = base64::encode(key_pair.public_key().as_ref()); - let private_key = base64::encode(pkcs8_bytes.as_ref()); agent.set_subject(subject.clone()); agent.set_by_shortname("name", name)?; - agent.set_by_shortname("publickey", &pubkey)?; + agent.set_by_shortname("publickey", &keypair.public)?; self.add_resource(&agent)?; - Ok((subject, private_key)) + Ok((subject, keypair.private)) } /// Fetches a resource, makes sure its subject matches. @@ -184,7 +177,7 @@ pub trait Storelike { for (prop_string, val_string) in resource_string { let propertyfull = self.get_property(&prop_string)?; let fullvalue = - Value::new(&val_string, &propertyfull.data_type).expect("Could not convert value"); + Value::new(&val_string, &propertyfull.data_type)?; res.set_propval(prop_string.clone(), fullvalue)?; } Ok(res) @@ -298,7 +291,10 @@ pub trait Storelike { all_pages.push(Vec::new()) } // Maybe I should default to last page, if current_page is too high? - let members = all_pages.get(current_page).ok_or("Page number is too high")?.clone(); + let members = all_pages + .get(current_page) + .ok_or("Page number is too high")? + .clone(); let total_items = sorted_subjects.len(); // Construct the pages (TODO), use pageSize let total_pages = (total_items + collection.page_size - 1) / collection.page_size; @@ -324,15 +320,15 @@ pub trait Storelike { data_type: match_datatype( &property_resource .get(urls::DATATYPE_PROP) - .ok_or(format!("Datatype not found for property {}", url))?, + .ok_or(format!("Datatype not found for Property {}.", url))?, ), shortname: property_resource .get(urls::SHORTNAME) - .ok_or(format!("Shortname not found for property {}", url))? + .ok_or(format!("Shortname not found for Property {}", url))? .into(), description: property_resource .get(urls::DESCRIPTION) - .ok_or(format!("Description not found for property {}", url))? + .ok_or(format!("Description not found for Property {}", url))? .into(), class_type: property_resource.get(urls::CLASSTYPE_PROP).cloned(), subject: url.into(), @@ -355,8 +351,10 @@ pub trait Storelike { let mut resource = self.get_resource(&removed_query_params)?; for class in resource.get_classes()? { match class.subject.as_ref() { - urls::COLLECTION => return crate::collections::construct_collection(self, query_params, resource), - _ => {}, + urls::COLLECTION => { + return crate::collections::construct_collection(self, query_params, resource) + } + _ => {} } } Ok(resource) diff --git a/lib/src/urls.rs b/lib/src/urls.rs index 49aae8720..27c047c84 100644 --- a/lib/src/urls.rs +++ b/lib/src/urls.rs @@ -31,7 +31,7 @@ pub const PUBLIC_KEY: &str = "https://atomicdata.dev/properties/publicKey"; // ... for Collections pub const COLLECTION_PROPERTY: &str = "https://atomicdata.dev/properties/collection/property"; pub const COLLECTION_VALUE: &str = "https://atomicdata.dev/properties/collection/value"; -pub const COLLECTION_ITEM_COUNT: &str = "https://atomicdata.dev/properties/collection/itemCount"; +pub const COLLECTION_MEMBER_COUNT: &str = "https://atomicdata.dev/properties/collection/totalMembers"; pub const COLLECTION_TOTAL_PAGES: &str = "https://atomicdata.dev/properties/collection/totalPages"; pub const COLLECTION_CURRENT_PAGE: &str = "https://atomicdata.dev/properties/collection/currentPage"; pub const COLLECTION_MEMBERS: &str = "https://atomicdata.dev/properties/collection/members"; diff --git a/server/Cargo.toml b/server/Cargo.toml index 0789735b0..7867da914 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/joepio/atomic-cli" [dependencies] # When publishing to cargo this needs to be set, but it needs to be absent when building docker file -atomic_lib = { version = "0.15.0", path = "../lib", features = ["db", "rdf"] } +atomic_lib = { version = "0.16.0", path = "../lib", features = ["config", "db", "rdf"] } serde_json = "1.0.56" serde = { version = "1.0.114", features = ["derive"] } uuid = {version = "0.8.1", features = ["v4"]} diff --git a/server/example_requests.http b/server/example_requests.http index 6dd52cf9c..c42e328b0 100644 --- a/server/example_requests.http +++ b/server/example_requests.http @@ -24,9 +24,9 @@ Accept: application/json Content-Type: application/json { - "subject": "http://0.0.0.0:8081/ttest", + "subject": "http://localhost:8081/test", "created_at": 1601239744, - "actor": "https://example.com/profile", + "signer": "http://localhost:8081/agents/root", "set": { "https://atomicdata.dev/properties/requires": "[\"http/properties/requires\"]" }, diff --git a/server/src/appstate.rs b/server/src/appstate.rs index 044f6647c..574ef3d7e 100644 --- a/server/src/appstate.rs +++ b/server/src/appstate.rs @@ -21,10 +21,26 @@ pub struct AppState { pub fn init(config: Config) -> BetterResult { let store = atomic_lib::Db::init(&config.store_path, config.local_base_url.clone())?; store.populate()?; - let mapping = Mapping::init(); - let tera = Tera::new("templates/*.html")?; + // Create a new identity if it does not yet exist. + + let path = atomic_lib::config::default_path()?; + match atomic_lib::config::read_config(&path) { + Ok(agent_config) => { + store.get_resource(&agent_config.agent).expect(&format!("An agent is present in {:?}, but this agent is not present in the store", path)); + } + Err(_) => { + let (agent, private_key) = store.create_agent("root")?; + let cfg = atomic_lib::config::Config { + agent, + server: config.local_base_url.clone(), + private_key + }; + atomic_lib::config::write_config(&path, cfg)?; + log::info!("Agent created. Check newly created config file: {:?}", path); + } + } Ok(AppState { store, diff --git a/server/src/handlers/commit.rs b/server/src/handlers/commit.rs index 2ef63fd32..1d3ba381f 100644 --- a/server/src/handlers/commit.rs +++ b/server/src/handlers/commit.rs @@ -16,6 +16,7 @@ pub async fn post_commit( let store = &mut context.store; let mut builder = HttpResponse::Ok(); let commit_resource = store.commit(commit.into_inner())?; - let body = format!("Commit succesfully applied. Can be seen at {}", commit_resource.get_subject()); - Ok(builder.body(body)) + let message = format!("Commit succesfully applied. Can be seen at {}", commit_resource.get_subject()); + log::info!("{}", &message); + Ok(builder.body(message)) } diff --git a/server/templates/base.html b/server/templates/base.html index 5108b7715..5303b32f7 100644 --- a/server/templates/base.html +++ b/server/templates/base.html @@ -18,6 +18,7 @@ Atomic Data /tpf /validate + /collections /path docs