Skip to content

Commit

Permalink
#288 #489 Register Endpoint and true URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
joepio committed Nov 9, 2022
1 parent f20b2c6 commit 5dd3729
Show file tree
Hide file tree
Showing 20 changed files with 225 additions and 95 deletions.
2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ bincode = {version = "1", optional = true}
directories = {version = ">= 2, < 5", optional = true}
html2md = {version = "0.2.13", optional = true}
kuchiki = {version = "0.8.1", optional = true}
lazy_static = "1"
lol_html = {version = "0.3.1", optional = true}
rand = {version = "0.8"}
regex = "1"
Expand All @@ -39,7 +40,6 @@ urlencoding = "2"
[dev-dependencies]
criterion = "0.3"
iai = "0.1"
lazy_static = "1"
ntest = "0.7"

[features]
Expand Down
2 changes: 1 addition & 1 deletion lib/src/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ pub fn create_collection_resource_for_class(

// Let the Collections collection be the top level item
let parent = if class.subject == urls::COLLECTION {
drive
drive.to_string()
} else {
format!("{}/collections", drive)
};
Expand Down
2 changes: 1 addition & 1 deletion lib/src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ impl Commit {
let default_parent = store.get_self_url().ok_or("There is no self_url set, and no parent in the Commit. The commit can not be applied.")?;
resource_old.set_propval(
urls::PARENT.into(),
Value::AtomicUrl(default_parent),
Value::AtomicUrl(default_parent.to_string()),
store,
)?;
}
Expand Down
28 changes: 16 additions & 12 deletions lib/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use std::{
sync::{Arc, Mutex},
};

use tracing::{info, instrument};
use tracing::instrument;
use url::Url;

use crate::{
atoms::IndexAtom,
Expand Down Expand Up @@ -75,7 +76,7 @@ pub struct Db {
/// A list of all the Collections currently being used. Is used to update `query_index`.
watched_queries: sled::Tree,
/// The address where the db will be hosted, e.g. http://localhost/
server_url: String,
server_url: Url,
/// Endpoints are checked whenever a resource is requested. They calculate (some properties of) the resource and return it.
endpoints: Vec<Endpoint>,
/// Function called whenever a Commit is applied.
Expand Down Expand Up @@ -192,14 +193,15 @@ impl Db {
}

fn map_sled_item_to_resource(
&self,
item: Result<(sled::IVec, sled::IVec), sled::Error>,
self_url: String,
include_external: bool,
) -> Option<Resource> {
let (subject, resource_bin) = item.expect(DB_CORRUPT_MSG);
let subject: String = String::from_utf8_lossy(&subject).to_string();

if !include_external && !subject.starts_with(&self_url) {
if !include_external && self.is_external_subject(&subject).ok()? {
return None;
}

Expand Down Expand Up @@ -307,14 +309,14 @@ impl Storelike for Db {
Ok(())
}

fn get_server_url(&self) -> &str {
fn get_server_url(&self) -> &Url {
&self.server_url
}

// Since the DB is often also the server, this should make sense.
// Some edge cases might appear later on (e.g. a slave DB that only stores copies?)
fn get_self_url(&self) -> Option<String> {
Some(self.get_server_url().into())
fn get_self_url(&self) -> Option<&Url> {
// Since the DB is often also the server, this should make sense.
// Some edge cases might appear later on (e.g. a slave DB that only stores copies?)
Some(self.get_server_url())
}

fn get_default_agent(&self) -> AtomicResult<crate::agents::Agent> {
Expand Down Expand Up @@ -486,7 +488,7 @@ impl Storelike for Db {
// Maybe make this optional?
q_filter.watch(self)?;

info!(filter = ?q_filter, "Building query index");
tracing::info!(filter = ?q_filter, "Building query index");

let atoms: IndexIterator = match (&q.property, q.value.as_ref()) {
(Some(prop), val) => find_in_prop_val_sub_index(self, prop, val),
Expand Down Expand Up @@ -525,7 +527,7 @@ impl Storelike for Db {
.expect("No self URL set, is required in DB");

let result = self.resources.into_iter().filter_map(move |item| {
Db::map_sled_item_to_resource(item, self_url.clone(), include_external)
Db::map_sled_item_to_resource(self, item, self_url.to_string(), include_external)
});

Box::new(result)
Expand All @@ -538,9 +540,11 @@ impl Storelike for Db {
// This is a potentially expensive operation, but is needed to make TPF queries work with the models created in here
self.build_index(true)
.map_err(|e| format!("Failed to build index. {}", e))?;
crate::populate::create_drive(self)
let default_agent = self
.get_default_agent()
.map_err(|_| "No default agent found")?;
crate::populate::create_drive(self, None, &default_agent.subject, true)
.map_err(|e| format!("Failed to create drive. {}", e))?;
crate::populate::set_drive_rights(self, true)?;
crate::populate::populate_collections(self)
.map_err(|e| format!("Failed to populate collections. {}", e))?;
crate::populate::populate_endpoints(self)
Expand Down
6 changes: 1 addition & 5 deletions lib/src/db/query_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,6 @@ pub fn query_indexed(store: &Db, q: &Query) -> AtomicResult<QueryResult> {
let mut resources = Vec::new();
let mut count = 0;

let self_url = store
.get_self_url()
.ok_or("No self_url set, required for Queries")?;

let limit = if let Some(limit) = q.limit {
limit
} else {
Expand All @@ -114,7 +110,7 @@ pub fn query_indexed(store: &Db, q: &Query) -> AtomicResult<QueryResult> {
let (_q_filter, _val, subject) = parse_collection_members_key(&k)?;

// If no external resources should be included, skip this one if it's an external resource
if !q.include_external && !subject.starts_with(&self_url) {
if !q.include_external && store.is_external_subject(subject)? {
continue;
}

Expand Down
1 change: 1 addition & 0 deletions lib/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub fn default_endpoints() -> Vec<Endpoint> {
plugins::path::path_endpoint(),
plugins::search::search_endpoint(),
plugins::files::upload_endpoint(),
plugins::register::register_endpoint(),
#[cfg(feature = "html")]
plugins::bookmark::bookmark_endpoint(),
]
Expand Down
1 change: 1 addition & 0 deletions lib/src/plugins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ pub mod invite;
pub mod bookmark;
pub mod files;
pub mod path;
pub mod register;
pub mod search;
pub mod versioning;
72 changes: 72 additions & 0 deletions lib/src/plugins/register.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! Creates a new Drive and optionally also an Agent.
use crate::{agents::Agent, endpoints::Endpoint, errors::AtomicResult, urls, Resource, Storelike};

pub fn register_endpoint() -> Endpoint {
Endpoint {
path: "/register".to_string(),
params: [
urls::INVITE_PUBKEY.to_string(),
urls::NAME.to_string(),
].into(),
description: "Allows new users to easily, in one request, make both an Agent and a Drive. This drive will be created at the subdomain of `name`.".to_string(),
shortname: "register".to_string(),
handle: Some(construct_register_redirect),
}
}

#[tracing::instrument(skip(store))]
pub fn construct_register_redirect(
url: url::Url,
store: &impl Storelike,
for_agent: Option<&str>,
) -> AtomicResult<Resource> {
let requested_subject = url.to_string();
let mut pub_key = None;
let mut name_option = None;
for (k, v) in url.query_pairs() {
match k.as_ref() {
"public-key" | urls::INVITE_PUBKEY => pub_key = Some(v.to_string()),
"name" | urls::NAME => name_option = Some(v.to_string()),
_ => {}
}
}
if pub_key.is_none() && name_option.is_none() {
return register_endpoint().to_resource(store);
}

let name = if let Some(n) = name_option {
n
} else {
return Err("No name provided".into());
};

let mut new_agent = None;

let drive_creator_agent: String = if let Some(key) = pub_key {
let new = Agent::new_from_public_key(store, &key)?.subject;
new_agent = Some(new.clone());
new
} else if let Some(agent) = for_agent {
agent.to_string()
} else {
return Err("No `public-key` provided".into());
};

// Create the new Drive
let drive = crate::populate::create_drive(store, Some(&name), &drive_creator_agent, false)?;

// Construct the Redirect Resource, which might provide the Client with a Subject for his Agent.
let mut redirect = Resource::new_instance(urls::REDIRECT, store)?;
redirect.set_propval_string(urls::DESTINATION.into(), drive.get_subject(), store)?;
if let Some(agent) = new_agent {
redirect.set_propval(
urls::REDIRECT_AGENT.into(),
crate::Value::AtomicUrl(agent),
store,
)?;
}
// The front-end requires the @id to be the same as requested
redirect.set_subject(requested_subject);
Ok(redirect)
}
71 changes: 47 additions & 24 deletions lib/src/populate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,32 +151,49 @@ pub fn populate_base_models(store: &impl Storelike) -> AtomicResult<()> {
Ok(())
}

/// Creates a Drive resource at the base URL. Does not set rights. Use set_drive_rights for that.
pub fn create_drive(store: &impl Storelike) -> AtomicResult<()> {
let self_url = store
.get_self_url()
.ok_or("No self_url set, cannot populate store with Drive")?;
let mut drive = store.get_resource_new(&self_url);
/// Creates a Drive resource at the base URL if no name is passed.
#[tracing::instrument(skip(store), level = "info")]
pub fn create_drive(
store: &impl Storelike,
drive_name: Option<&str>,
for_agent: &str,
public_read: bool,
) -> AtomicResult<Resource> {
let mut self_url = if let Some(url) = store.get_self_url() {
url.to_owned()
} else {
return Err("No self URL set. Cannot create drive.".into());
};
let drive_subject: String = if let Some(name) = drive_name {
// Let's make a subdomain
let host = self_url.host().expect("No host in server_url");
let subdomain_host = format!("{}.{}", name, host);
self_url.set_host(Some(&subdomain_host))?;
self_url.to_string()
} else {
self_url.to_string()
};

let mut drive = if drive_name.is_some() {
if store.get_resource(&drive_subject).is_ok() {
return Err("Drive URL is already taken".into());
}
Resource::new(drive_subject)
} else {
// Only for the base URL (of no drive name is passed), we should not check if the drive exists.
// This is because we use `create_drive` in the `--initialize` command.
store.get_resource_new(&drive_subject)
};
drive.set_class(urls::DRIVE);
let server_url = url::Url::parse(store.get_server_url())?;
drive.set_propval_string(
urls::NAME.into(),
server_url.host_str().ok_or("Can't use current base URL")?,
drive_name.unwrap_or_else(|| self_url.host_str().unwrap()),
store,
)?;
drive.save_locally(store)?;
Ok(())
}

/// 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 write_agent = store.get_default_agent()?.subject;
let read_agent = write_agent.clone();

drive.push_propval(urls::WRITE, write_agent.into(), true)?;
drive.push_propval(urls::READ, read_agent.into(), true)?;
// Set rights
drive.push_propval(urls::WRITE, for_agent.into(), true)?;
drive.push_propval(urls::READ, for_agent.into(), true)?;
if public_read {
drive.push_propval(urls::READ, urls::PUBLIC_AGENT.into(), true)?;
}
Expand All @@ -189,8 +206,10 @@ Register your Agent by visiting [`/setup`]({}/setup). After that, edit this page
Note that, by default, all resources are `public`. You can edit this by opening the context menu (the three dots in the navigation bar), and going to `share`.
"#, store.get_server_url()), store)?;
}

drive.save_locally(store)?;
Ok(())

Ok(drive)
}

/// Imports the Atomic Data Core items (the entire atomicdata.dev Ontology / Vocabulary)
Expand Down Expand Up @@ -251,9 +270,13 @@ pub fn populate_importer(store: &crate::Db) -> AtomicResult<()> {
let base = store
.get_self_url()
.ok_or("No self URL in this Store - required for populating importer")?;
let mut importer = Resource::new(urls::construct_path_import(&base));
let mut importer = Resource::new(urls::construct_path_import(base));
importer.set_class(urls::IMPORTER);
importer.set_propval(urls::PARENT.into(), Value::AtomicUrl(base), store)?;
importer.set_propval(
urls::PARENT.into(),
Value::AtomicUrl(base.to_string()),
store,
)?;
importer.set_propval(urls::NAME.into(), Value::String("Import".into()), store)?;
importer.save_locally(store)?;
Ok(())
Expand All @@ -264,7 +287,7 @@ pub fn populate_importer(store: &crate::Db) -> AtomicResult<()> {
/// Useful for helping a new user get started.
pub fn populate_sidebar_items(store: &crate::Db) -> AtomicResult<()> {
let base = store.get_self_url().ok_or("No self_url")?;
let mut drive = store.get_resource(&base)?;
let mut drive = store.get_resource(base.as_str())?;
let arr = vec![
format!("{}/setup", base),
format!("{}/import", base),
Expand Down
8 changes: 1 addition & 7 deletions lib/src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,7 @@ impl Resource {
let commit_builder = self.get_commit_builder().clone();
let commit = commit_builder.sign(&agent, store, self)?;
// If the current client is a server, and the subject is hosted here, don't post
let should_post = if let Some(self_url) = store.get_self_url() {
!self.subject.starts_with(&self_url)
} else {
// Current client is not a server, has no own persisted store
true
};
if should_post {
if store.is_external_subject(&commit.subject)? {
crate::client::post_commit(&commit, store)?;
}
let opts = CommitOpts {
Expand Down
12 changes: 8 additions & 4 deletions lib/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ pub struct Store {
default_agent: Arc<Mutex<Option<crate::agents::Agent>>>,
}

lazy_static::lazy_static! {
static ref LOCAL_STORE_URL: Url = Url::parse("local:store").unwrap();
}

impl Store {
/// Creates an empty Store.
/// Run `.populate()` to get useful standard models loaded into your store.
Expand Down Expand Up @@ -180,14 +184,14 @@ impl Storelike for Store {
)
}

fn get_server_url(&self) -> &str {
fn get_server_url(&self) -> &Url {
// TODO Should be implemented later when companion functionality is here
// https://github.com/atomicdata-dev/atomic-data-rust/issues/6
"local:store"
&LOCAL_STORE_URL
}

fn get_self_url(&self) -> Option<String> {
Some(self.get_server_url().into())
fn get_self_url(&self) -> Option<&Url> {
Some(self.get_server_url())
}

fn get_default_agent(&self) -> AtomicResult<crate::agents::Agent> {
Expand Down
Loading

0 comments on commit 5dd3729

Please sign in to comment.