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 Jul 31, 2023
1 parent 3462b2c commit b60a355
Show file tree
Hide file tree
Showing 18 changed files with 195 additions and 73 deletions.
1 change: 1 addition & 0 deletions 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.14", 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 Down
2 changes: 1 addition & 1 deletion lib/src/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,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 @@ -178,7 +178,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
23 changes: 13 additions & 10 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::{
agents::ForAgent,
Expand Down Expand Up @@ -76,7 +77,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 @@ -193,14 +194,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 @@ -308,14 +310,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 @@ -479,7 +481,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 @@ -523,7 +525,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 Down Expand Up @@ -577,6 +579,7 @@ impl Storelike for Db {

fn populate(&self) -> AtomicResult<()> {
crate::populate::populate_all(self)
crate::populate::create_drive(self, None, &default_agent.subject, true)
}

#[instrument(skip(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 @@ -100,10 +100,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 @@ -119,7 +115,7 @@ pub fn query_indexed(store: &Db, q: &Query) -> AtomicResult<QueryResult> {
let (k, _v) = kv.map_err(|_e| "Unable to parse query_cached")?;
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 @@ -81,6 +81,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(),
plugins::importer::import_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 @@ -44,5 +44,6 @@ pub mod bookmark;
pub mod files;
pub mod path;
pub mod query;
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)
}
72 changes: 48 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 @@ -259,7 +278,11 @@ pub fn populate_importer(store: &crate::Db) -> AtomicResult<()> {
.ok_or("No self URL in this Store - required for populating importer")?;
let mut importer = crate::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 @@ -270,7 +293,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 All @@ -289,7 +312,8 @@ pub fn populate_all(store: &crate::Db) -> AtomicResult<()> {
// populate_base_models should be run in init, instead of here, since it will result in infinite loops without
populate_default_store(store)
.map_err(|e| format!("Failed to populate default store. {}", e))?;
create_drive(store).map_err(|e| format!("Failed to create drive. {}", e))?;
create_drive(self, None, &default_agent.subject, true)
.map_err(|e| format!("Failed to create drive. {}", e))?;
set_drive_rights(store, true)?;
populate_collections(store).map_err(|e| format!("Failed to populate collections. {}", e))?;
populate_endpoints(store).map_err(|e| format!("Failed to populate endpoints. {}", e))?;
Expand Down
8 changes: 1 addition & 7 deletions lib/src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,13 +336,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
10 changes: 7 additions & 3 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 @@ -173,14 +177,14 @@ impl Storelike for Store {
Box::new(self.hashmap.lock().unwrap().clone().into_values())
}

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-server/issues/6
"local:store"
}

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 b60a355

Please sign in to comment.