Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Rename node `tracking` module
cloudhead committed 2 years ago
commit 1b026ae0ee3cdc7865ea874a14faab809594c0c1
parent e65595c4c7a98307be5ea8cd977b221c7a39dd53
39 files changed +907 -906
modified radicle-cli/examples/rad-clone-all.md
@@ -1,6 +1,6 @@
```
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope all
-
✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Forking under z6Mkux1…nVhib7Z..
✓ Creating checkout in ./heartwood..
modified radicle-cli/examples/rad-clone-connect.md
@@ -3,7 +3,7 @@ automatically connect to the necessary seeds.

```
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
✓ Connecting to z6MknSL…StBU8Vi@[..]
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Connecting to z6Mkt67…v4N1tRk@[..]
modified radicle-cli/examples/rad-clone-unknown.md
@@ -2,6 +2,6 @@ Trying to clone a repository that is not in our routing table returns an error:

``` (fail)
$ rad clone rad:zVNuptPuk5XauitpCWSNVCXGGfXW --scope followed
-
✓ Tracking relationship established for rad:zVNuptPuk5XauitpCWSNVCXGGfXW with scope 'followed'
+
✓ Seeding policy updated for rad:zVNuptPuk5XauitpCWSNVCXGGfXW with scope 'followed'
✗ Error: no seeds found for rad:zVNuptPuk5XauitpCWSNVCXGGfXW
```
modified radicle-cli/examples/rad-clone.md
@@ -3,7 +3,7 @@ To create a local copy of a repository on the radicle network, we use the

```
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope followed
-
✓ Tracking relationship established for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
+
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Forking under z6Mkt67…v4N1tRk..
✓ Creating checkout in ./heartwood..
modified radicle-cli/examples/rad-patch-pull-update.md
@@ -20,7 +20,7 @@ To push changes, run `git push`.

``` ~bob
$ rad clone rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
-
✓ Tracking relationship established for rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK with scope 'all'
+
✓ Seeding policy updated for rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK with scope 'all'
✓ Fetching rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK from z6MknSL…StBU8Vi..
✓ Forking under z6Mkt67…v4N1tRk..
✓ Creating checkout in ./heartwood..
modified radicle-cli/src/commands/clone.rs
@@ -12,7 +12,7 @@ use radicle::git::raw;
use radicle::identity::doc;
use radicle::identity::doc::{DocError, Id};
use radicle::node;
-
use radicle::node::tracking::Scope;
+
use radicle::node::policy::Scope;
use radicle::node::{Handle as _, Node};
use radicle::prelude::*;
use radicle::rad;
@@ -39,7 +39,7 @@ Usage

Options

-
    --scope <scope>   Tracking scope (default: all)
+
    --scope <scope>   Follow scope (default: all)
    --no-announce     Do not announce our new refs to the network
    --help            Print help

@@ -219,7 +219,7 @@ pub fn clone<G: Signer>(
    // Track.
    if node.seed(id, scope)? {
        term::success!(
-
            "Tracking relationship established for {} with scope '{scope}'",
+
            "Seeding policy updated for {} with scope '{scope}'",
            term::format::tertiary(id)
        );
    }
modified radicle-cli/src/commands/follow.rs
@@ -2,8 +2,7 @@ use std::ffi::OsString;

use anyhow::anyhow;

-
use radicle::node::tracking::Alias;
-
use radicle::node::{Handle, NodeId};
+
use radicle::node::{Alias, Handle, NodeId};
use radicle::{prelude::*, Node};

use crate::terminal as term;
modified radicle-cli/src/commands/init.rs
@@ -12,7 +12,7 @@ use serde_json as json;
use radicle::crypto::{ssh, Verified};
use radicle::git::RefString;
use radicle::identity::Visibility;
-
use radicle::node::tracking::Scope;
+
use radicle::node::policy::Scope;
use radicle::node::{Handle, NodeId};
use radicle::prelude::Doc;
use radicle::{profile, Node};
@@ -37,7 +37,7 @@ Options
        --name <string>            Name of the project
        --description <string>     Description of the project
        --default-branch <name>    The default branch of the project
-
        --scope <scope>            Tracking scope (default: all)
+
        --scope <scope>            Repository follow scope (default: all)
        --private                  Set repository visibility to *private*
        --public                   Set repository visibility to *public*
    -u, --set-upstream             Setup the upstream of the default branch
modified radicle-cli/src/commands/inspect.rs
@@ -9,7 +9,7 @@ use chrono::prelude::*;

use radicle::identity::Id;
use radicle::identity::Identity;
-
use radicle::node::tracking::Policy;
+
use radicle::node::policy::Policy;
use radicle::node::AliasStore as _;
use radicle::storage::refs::RefsAt;
use radicle::storage::{ReadRepository, ReadStorage};
modified radicle-cli/src/commands/node/policies.rs
@@ -1,5 +1,5 @@
use radicle::crypto::PublicKey;
-
use radicle::node::{tracking, AliasStore};
+
use radicle::node::{policy, AliasStore};
use radicle::prelude::Did;
use radicle::Profile;

@@ -16,7 +16,7 @@ pub fn seeding(profile: &Profile) -> anyhow::Result<()> {
    ]);
    t.divider();

-
    for tracking::Repo { id, scope, policy } in store.repo_policies()? {
+
    for policy::Repo { id, scope, policy } in store.repo_policies()? {
        let id = id.to_string();
        let scope = scope.to_string();
        let policy = policy.to_string();
@@ -43,7 +43,7 @@ pub fn following(profile: &Profile) -> anyhow::Result<()> {
    ]);
    t.divider();

-
    for tracking::Node { id, alias, policy } in store.node_policies()? {
+
    for policy::Node { id, alias, policy } in store.node_policies()? {
        t.push([
            term::format::highlight(Did::from(id).to_string()),
            match alias {
modified radicle-cli/src/commands/seed.rs
@@ -3,7 +3,7 @@ use std::time;

use anyhow::anyhow;

-
use radicle::node::tracking::Scope;
+
use radicle::node::policy::Scope;
use radicle::node::Handle;
use radicle::{prelude::*, Node};

modified radicle-cli/src/project.rs
@@ -2,7 +2,7 @@ use radicle::prelude::*;

use crate::git;
use radicle::git::RefStr;
-
use radicle::node::tracking::Scope;
+
use radicle::node::policy::Scope;
use radicle::node::{Handle, NodeId};
use radicle::Node;

modified radicle-cli/tests/commands.rs
@@ -14,7 +14,7 @@ use radicle::storage::{ReadStorage, RemoteRepository};
use radicle::test::fixtures;

use radicle_cli_test::TestFormula;
-
use radicle_node::service::tracking::{Policy, Scope};
+
use radicle_node::service::policy::{Policy, Scope};
use radicle_node::service::Event;
use radicle_node::test::environment::{Config, Environment};
#[allow(unused_imports)]
modified radicle-fetch/src/handle.rs
@@ -86,7 +86,7 @@ impl<S> Handle<S> {
pub mod error {
    use std::io;

-
    use radicle::node::tracking;
+
    use radicle::node::policy;
    use radicle::prelude::Id;
    use radicle::{git, storage};
    use thiserror::Error;
@@ -96,7 +96,7 @@ pub mod error {
        #[error(transparent)]
        Io(#[from] io::Error),
        #[error(transparent)]
-
        Tracking(#[from] tracking::config::Error),
+
        Tracking(#[from] policy::config::Error),
    }

    #[derive(Debug, Error)]
@@ -105,7 +105,7 @@ pub mod error {
        FailedPolicy {
            rid: Id,
            #[source]
-
            err: tracking::store::Error,
+
            err: policy::store::Error,
        },
        #[error("cannot fetch {rid} as it is not tracked")]
        BlockedPolicy { rid: Id },
@@ -113,7 +113,7 @@ pub mod error {
        FailedNodes {
            rid: Id,
            #[source]
-
            err: tracking::store::Error,
+
            err: policy::store::Error,
        },

        #[error(transparent)]
modified radicle-fetch/src/tracking.rs
@@ -1,11 +1,11 @@
use std::collections::HashSet;

use radicle::crypto::PublicKey;
-
use radicle::node::tracking::config::Config;
-
use radicle::node::tracking::store::Read;
+
use radicle::node::policy::config::Config;
+
use radicle::node::policy::store::Read;
use radicle::prelude::Id;

-
pub use radicle::node::tracking::{Policy, Scope};
+
pub use radicle::node::policy::{Policy, Scope};

#[derive(Clone, Debug)]
pub enum Tracked {
@@ -70,14 +70,14 @@ impl BlockList {
}

pub mod error {
-
    use radicle::node::tracking;
+
    use radicle::node::policy;
    use radicle::prelude::Id;
    use radicle::storage;
    use thiserror::Error;

    #[derive(Debug, Error)]
    #[error(transparent)]
-
    pub struct Blocked(#[from] tracking::config::Error);
+
    pub struct Blocked(#[from] policy::config::Error);

    #[derive(Debug, Error)]
    pub enum Tracking {
@@ -85,7 +85,7 @@ pub mod error {
        FailedPolicy {
            rid: Id,
            #[source]
-
            err: tracking::store::Error,
+
            err: policy::store::Error,
        },
        #[error("cannot fetch {rid} as it is not tracked")]
        BlockedPolicy { rid: Id },
@@ -93,7 +93,7 @@ pub mod error {
        FailedNodes {
            rid: Id,
            #[source]
-
            err: tracking::store::Error,
+
            err: policy::store::Error,
        },

        #[error(transparent)]
modified radicle-httpd/src/api.rs
@@ -18,8 +18,8 @@ use tower_http::cors::{self, CorsLayer};
use radicle::cob::{issue, Uri};
use radicle::cob::{patch, Embed};
use radicle::identity::{DocAt, Id};
+
use radicle::node::policy::Scope;
use radicle::node::routing::Store;
-
use radicle::node::tracking::Scope;
use radicle::node::{Handle, NodeId};
use radicle::storage::{Oid, ReadRepository, ReadStorage};
use radicle::{Node, Profile};
modified radicle-httpd/src/api/error.rs
@@ -72,7 +72,7 @@ pub enum Error {

    /// Tracking store error.
    #[error(transparent)]
-
    TrackingStore(#[from] radicle::node::tracking::store::Error),
+
    TrackingStore(#[from] radicle::node::policy::store::Error),

    /// Node database error.
    #[error(transparent)]
modified radicle-httpd/src/api/v1/node.rs
@@ -7,7 +7,7 @@ use hyper::StatusCode;
use serde_json::json;

use radicle::identity::Id;
-
use radicle::node::{tracking, Handle, DEFAULT_TIMEOUT};
+
use radicle::node::{policy, Handle, DEFAULT_TIMEOUT};
use radicle::Node;

use crate::api::error::Error;
@@ -20,7 +20,7 @@ pub fn router(ctx: Context) -> Router {
        .route("/node/policies/repos", get(node_policies_repos_handler))
        .route(
            "/node/policies/repos/:rid",
-
            put(node_policies_track_handler).delete(node_policies_untrack_handler),
+
            put(node_policies_seed_handler).delete(node_policies_unseed_handler),
        )
        .with_state(ctx)
}
@@ -57,7 +57,7 @@ async fn node_policies_repos_handler(State(ctx): State<Context>) -> impl IntoRes
    let tracking = ctx.profile.tracking()?;
    let mut repos = Vec::new();

-
    for tracking::Repo { id, scope, policy } in tracking.repo_policies()? {
+
    for policy::Repo { id, scope, policy } in tracking.repo_policies()? {
        repos.push(json!({
            "id": id,
            "scope": scope,
@@ -70,7 +70,7 @@ async fn node_policies_repos_handler(State(ctx): State<Context>) -> impl IntoRes

/// Track a new repo.
/// `PUT /node/policies/repos/:rid`
-
async fn node_policies_track_handler(
+
async fn node_policies_seed_handler(
    State(ctx): State<Context>,
    AuthBearer(token): AuthBearer,
    Path(project): Path<Id>,
@@ -78,7 +78,8 @@ async fn node_policies_track_handler(
) -> impl IntoResponse {
    api::auth::validate(&ctx, &token).await?;
    let mut node = Node::new(ctx.profile.socket());
-
    node.track_repo(project, qs.scope.unwrap_or_default())?;
+
    node.seed(project, qs.scope.unwrap_or_default())?;
+

    if let Some(from) = qs.from {
        let results = node.fetch(project, from, DEFAULT_TIMEOUT)?;
        return Ok::<_, Error>((
@@ -86,20 +87,19 @@ async fn node_policies_track_handler(
            Json(json!({ "success": true, "results": results })),
        ));
    }
-

    Ok::<_, Error>((StatusCode::OK, Json(json!({ "success": true }))))
}

/// Untrack a repo.
/// `DELETE /node/policies/repos/:rid`
-
async fn node_policies_untrack_handler(
+
async fn node_policies_unseed_handler(
    State(ctx): State<Context>,
    AuthBearer(token): AuthBearer,
    Path(project): Path<Id>,
) -> impl IntoResponse {
    api::auth::validate(&ctx, &token).await?;
    let mut node = Node::new(ctx.profile.socket());
-
    node.untrack_repo(project)?;
+
    node.unseed(project)?;

    Ok::<_, Error>((StatusCode::OK, Json(json!({ "success": true }))))
}
modified radicle-node/src/control.rs
@@ -228,7 +228,7 @@ mod tests {
    use crate::identity::Id;
    use crate::node::Handle;
    use crate::node::{Alias, Node, NodeId};
-
    use crate::service::tracking::Scope;
+
    use crate::service::policy::Scope;
    use crate::test;

    #[test]
modified radicle-node/src/runtime.rs
@@ -26,7 +26,7 @@ use crate::control;
use crate::crypto::Signer;
use crate::node::{routing, NodeId};
use crate::service::message::NodeAnnouncement;
-
use crate::service::{gossip, tracking, Event};
+
use crate::service::{gossip, policy, Event};
use crate::wire::Wire;
use crate::wire::{self, Decode};
use crate::worker;
@@ -46,7 +46,7 @@ pub enum Error {
    Database(#[from] node::db::Error),
    /// A tracking database error.
    #[error("tracking database error: {0}")]
-
    Tracking(#[from] tracking::Error),
+
    Tracking(#[from] policy::Error),
    /// A gossip database error.
    #[error("gossip database error: {0}")]
    Gossip(#[from] gossip::Error),
@@ -147,7 +147,7 @@ impl Runtime {

        log::info!(target: "node", "Opening tracking policy configuration..");
        let tracking = home.tracking_mut()?;
-
        let tracking = tracking::Config::new(policy, scope, tracking);
+
        let tracking = policy::Config::new(policy, scope, tracking);

        log::info!(target: "node", "Default tracking policy set to '{}'", &policy);
        log::info!(target: "node", "Initializing service ({:?})..", network);
modified radicle-node/src/runtime/handle.rs
@@ -14,7 +14,7 @@ use crate::node::{Alias, Command, FetchResult};
use crate::profile::Home;
use crate::runtime::Emitter;
use crate::service;
-
use crate::service::tracking;
+
use crate::service::policy;
use crate::service::NodeId;
use crate::service::{CommandError, Config, QueryState};
use crate::service::{Event, Events};
@@ -213,7 +213,7 @@ impl radicle::node::Handle for Handle {
        receiver.recv().map_err(Error::from)
    }

-
    fn seed(&mut self, id: Id, scope: tracking::Scope) -> Result<bool, Error> {
+
    fn seed(&mut self, id: Id, scope: policy::Scope) -> Result<bool, Error> {
        let (sender, receiver) = chan::bounded(1);
        self.command(service::Command::TrackRepo(id, scope, sender))?;
        receiver.recv().map_err(Error::from)
modified radicle-node/src/service.rs
@@ -45,7 +45,7 @@ use crate::runtime::Emitter;
use crate::service::gossip::Store as _;
use crate::service::message::{Announcement, AnnouncementMessage, Info, Ping};
use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
-
use crate::service::tracking::{store::Write, Scope};
+
use crate::service::policy::{store::Write, Policy, Scope};
use crate::storage;
use crate::storage::refs::RefsAt;
use crate::storage::ReadRepository;
@@ -59,12 +59,12 @@ pub use crate::node::{config::Network, Config, NodeId};
pub use crate::service::message::{Message, ZeroBytes};
pub use crate::service::session::Session;

-
pub use radicle::node::tracking::config as tracking;
+
pub use radicle::node::policy::config as policy;

use self::io::Outbox;
use self::limitter::RateLimiter;
use self::message::{InventoryAnnouncement, RefsStatus};
-
use self::tracking::NamespacesError;
+
use self::policy::NamespacesError;

/// How often to run the "idle" task.
pub const IDLE_INTERVAL: LocalDuration = LocalDuration::from_secs(30);
@@ -140,7 +140,7 @@ pub enum Error {
    #[error(transparent)]
    Seeds(#[from] seed::Error),
    #[error(transparent)]
-
    Tracking(#[from] tracking::Error),
+
    Tracking(#[from] policy::Error),
    #[error(transparent)]
    Repository(#[from] radicle::storage::RepositoryError),
    #[error("namespaces error: {0}")]
@@ -213,7 +213,7 @@ pub enum CommandError {
    #[error(transparent)]
    Routing(#[from] routing::Error),
    #[error(transparent)]
-
    Tracking(#[from] tracking::Error),
+
    Tracking(#[from] policy::Error),
}

/// Error returned by [`Service::try_fetch`].
@@ -305,7 +305,7 @@ pub struct Service<D, S, G> {
    /// Node database.
    db: Stores<D>,
    /// Tracking policy configuration.
-
    tracking: tracking::Config<Write>,
+
    tracking: policy::Config<Write>,
    /// Peer sessions, currently or recently connected.
    sessions: Sessions,
    /// Clock. Tells the time.
@@ -364,7 +364,7 @@ where
        clock: LocalTime,
        db: Stores<D>,
        storage: S,
-
        tracking: tracking::Config<Write>,
+
        tracking: policy::Config<Write>,
        signer: G,
        rng: Rng,
        node: NodeAnnouncement,
@@ -404,7 +404,7 @@ where

    /// Track a repository.
    /// Returns whether or not the tracking policy was updated.
-
    pub fn track_repo(&mut self, id: &Id, scope: Scope) -> Result<bool, tracking::Error> {
+
    pub fn track_repo(&mut self, id: &Id, scope: Scope) -> Result<bool, policy::Error> {
        let updated = self.tracking.track_repo(id, scope)?;
        self.filter.insert(id);

@@ -415,7 +415,7 @@ where
    /// Returns whether or not the tracking policy was updated.
    /// Note that when untracking, we don't announce anything to the network. This is because by
    /// simply not announcing it anymore, it will eventually be pruned by nodes.
-
    pub fn untrack_repo(&mut self, id: &Id) -> Result<bool, tracking::Error> {
+
    pub fn untrack_repo(&mut self, id: &Id) -> Result<bool, policy::Error> {
        let updated = self.tracking.untrack_repo(id)?;
        // Nb. This is potentially slow if we have lots of projects. We should probably
        // only re-compute the filter when we've untracked a certain amount of projects
@@ -425,13 +425,13 @@ where
        self.filter = Filter::new(
            self.tracking
                .repo_policies()?
-
                .filter_map(|t| (t.policy == tracking::Policy::Allow).then_some(t.id)),
+
                .filter_map(|t| (t.policy == Policy::Allow).then_some(t.id)),
        );
        Ok(updated)
    }

    /// Check whether we are tracking a certain repository.
-
    pub fn is_tracking(&self, id: &Id) -> Result<bool, tracking::Error> {
+
    pub fn is_tracking(&self, id: &Id) -> Result<bool, policy::Error> {
        self.tracking.is_repo_tracked(id)
    }

@@ -464,7 +464,7 @@ where
    }

    /// Get the tracking policy.
-
    pub fn tracking(&self) -> &tracking::Config<Write> {
+
    pub fn tracking(&self) -> &policy::Config<Write> {
        &self.tracking
    }

@@ -570,7 +570,7 @@ where
        self.filter = Filter::new(
            self.tracking
                .repo_policies()?
-
                .filter_map(|t| (t.policy == tracking::Policy::Allow).then_some(t.id)),
+
                .filter_map(|t| (t.policy == Policy::Allow).then_some(t.id)),
        );
        // Try to establish some connections.
        self.maintain_connections();
@@ -1266,7 +1266,7 @@ where
                    "Service::handle_announcement: error accessing repo tracking configuration",
                );

-
                if repo_entry.policy == tracking::Policy::Allow {
+
                if repo_entry.policy == Policy::Allow {
                    let (fresh, stale) = match self.refs_status_of(message, &repo_entry.scope) {
                        Ok(RefsStatus { fresh, stale }) => (fresh, stale),
                        Err(e) => {
@@ -1411,7 +1411,7 @@ where
    fn refs_status_of(
        &self,
        message: &RefsAnnouncement,
-
        scope: &tracking::Scope,
+
        scope: &policy::Scope,
    ) -> Result<RefsStatus, Error> {
        let mut refs = message.refs_status(&self.storage)?;

@@ -1423,8 +1423,8 @@ where

        // Second, check the scope.
        match scope {
-
            tracking::Scope::All => Ok(refs),
-
            tracking::Scope::Followed => {
+
            policy::Scope::All => Ok(refs),
+
            policy::Scope::Followed => {
                match self.tracking.namespaces_for(&self.storage, &message.rid) {
                    Ok(Namespaces::All) => Ok(refs),
                    Ok(Namespaces::Followed(mut followed)) => {
@@ -1817,7 +1817,7 @@ where

    /// Return a new filter object, based on our tracking policy.
    fn filter(&self) -> Filter {
-
        if self.config.policy == tracking::Policy::Allow {
+
        if self.config.policy == Policy::Allow {
            // TODO: Remove bits for blocked repos.
            Filter::default()
        } else {
@@ -1920,7 +1920,7 @@ where
        let missing = self
            .tracking
            .repo_policies()?
-
            .filter_map(|t| (t.policy == tracking::Policy::Allow).then_some(t.id))
+
            .filter_map(|t| (t.policy == Policy::Allow).then_some(t.id))
            .filter(|rid| !inventory.contains(rid));

        for rid in missing {
modified radicle-node/src/test/environment.rs
@@ -18,8 +18,8 @@ use radicle::crypto::{KeyPair, Seed, Signer};
use radicle::git;
use radicle::git::refname;
use radicle::identity::{Id, Visibility};
+
use radicle::node::policy::store as policy;
use radicle::node::routing::Store;
-
use radicle::node::tracking::store as tracking;
use radicle::node::Database;
use radicle::node::{Alias, TRACKING_DB_FILE};
use radicle::node::{ConnectOptions, Handle as _};
@@ -82,7 +82,7 @@ impl Environment {
        let signer = MemorySigner::load(&profile.keystore, None).unwrap();

        let tracking_db = profile.home.node().join(TRACKING_DB_FILE);
-
        let tracking = tracking::Config::open(tracking_db).unwrap();
+
        let tracking = policy::Config::open(tracking_db).unwrap();
        let db = profile.database_mut().unwrap();
        let db = service::Stores::from(db);

@@ -122,7 +122,7 @@ impl Environment {
        )
        .unwrap();

-
        tracking::Config::open(tracking_db).unwrap();
+
        policy::Config::open(tracking_db).unwrap();
        home.database_mut().unwrap(); // Just create the database.

        transport::local::register(storage.clone());
@@ -149,7 +149,7 @@ pub struct Node<G> {
    pub storage: Storage,
    pub config: Config,
    pub db: service::Stores<Database>,
-
    pub tracking: tracking::Config<tracking::Write>,
+
    pub tracking: policy::Config<policy::Write>,
}

/// Handle to a running node.
@@ -420,7 +420,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {

        assert!(self
            .tracking
-
            .track_repo(&id, node::tracking::Scope::Followed)
+
            .track_repo(&id, node::policy::Scope::Followed)
            .unwrap());

        log::debug!(
modified radicle-node/src/test/handle.rs
@@ -9,7 +9,7 @@ use radicle::storage::refs::RefsAt;
use crate::identity::Id;
use crate::node::{Alias, Config, ConnectOptions, ConnectResult, Event, FetchResult, Seeds};
use crate::runtime::HandleError;
-
use crate::service::tracking;
+
use crate::service::policy;
use crate::service::NodeId;

#[derive(Default, Clone)]
@@ -60,7 +60,7 @@ impl radicle::node::Handle for Handle {
        })
    }

-
    fn seed(&mut self, id: Id, _scope: tracking::Scope) -> Result<bool, Self::Error> {
+
    fn seed(&mut self, id: Id, _scope: policy::Scope) -> Result<bool, Self::Error> {
        Ok(self.tracking_repos.lock().unwrap().insert(id))
    }

modified radicle-node/src/test/peer.rs
@@ -24,7 +24,7 @@ use crate::runtime::Emitter;
use crate::service;
use crate::service::io::Io;
use crate::service::message::*;
-
use crate::service::tracking::{Policy, Scope};
+
use crate::service::policy::{Policy, Scope};
use crate::service::*;
use crate::storage::git::transport::remote;
use crate::storage::Inventory;
@@ -162,8 +162,8 @@ where
        storage: S,
        mut config: Config<G>,
    ) -> Self {
-
        let tracking = tracking::Store::<tracking::store::Write>::memory().unwrap();
-
        let mut tracking = tracking::Config::new(config.policy, config.scope, tracking);
+
        let tracking = policy::Store::<policy::store::Write>::memory().unwrap();
+
        let mut tracking = policy::Config::new(config.policy, config.scope, tracking);
        let id = *config.signer.public_key();
        let ip = ip.into();
        let local_addr = net::SocketAddr::new(ip, config.rng.u16(..));
modified radicle-node/src/tests.rs
@@ -395,7 +395,7 @@ fn test_tracking() {
    let (sender, receiver) = chan::bounded(1);
    alice.command(Command::TrackRepo(
        proj_id,
-
        tracking::Scope::default(),
+
        policy::Scope::default(),
        sender,
    ));
    let policy_change = receiver.recv().map_err(runtime::HandleError::from).unwrap();
@@ -504,7 +504,7 @@ fn test_announcement_rebroadcast_duplicates() {
        anns.insert(bob.node_announcement());

        for rid in rids {
-
            alice.track_repo(&rid, tracking::Scope::All).unwrap();
+
            alice.track_repo(&rid, policy::Scope::All).unwrap();
            anns.insert(carol.refs_announcement(rid));
            anns.insert(bob.refs_announcement(rid));
        }
@@ -676,9 +676,9 @@ fn test_refs_announcement_relay() {
    };
    let bob_inv = bob.storage().inventory().unwrap();

-
    alice.track_repo(&bob_inv[0], tracking::Scope::All).unwrap();
-
    alice.track_repo(&bob_inv[1], tracking::Scope::All).unwrap();
-
    alice.track_repo(&bob_inv[2], tracking::Scope::All).unwrap();
+
    alice.track_repo(&bob_inv[0], policy::Scope::All).unwrap();
+
    alice.track_repo(&bob_inv[1], policy::Scope::All).unwrap();
+
    alice.track_repo(&bob_inv[2], policy::Scope::All).unwrap();
    alice.connect_to(&bob);
    alice.connect_to(&eve);
    alice.receive(eve.id(), Message::Subscribe(Subscribe::all()));
@@ -742,7 +742,7 @@ fn test_refs_announcement_fetch_trusted_no_inventory() {
    let bob_inv = bob.storage().inventory().unwrap();
    let rid = bob_inv[0];

-
    alice.track_repo(&rid, tracking::Scope::Followed).unwrap();
+
    alice.track_repo(&rid, policy::Scope::Followed).unwrap();
    alice.connect_to(&bob);

    // Alice receives Bob's refs.
@@ -798,7 +798,7 @@ fn test_refs_announcement_followed() {

    // Alice uses Scope::Followed, and did not track Bob yet.
    alice.connect_to(&bob);
-
    alice.track_repo(&rid, tracking::Scope::Followed).unwrap();
+
    alice.track_repo(&rid, policy::Scope::Followed).unwrap();

    // Alice receives Bob's refs
    alice.receive(bob.id(), bob.refs_announcement(rid));
@@ -834,7 +834,7 @@ fn test_refs_announcement_no_subscribe() {
    let eve = Peer::new("eve", [9, 9, 9, 9]);
    let id = arbitrary::gen(1);

-
    alice.track_repo(&id, tracking::Scope::All).unwrap();
+
    alice.track_repo(&id, policy::Scope::All).unwrap();
    alice.connect_to(&bob);
    alice.connect_to(&eve);
    alice.receive(bob.id(), bob.refs_announcement(rid));
@@ -863,7 +863,7 @@ fn test_refs_announcement_offline() {
    let inv = alice.inventory();
    let rid = inv.first().unwrap();
    let mut bob = Peer::new("bob", [8, 8, 8, 8]);
-
    bob.track_repo(rid, tracking::Scope::All).unwrap();
+
    bob.track_repo(rid, policy::Scope::All).unwrap();

    // Make sure alice's service wasn't initialized before.
    assert!(alice.initialize());
@@ -1255,7 +1255,7 @@ fn test_track_repo_subscribe() {
    let (send, recv) = chan::bounded(1);

    alice.connect_to(&bob);
-
    alice.command(Command::TrackRepo(rid, tracking::Scope::default(), send));
+
    alice.command(Command::TrackRepo(rid, policy::Scope::default(), send));
    assert!(recv.recv().unwrap());

    assert_matches!(
@@ -1275,7 +1275,7 @@ fn test_fetch_missing_inventory_on_gossip() {
    let bob = Peer::new("bob", [8, 8, 8, 8]);
    let now = LocalTime::now();

-
    alice.track_repo(&rid, node::tracking::Scope::All).unwrap();
+
    alice.track_repo(&rid, node::policy::Scope::All).unwrap();
    alice.connect_to(&bob);
    alice.receive(
        bob.id(),
@@ -1300,7 +1300,7 @@ fn test_fetch_missing_inventory_on_schedule() {
    let bob = Peer::new("bob", [8, 8, 8, 8]);
    let now = LocalTime::now();

-
    alice.track_repo(&rid, node::tracking::Scope::All).unwrap();
+
    alice.track_repo(&rid, node::policy::Scope::All).unwrap();
    alice.connect_to(&bob);
    alice.receive(
        bob.id(),
@@ -1441,7 +1441,7 @@ fn test_refs_synced_event() {
    });
    let msg = ann.signed(bob.signer());

-
    alice.track_repo(&acme, tracking::Scope::All).unwrap();
+
    alice.track_repo(&acme, policy::Scope::All).unwrap();
    alice.connect_to(&bob);
    alice.receive(bob.id, Message::Announcement(msg));

@@ -1542,14 +1542,14 @@ fn test_push_and_pull() {
    let (sender, _) = chan::bounded(1);
    bob.command(service::Command::TrackRepo(
        proj_id,
-
        tracking::Scope::default(),
+
        policy::Scope::default(),
        sender,
    ));
    // Eve seeds Alice's project.
    let (sender, _) = chan::bounded(1);
    eve.command(service::Command::TrackRepo(
        proj_id,
-
        tracking::Scope::default(),
+
        policy::Scope::default(),
        sender,
    ));

modified radicle-node/src/tests/e2e.rs
@@ -13,7 +13,7 @@ use radicle::{assert_matches, rad};
use crate::node::config::Limits;
use crate::node::{Config, ConnectOptions};
use crate::service;
-
use crate::service::tracking::Scope;
+
use crate::service::policy::Scope;
use crate::storage::git::transport;
use crate::test::environment::{converge, Environment, Node};
use crate::test::logger;
modified radicle-node/src/worker.rs
@@ -18,7 +18,8 @@ use radicle::{crypto, Storage};
use radicle_fetch::FetchLimit;

use crate::runtime::{thread, Handle};
-
use crate::service::tracking;
+
use crate::service::policy;
+
use crate::service::policy::Policy;
use crate::wire::StreamId;

pub use channels::{ChannelEvent, Channels};
@@ -49,7 +50,7 @@ pub enum FetchError {
    #[error(transparent)]
    Storage(#[from] radicle::storage::Error),
    #[error(transparent)]
-
    TrackingConfig(#[from] radicle::node::tracking::store::Error),
+
    TrackingConfig(#[from] radicle::node::policy::store::Error),
    #[error(transparent)]
    Tracked(#[from] radicle_fetch::tracking::error::Tracking),
    #[error(transparent)]
@@ -152,9 +153,9 @@ pub struct TaskResult {
#[derive(Debug, Clone)]
pub struct FetchConfig {
    /// Default policy, if a policy for a specific node or repository was not found.
-
    pub policy: tracking::Policy,
+
    pub policy: Policy,
    /// Default scope, if a scope for a specific repository was not found.
-
    pub scope: tracking::Scope,
+
    pub scope: policy::Scope,
    /// Path to the tracking database.
    pub tracking_db: PathBuf,
    /// Data limits when fetching from a remote.
@@ -282,12 +283,11 @@ impl Worker {
            local,
            expiry,
        } = &self.fetch_config;
-
        let tracking =
-
            tracking::Config::new(*policy, *scope, tracking::Store::reader(tracking_db)?);
+
        let policies = policy::Config::new(*policy, *scope, policy::Store::reader(tracking_db)?);
        // N.b. if the `rid` is blocked this will return an error, so
        // we won't continue with any further set up of the fetch.
-
        let tracked = radicle_fetch::Tracked::from_config(rid, &tracking)?;
-
        let blocked = radicle_fetch::BlockList::from_config(&tracking)?;
+
        let tracked = radicle_fetch::Tracked::from_config(rid, &policies)?;
+
        let blocked = radicle_fetch::BlockList::from_config(&policies)?;

        let handle = fetch::Handle::new(rid, *local, &self.storage, tracked, blocked, channels)?;
        let result = handle.fetch(rid, &self.storage, *limit, remote, refs_at)?;
modified radicle/src/node.rs
@@ -4,9 +4,9 @@ pub mod address;
pub mod config;
pub mod db;
pub mod events;
+
pub mod policy;
pub mod routing;
pub mod seed;
-
pub mod tracking;

use std::collections::{BTreeSet, HashMap, HashSet};
use std::io::{BufRead, BufReader};
@@ -414,7 +414,7 @@ pub enum Command {

    /// Seed the given repository.
    #[serde(rename_all = "camelCase")]
-
    Seed { rid: Id, scope: tracking::Scope },
+
    Seed { rid: Id, scope: policy::Scope },

    /// Unseed the given repository.
    #[serde(rename_all = "camelCase")]
@@ -777,7 +777,7 @@ pub trait Handle: Clone + Sync + Send {
    ) -> Result<FetchResult, Self::Error>;
    /// Start seeding the given repo. May update the scope. Does nothing if the
    /// repo is already seeded.
-
    fn seed(&mut self, id: Id, scope: tracking::Scope) -> Result<bool, Self::Error>;
+
    fn seed(&mut self, id: Id, scope: policy::Scope) -> Result<bool, Self::Error>;
    /// Start following the given peer.
    fn follow(&mut self, id: NodeId, alias: Option<Alias>) -> Result<bool, Self::Error>;
    /// Un-seed the given repo and delete it from storage.
@@ -988,7 +988,7 @@ impl Handle for Node {
        Ok(response.updated)
    }

-
    fn seed(&mut self, rid: Id, scope: tracking::Scope) -> Result<bool, Error> {
+
    fn seed(&mut self, rid: Id, scope: policy::Scope) -> Result<bool, Error> {
        let mut line = self.call::<Success>(Command::Seed { rid, scope }, DEFAULT_TIMEOUT)?;
        let response = line.next().ok_or(Error::EmptyResponse)??;

modified radicle/src/node/config.rs
@@ -6,7 +6,7 @@ use cyphernet::addr::PeerAddr;
use localtime::LocalDuration;

use crate::node;
-
use crate::node::tracking::{Policy, Scope};
+
use crate::node::policy::{Policy, Scope};
use crate::node::{Address, Alias, NodeId};

/// Target number of peers to maintain connections to.
added radicle/src/node/policy.rs
@@ -0,0 +1,160 @@
+
pub mod config;
+
pub mod store;
+

+
use std::fmt;
+
use std::str::FromStr;
+

+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
use crate::prelude::Id;
+

+
pub use super::{Alias, NodeId};
+

+
/// Repository seeding policy.
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct Repo {
+
    pub id: Id,
+
    pub scope: Scope,
+
    pub policy: Policy,
+
}
+

+
/// Node following policy.
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct Node {
+
    pub id: NodeId,
+
    pub alias: Option<Alias>,
+
    pub policy: Policy,
+
}
+

+
/// Resource policy.
+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
pub enum Policy {
+
    /// The resource is allowed.
+
    Allow,
+
    /// The resource is blocked.
+
    #[default]
+
    Block,
+
}
+

+
impl fmt::Display for Policy {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match self {
+
            Self::Allow => write!(f, "allow"),
+
            Self::Block => write!(f, "block"),
+
        }
+
    }
+
}
+

+
impl FromStr for Policy {
+
    type Err = String;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        match s {
+
            "allow" => Ok(Self::Allow),
+
            "block" => Ok(Self::Block),
+
            _ => Err(s.to_owned()),
+
        }
+
    }
+
}
+

+
impl sqlite::BindableWithIndex for Policy {
+
    fn bind<I: sqlite::ParameterIndex>(
+
        self,
+
        stmt: &mut sqlite::Statement<'_>,
+
        i: I,
+
    ) -> sqlite::Result<()> {
+
        match self {
+
            Self::Allow => "allow",
+
            Self::Block => "block",
+
        }
+
        .bind(stmt, i)
+
    }
+
}
+

+
impl TryFrom<&sqlite::Value> for Policy {
+
    type Error = sqlite::Error;
+

+
    fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
+
        let message = Some("sql: invalid policy".to_owned());
+

+
        match value {
+
            sqlite::Value::String(s) if s == "allow" => Ok(Policy::Allow),
+
            sqlite::Value::String(s) if s == "block" => Ok(Policy::Block),
+
            _ => Err(sqlite::Error {
+
                code: None,
+
                message,
+
            }),
+
        }
+
    }
+
}
+

+
/// Follow scope of a seeded repository.
+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
pub enum Scope {
+
    /// Seed remotes that are explicitly followed.
+
    #[default]
+
    Followed,
+
    /// Seed all remotes.
+
    All,
+
}
+

+
impl fmt::Display for Scope {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match self {
+
            Scope::Followed => f.write_str("followed"),
+
            Scope::All => f.write_str("all"),
+
        }
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
#[error("invalid tracking scope: {0:?}")]
+
pub struct ParseScopeError(String);
+

+
impl FromStr for Scope {
+
    type Err = ParseScopeError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        match s {
+
            "followed" => Ok(Self::Followed),
+
            "all" => Ok(Self::All),
+
            _ => Err(ParseScopeError(s.to_string())),
+
        }
+
    }
+
}
+

+
impl sqlite::BindableWithIndex for Scope {
+
    fn bind<I: sqlite::ParameterIndex>(
+
        self,
+
        stmt: &mut sqlite::Statement<'_>,
+
        i: I,
+
    ) -> sqlite::Result<()> {
+
        let s = match self {
+
            Self::Followed => "followed",
+
            Self::All => "all",
+
        };
+
        s.bind(stmt, i)
+
    }
+
}
+

+
impl TryFrom<&sqlite::Value> for Scope {
+
    type Error = sqlite::Error;
+

+
    fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
+
        let message = Some("invalid remote scope".to_owned());
+

+
        match value {
+
            sqlite::Value::String(scope) => Scope::from_str(scope).map_err(|_| sqlite::Error {
+
                code: None,
+
                message,
+
            }),
+
            _ => Err(sqlite::Error {
+
                code: None,
+
                message,
+
            }),
+
        }
+
    }
+
}
added radicle/src/node/policy/config.rs
@@ -0,0 +1,166 @@
+
use core::fmt;
+
use std::collections::HashSet;
+
use std::ops;
+

+
use log::error;
+
use thiserror::Error;
+

+
use crate::crypto::PublicKey;
+
use crate::prelude::{Id, NodeId};
+
use crate::storage::{Namespaces, ReadRepository as _, ReadStorage, RepositoryError};
+

+
pub use crate::node::policy::store;
+
pub use crate::node::policy::store::Config as Store;
+
pub use crate::node::policy::store::Error;
+
pub use crate::node::policy::{Alias, Node, Policy, Repo, Scope};
+

+
#[derive(Debug, Error)]
+
pub enum NamespacesError {
+
    #[error("failed to find tracking policy for {rid}")]
+
    FailedPolicy {
+
        rid: Id,
+
        #[source]
+
        err: Error,
+
    },
+
    #[error("cannot fetch {rid} as it is not tracked")]
+
    BlockedPolicy { rid: Id },
+
    #[error("failed to get tracking nodes for {rid}")]
+
    FailedNodes {
+
        rid: Id,
+
        #[source]
+
        err: Error,
+
    },
+
    #[error("failed to get delegates for {rid}")]
+
    FailedDelegates {
+
        rid: Id,
+
        #[source]
+
        err: RepositoryError,
+
    },
+
    #[error(transparent)]
+
    Git(#[from] crate::git::raw::Error),
+
    #[error("could not find any followed nodes for {rid}")]
+
    NoFollowed { rid: Id },
+
}
+

+
/// Tracking configuration.
+
pub struct Config<T> {
+
    /// Default policy, if a policy for a specific node or repository was not found.
+
    policy: Policy,
+
    /// Default scope, if a scope for a specific repository was not found.
+
    scope: Scope,
+
    /// Underlying configuration store.
+
    store: Store<T>,
+
}
+

+
// N.b. deriving `Debug` will require `T: Debug` so we manually
+
// implement it here.
+
impl<T> fmt::Debug for Config<T> {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        f.debug_struct("Config")
+
            .field("policy", &self.policy)
+
            .field("scope", &self.scope)
+
            .field("store", &self.store)
+
            .finish()
+
    }
+
}
+

+
impl<T> Config<T> {
+
    /// Create a new tracking configuration.
+
    pub fn new(policy: Policy, scope: Scope, store: Store<T>) -> Self {
+
        Self {
+
            policy,
+
            scope,
+
            store,
+
        }
+
    }
+

+
    /// Check if a repository is tracked.
+
    pub fn is_repo_tracked(&self, id: &Id) -> Result<bool, Error> {
+
        self.repo_policy(id)
+
            .map(|entry| entry.policy == Policy::Allow)
+
    }
+

+
    /// Check if a node is tracked.
+
    pub fn is_node_tracked(&self, id: &NodeId) -> Result<bool, Error> {
+
        self.node_policy(id)
+
            .map(|entry| entry.policy == Policy::Allow)
+
    }
+

+
    /// Get a node's tracking information.
+
    /// Returns the default policy if the node isn't found.
+
    pub fn node_policy(&self, id: &NodeId) -> Result<Node, Error> {
+
        Ok(self.store.node_policy(id)?.unwrap_or(Node {
+
            id: *id,
+
            alias: None,
+
            policy: self.policy,
+
        }))
+
    }
+

+
    /// Get a repository's tracking information.
+
    /// Returns the default policy if the repo isn't found.
+
    pub fn repo_policy(&self, id: &Id) -> Result<Repo, Error> {
+
        Ok(self.store.repo_policy(id)?.unwrap_or(Repo {
+
            id: *id,
+
            scope: self.scope,
+
            policy: self.policy,
+
        }))
+
    }
+

+
    pub fn namespaces_for<S>(&self, storage: &S, rid: &Id) -> Result<Namespaces, NamespacesError>
+
    where
+
        S: ReadStorage,
+
    {
+
        use NamespacesError::*;
+

+
        let entry = self
+
            .repo_policy(rid)
+
            .map_err(|err| FailedPolicy { rid: *rid, err })?;
+
        match entry.policy {
+
            Policy::Block => {
+
                error!(target: "service", "Attempted to fetch untracked repo {rid}");
+
                Err(NamespacesError::BlockedPolicy { rid: *rid })
+
            }
+
            Policy::Allow => match entry.scope {
+
                Scope::All => Ok(Namespaces::All),
+
                Scope::Followed => {
+
                    let nodes = self
+
                        .node_policies()
+
                        .map_err(|err| FailedNodes { rid: *rid, err })?;
+
                    let mut followed: HashSet<_> = nodes
+
                        .filter_map(|node| (node.policy == Policy::Allow).then_some(node.id))
+
                        .collect();
+

+
                    if let Ok(repo) = storage.repository(*rid) {
+
                        let delegates = repo
+
                            .delegates()
+
                            .map_err(|err| FailedDelegates { rid: *rid, err })?
+
                            .map(PublicKey::from);
+
                        followed.extend(delegates);
+
                    };
+
                    if followed.is_empty() {
+
                        // Nb. returning All here because the
+
                        // fetching logic will correctly determine
+
                        // followed and delegate remotes.
+
                        Ok(Namespaces::All)
+
                    } else {
+
                        Ok(Namespaces::Followed(followed))
+
                    }
+
                }
+
            },
+
        }
+
    }
+
}
+

+
impl<T> ops::Deref for Config<T> {
+
    type Target = Store<T>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.store
+
    }
+
}
+

+
impl<T> ops::DerefMut for Config<T> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.store
+
    }
+
}
added radicle/src/node/policy/schema.sql
@@ -0,0 +1,31 @@
+
--
+
-- Node policy database.
+
--
+

+
-- Node follow policies.
+
create table if not exists "following" (
+
  -- Node ID.
+
  "id"                 text      primary key not null,
+
  -- Node alias. May override the alias announced by the node.
+
  "alias"              text      default '',
+
  -- Tracking policy for this node.
+
  "policy"             text      default 'allow'
+
  --
+
) strict;
+

+
-- Repository seeding policies.
+
create table if not exists "seeding" (
+
  -- Repository ID.
+
  "id"                 text      primary key not null,
+
  -- Tracking scope for this repository.
+
  --
+
  -- Valid values are:
+
  --
+
  -- "followed"        seed repository delegates and remotes in the `following` table.
+
  -- "all"             seed all remotes.
+
  --
+
  "scope"              text      default 'followed',
+
  -- Tracking policy for this repository.
+
  "policy"             text      default 'allow'
+
  --
+
) strict;
added radicle/src/node/policy/store.rs
@@ -0,0 +1,436 @@
+
#![allow(clippy::type_complexity)]
+
use std::marker::PhantomData;
+
use std::path::Path;
+
use std::{fmt, io, ops::Not as _, str::FromStr, time};
+

+
use sqlite as sql;
+
use thiserror::Error;
+

+
use crate::node::{Alias, AliasStore};
+
use crate::prelude::{Id, NodeId};
+

+
use super::{Node, Policy, Repo, Scope};
+

+
/// How long to wait for the database lock to be released before failing a read.
+
const DB_READ_TIMEOUT: time::Duration = time::Duration::from_secs(3);
+
/// How long to wait for the database lock to be released before failing a write.
+
const DB_WRITE_TIMEOUT: time::Duration = time::Duration::from_secs(6);
+

+
#[derive(Error, Debug)]
+
pub enum Error {
+
    /// I/O error.
+
    #[error("i/o error: {0}")]
+
    Io(#[from] io::Error),
+
    /// An Internal error.
+
    #[error("internal error: {0}")]
+
    Internal(#[from] sql::Error),
+
}
+

+
/// Read-only type witness.
+
pub struct Read;
+
/// Read-write type witness.
+
pub struct Write;
+

+
/// Read only config.
+
pub type ConfigReader = Config<Read>;
+
/// Read-write config.
+
pub type ConfigWriter = Config<Write>;
+

+
/// Tracking configuration.
+
pub struct Config<T> {
+
    db: sql::Connection,
+
    _marker: PhantomData<T>,
+
}
+

+
impl<T> fmt::Debug for Config<T> {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Config(..)")
+
    }
+
}
+

+
impl Config<Read> {
+
    const SCHEMA: &'static str = include_str!("schema.sql");
+

+
    /// Same as [`Self::open`], but in read-only mode. This is useful to have multiple
+
    /// open databases, as no locking is required.
+
    pub fn reader<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
+
        let mut db =
+
            sql::Connection::open_with_flags(path, sqlite::OpenFlags::new().with_read_only())?;
+
        db.set_busy_timeout(DB_READ_TIMEOUT.as_millis() as usize)?;
+
        db.execute(Self::SCHEMA)?;
+

+
        Ok(Self {
+
            db,
+
            _marker: PhantomData,
+
        })
+
    }
+

+
    /// Create a new in-memory address book.
+
    pub fn memory() -> Result<Self, Error> {
+
        let db = sql::Connection::open_with_flags(
+
            ":memory:",
+
            sqlite::OpenFlags::new().with_read_only(),
+
        )?;
+
        db.execute(Self::SCHEMA)?;
+

+
        Ok(Self {
+
            db,
+
            _marker: PhantomData,
+
        })
+
    }
+
}
+

+
impl Config<Write> {
+
    const SCHEMA: &'static str = include_str!("schema.sql");
+

+
    /// Open a policy store at the given path. Creates a new store if it
+
    /// doesn't exist.
+
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
+
        let mut db = sql::Connection::open(path)?;
+
        db.set_busy_timeout(DB_WRITE_TIMEOUT.as_millis() as usize)?;
+
        db.execute(Self::SCHEMA)?;
+

+
        Ok(Self {
+
            db,
+
            _marker: PhantomData,
+
        })
+
    }
+

+
    /// Create a new in-memory address book.
+
    pub fn memory() -> Result<Self, Error> {
+
        let db = sql::Connection::open(":memory:")?;
+
        db.execute(Self::SCHEMA)?;
+

+
        Ok(Self {
+
            db,
+
            _marker: PhantomData,
+
        })
+
    }
+

+
    /// Get a read-only version of this store.
+
    pub fn read_only(self) -> ConfigReader {
+
        Config {
+
            db: self.db,
+
            _marker: PhantomData,
+
        }
+
    }
+

+
    /// Track a node.
+
    pub fn track_node(&mut self, id: &NodeId, alias: Option<&str>) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare(
+
            "INSERT INTO `following` (id, alias)
+
             VALUES (?1, ?2)
+
             ON CONFLICT DO UPDATE
+
             SET alias = ?2 WHERE alias != ?2",
+
        )?;
+

+
        stmt.bind((1, id))?;
+
        stmt.bind((2, alias.unwrap_or_default()))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Track a repository.
+
    pub fn track_repo(&mut self, id: &Id, scope: Scope) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare(
+
            "INSERT INTO `seeding` (id, scope)
+
             VALUES (?1, ?2)
+
             ON CONFLICT DO UPDATE
+
             SET scope = ?2 WHERE scope != ?2",
+
        )?;
+

+
        stmt.bind((1, id))?;
+
        stmt.bind((2, scope))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Set a node's tracking policy.
+
    pub fn set_node_policy(&mut self, id: &NodeId, policy: Policy) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare(
+
            "INSERT INTO `following` (id, policy)
+
             VALUES (?1, ?2)
+
             ON CONFLICT DO UPDATE
+
             SET policy = ?2 WHERE policy != ?2",
+
        )?;
+

+
        stmt.bind((1, id))?;
+
        stmt.bind((2, policy))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Set a repository's tracking policy.
+
    pub fn set_repo_policy(&mut self, id: &Id, policy: Policy) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare(
+
            "INSERT INTO `seeding` (id, policy)
+
             VALUES (?1, ?2)
+
             ON CONFLICT DO UPDATE
+
             SET policy = ?2 WHERE policy != ?2",
+
        )?;
+

+
        stmt.bind((1, id))?;
+
        stmt.bind((2, policy))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Untrack a node.
+
    pub fn untrack_node(&mut self, id: &NodeId) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare("DELETE FROM `following` WHERE id = ?")?;
+

+
        stmt.bind((1, id))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+

+
    /// Untrack a repository.
+
    pub fn untrack_repo(&mut self, id: &Id) -> Result<bool, Error> {
+
        let mut stmt = self.db.prepare("DELETE FROM `seeding` WHERE id = ?")?;
+

+
        stmt.bind((1, id))?;
+
        stmt.next()?;
+

+
        Ok(self.db.change_count() > 0)
+
    }
+
}
+

+
/// `Read` methods for `Config`. This implies that a
+
/// `Config<Write>` can access these functions as well.
+
impl<T> Config<T> {
+
    /// Check if a node is tracked.
+
    pub fn is_node_tracked(&self, id: &NodeId) -> Result<bool, Error> {
+
        Ok(matches!(
+
            self.node_policy(id)?,
+
            Some(Node {
+
                policy: Policy::Allow,
+
                ..
+
            })
+
        ))
+
    }
+

+
    /// Check if a repository is tracked.
+
    pub fn is_repo_tracked(&self, id: &Id) -> Result<bool, Error> {
+
        Ok(matches!(
+
            self.repo_policy(id)?,
+
            Some(Repo {
+
                policy: Policy::Allow,
+
                ..
+
            })
+
        ))
+
    }
+

+
    /// Get a node's tracking policy.
+
    pub fn node_policy(&self, id: &NodeId) -> Result<Option<Node>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT alias, policy FROM `following` WHERE id = ?")?;
+

+
        stmt.bind((1, id))?;
+

+
        if let Some(Ok(row)) = stmt.into_iter().next() {
+
            let alias = row.read::<&str, _>("alias");
+
            let alias = alias
+
                .is_empty()
+
                .not()
+
                .then_some(alias.to_owned())
+
                .and_then(|s| Alias::from_str(&s).ok());
+
            let policy = row.read::<Policy, _>("policy");
+

+
            return Ok(Some(Node {
+
                id: *id,
+
                alias,
+
                policy,
+
            }));
+
        }
+
        Ok(None)
+
    }
+

+
    /// Get a repository's tracking policy.
+
    pub fn repo_policy(&self, id: &Id) -> Result<Option<Repo>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT scope, policy FROM `seeding` WHERE id = ?")?;
+

+
        stmt.bind((1, id))?;
+

+
        if let Some(Ok(row)) = stmt.into_iter().next() {
+
            return Ok(Some(Repo {
+
                id: *id,
+
                scope: row.read::<Scope, _>("scope"),
+
                policy: row.read::<Policy, _>("policy"),
+
            }));
+
        }
+
        Ok(None)
+
    }
+

+
    /// Get node tracking policies.
+
    pub fn node_policies(&self) -> Result<Box<dyn Iterator<Item = Node>>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT id, alias, policy FROM `following`")?
+
            .into_iter();
+
        let mut entries = Vec::new();
+

+
        while let Some(Ok(row)) = stmt.next() {
+
            let id = row.read("id");
+
            let alias = row.read::<&str, _>("alias").to_owned();
+
            let alias = alias
+
                .is_empty()
+
                .not()
+
                .then_some(alias.to_owned())
+
                .and_then(|s| Alias::from_str(&s).ok());
+
            let policy = row.read::<Policy, _>("policy");
+

+
            entries.push(Node { id, alias, policy });
+
        }
+
        Ok(Box::new(entries.into_iter()))
+
    }
+

+
    // TODO: see if sql can return iterator directly
+
    /// Get repository tracking policies.
+
    pub fn repo_policies(&self) -> Result<Box<dyn Iterator<Item = Repo>>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT id, scope, policy FROM `seeding`")?
+
            .into_iter();
+
        let mut entries = Vec::new();
+

+
        while let Some(Ok(row)) = stmt.next() {
+
            let id = row.read("id");
+
            let scope = row.read("scope");
+
            let policy = row.read::<Policy, _>("policy");
+

+
            entries.push(Repo { id, scope, policy });
+
        }
+
        Ok(Box::new(entries.into_iter()))
+
    }
+
}
+

+
impl<T> AliasStore for Config<T> {
+
    /// Retrieve `alias` of given node.
+
    /// Calls `Self::node_policy` under the hood.
+
    fn alias(&self, nid: &NodeId) -> Option<Alias> {
+
        self.node_policy(nid)
+
            .map(|node| node.and_then(|n| n.alias))
+
            .unwrap_or(None)
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use crate::assert_matches;
+

+
    use super::*;
+
    use crate::test::arbitrary;
+

+
    #[test]
+
    fn test_track_and_untrack_node() {
+
        let id = arbitrary::gen::<NodeId>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_node(&id, Some("eve")).unwrap());
+
        assert!(db.is_node_tracked(&id).unwrap());
+
        assert!(!db.track_node(&id, Some("eve")).unwrap());
+
        assert!(db.untrack_node(&id).unwrap());
+
        assert!(!db.is_node_tracked(&id).unwrap());
+
    }
+

+
    #[test]
+
    fn test_track_and_untrack_repo() {
+
        let id = arbitrary::gen::<Id>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_repo(&id, Scope::All).unwrap());
+
        assert!(db.is_repo_tracked(&id).unwrap());
+
        assert!(!db.track_repo(&id, Scope::All).unwrap());
+
        assert!(db.untrack_repo(&id).unwrap());
+
        assert!(!db.is_repo_tracked(&id).unwrap());
+
    }
+

+
    #[test]
+
    fn test_node_policies() {
+
        let ids = arbitrary::vec::<NodeId>(3);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        for id in &ids {
+
            assert!(db.track_node(id, None).unwrap());
+
        }
+
        let mut entries = db.node_policies().unwrap();
+
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[0]);
+
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[1]);
+
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[2]);
+
    }
+

+
    #[test]
+
    fn test_repo_policies() {
+
        let ids = arbitrary::vec::<Id>(3);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        for id in &ids {
+
            assert!(db.track_repo(id, Scope::All).unwrap());
+
        }
+
        let mut entries = db.repo_policies().unwrap();
+
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[0]);
+
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[1]);
+
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[2]);
+
    }
+

+
    #[test]
+
    fn test_update_alias() {
+
        let id = arbitrary::gen::<NodeId>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_node(&id, Some("eve")).unwrap());
+
        assert_eq!(
+
            db.node_policy(&id).unwrap().unwrap().alias,
+
            Some(Alias::from_str("eve").unwrap())
+
        );
+
        assert!(db.track_node(&id, None).unwrap());
+
        assert_eq!(db.node_policy(&id).unwrap().unwrap().alias, None);
+
        assert!(!db.track_node(&id, None).unwrap());
+
        assert!(db.track_node(&id, Some("alice")).unwrap());
+
        assert_eq!(
+
            db.node_policy(&id).unwrap().unwrap().alias,
+
            Some(Alias::new("alice"))
+
        );
+
    }
+

+
    #[test]
+
    fn test_update_scope() {
+
        let id = arbitrary::gen::<Id>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_repo(&id, Scope::All).unwrap());
+
        assert_eq!(db.repo_policy(&id).unwrap().unwrap().scope, Scope::All);
+
        assert!(db.track_repo(&id, Scope::Followed).unwrap());
+
        assert_eq!(db.repo_policy(&id).unwrap().unwrap().scope, Scope::Followed);
+
    }
+

+
    #[test]
+
    fn test_repo_policy() {
+
        let id = arbitrary::gen::<Id>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_repo(&id, Scope::All).unwrap());
+
        assert_eq!(db.repo_policy(&id).unwrap().unwrap().policy, Policy::Allow);
+
        assert!(db.set_repo_policy(&id, Policy::Block).unwrap());
+
        assert_eq!(db.repo_policy(&id).unwrap().unwrap().policy, Policy::Block);
+
    }
+

+
    #[test]
+
    fn test_node_policy() {
+
        let id = arbitrary::gen::<NodeId>(1);
+
        let mut db = Config::open(":memory:").unwrap();
+

+
        assert!(db.track_node(&id, None).unwrap());
+
        assert_eq!(db.node_policy(&id).unwrap().unwrap().policy, Policy::Allow);
+
        assert!(db.set_node_policy(&id, Policy::Block).unwrap());
+
        assert_eq!(db.node_policy(&id).unwrap().unwrap().policy, Policy::Block);
+
    }
+
}
deleted radicle/src/node/tracking.rs
@@ -1,158 +0,0 @@
-
pub mod config;
-
pub mod store;
-

-
use std::fmt;
-
use std::str::FromStr;
-

-
use serde::{Deserialize, Serialize};
-
use thiserror::Error;
-

-
use crate::prelude::Id;
-

-
pub use super::{Alias, NodeId};
-

-
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-
pub struct Repo {
-
    pub id: Id,
-
    pub scope: Scope,
-
    pub policy: Policy,
-
}
-

-
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-
pub struct Node {
-
    pub id: NodeId,
-
    pub alias: Option<Alias>,
-
    pub policy: Policy,
-
}
-

-
/// Resource policy.
-
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase")]
-
pub enum Policy {
-
    /// The resource is allowed.
-
    Allow,
-
    /// The resource is blocked.
-
    #[default]
-
    Block,
-
}
-

-
impl fmt::Display for Policy {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        match self {
-
            Self::Allow => write!(f, "allow"),
-
            Self::Block => write!(f, "block"),
-
        }
-
    }
-
}
-

-
impl FromStr for Policy {
-
    type Err = String;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        match s {
-
            "allow" => Ok(Self::Allow),
-
            "block" => Ok(Self::Block),
-
            _ => Err(s.to_owned()),
-
        }
-
    }
-
}
-

-
impl sqlite::BindableWithIndex for Policy {
-
    fn bind<I: sqlite::ParameterIndex>(
-
        self,
-
        stmt: &mut sqlite::Statement<'_>,
-
        i: I,
-
    ) -> sqlite::Result<()> {
-
        match self {
-
            Self::Allow => "allow",
-
            Self::Block => "block",
-
        }
-
        .bind(stmt, i)
-
    }
-
}
-

-
impl TryFrom<&sqlite::Value> for Policy {
-
    type Error = sqlite::Error;
-

-
    fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
-
        let message = Some("sql: invalid policy".to_owned());
-

-
        match value {
-
            sqlite::Value::String(s) if s == "allow" => Ok(Policy::Allow),
-
            sqlite::Value::String(s) if s == "block" => Ok(Policy::Block),
-
            _ => Err(sqlite::Error {
-
                code: None,
-
                message,
-
            }),
-
        }
-
    }
-
}
-

-
/// Follow scope of a seeded repository.
-
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase")]
-
pub enum Scope {
-
    /// Seed remotes that are explicitly followed.
-
    #[default]
-
    Followed,
-
    /// Seed all remotes.
-
    All,
-
}
-

-
impl fmt::Display for Scope {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        match self {
-
            Scope::Followed => f.write_str("followed"),
-
            Scope::All => f.write_str("all"),
-
        }
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
#[error("invalid tracking scope: {0:?}")]
-
pub struct ParseScopeError(String);
-

-
impl FromStr for Scope {
-
    type Err = ParseScopeError;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        match s {
-
            "followed" => Ok(Self::Followed),
-
            "all" => Ok(Self::All),
-
            _ => Err(ParseScopeError(s.to_string())),
-
        }
-
    }
-
}
-

-
impl sqlite::BindableWithIndex for Scope {
-
    fn bind<I: sqlite::ParameterIndex>(
-
        self,
-
        stmt: &mut sqlite::Statement<'_>,
-
        i: I,
-
    ) -> sqlite::Result<()> {
-
        let s = match self {
-
            Self::Followed => "followed",
-
            Self::All => "all",
-
        };
-
        s.bind(stmt, i)
-
    }
-
}
-

-
impl TryFrom<&sqlite::Value> for Scope {
-
    type Error = sqlite::Error;
-

-
    fn try_from(value: &sqlite::Value) -> Result<Self, Self::Error> {
-
        let message = Some("invalid remote scope".to_owned());
-

-
        match value {
-
            sqlite::Value::String(scope) => Scope::from_str(scope).map_err(|_| sqlite::Error {
-
                code: None,
-
                message,
-
            }),
-
            _ => Err(sqlite::Error {
-
                code: None,
-
                message,
-
            }),
-
        }
-
    }
-
}
deleted radicle/src/node/tracking/config.rs
@@ -1,166 +0,0 @@
-
use core::fmt;
-
use std::collections::HashSet;
-
use std::ops;
-

-
use log::error;
-
use thiserror::Error;
-

-
use crate::crypto::PublicKey;
-
use crate::prelude::{Id, NodeId};
-
use crate::storage::{Namespaces, ReadRepository as _, ReadStorage, RepositoryError};
-

-
pub use crate::node::tracking::store;
-
pub use crate::node::tracking::store::Config as Store;
-
pub use crate::node::tracking::store::Error;
-
pub use crate::node::tracking::{Alias, Node, Policy, Repo, Scope};
-

-
#[derive(Debug, Error)]
-
pub enum NamespacesError {
-
    #[error("failed to find tracking policy for {rid}")]
-
    FailedPolicy {
-
        rid: Id,
-
        #[source]
-
        err: Error,
-
    },
-
    #[error("cannot fetch {rid} as it is not tracked")]
-
    BlockedPolicy { rid: Id },
-
    #[error("failed to get tracking nodes for {rid}")]
-
    FailedNodes {
-
        rid: Id,
-
        #[source]
-
        err: Error,
-
    },
-
    #[error("failed to get delegates for {rid}")]
-
    FailedDelegates {
-
        rid: Id,
-
        #[source]
-
        err: RepositoryError,
-
    },
-
    #[error(transparent)]
-
    Git(#[from] crate::git::raw::Error),
-
    #[error("could not find any followed nodes for {rid}")]
-
    NoFollowed { rid: Id },
-
}
-

-
/// Tracking configuration.
-
pub struct Config<T> {
-
    /// Default policy, if a policy for a specific node or repository was not found.
-
    policy: Policy,
-
    /// Default scope, if a scope for a specific repository was not found.
-
    scope: Scope,
-
    /// Underlying configuration store.
-
    store: Store<T>,
-
}
-

-
// N.b. deriving `Debug` will require `T: Debug` so we manually
-
// implement it here.
-
impl<T> fmt::Debug for Config<T> {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        f.debug_struct("Config")
-
            .field("policy", &self.policy)
-
            .field("scope", &self.scope)
-
            .field("store", &self.store)
-
            .finish()
-
    }
-
}
-

-
impl<T> Config<T> {
-
    /// Create a new tracking configuration.
-
    pub fn new(policy: Policy, scope: Scope, store: Store<T>) -> Self {
-
        Self {
-
            policy,
-
            scope,
-
            store,
-
        }
-
    }
-

-
    /// Check if a repository is tracked.
-
    pub fn is_repo_tracked(&self, id: &Id) -> Result<bool, Error> {
-
        self.repo_policy(id)
-
            .map(|entry| entry.policy == Policy::Allow)
-
    }
-

-
    /// Check if a node is tracked.
-
    pub fn is_node_tracked(&self, id: &NodeId) -> Result<bool, Error> {
-
        self.node_policy(id)
-
            .map(|entry| entry.policy == Policy::Allow)
-
    }
-

-
    /// Get a node's tracking information.
-
    /// Returns the default policy if the node isn't found.
-
    pub fn node_policy(&self, id: &NodeId) -> Result<Node, Error> {
-
        Ok(self.store.node_policy(id)?.unwrap_or(Node {
-
            id: *id,
-
            alias: None,
-
            policy: self.policy,
-
        }))
-
    }
-

-
    /// Get a repository's tracking information.
-
    /// Returns the default policy if the repo isn't found.
-
    pub fn repo_policy(&self, id: &Id) -> Result<Repo, Error> {
-
        Ok(self.store.repo_policy(id)?.unwrap_or(Repo {
-
            id: *id,
-
            scope: self.scope,
-
            policy: self.policy,
-
        }))
-
    }
-

-
    pub fn namespaces_for<S>(&self, storage: &S, rid: &Id) -> Result<Namespaces, NamespacesError>
-
    where
-
        S: ReadStorage,
-
    {
-
        use NamespacesError::*;
-

-
        let entry = self
-
            .repo_policy(rid)
-
            .map_err(|err| FailedPolicy { rid: *rid, err })?;
-
        match entry.policy {
-
            Policy::Block => {
-
                error!(target: "service", "Attempted to fetch untracked repo {rid}");
-
                Err(NamespacesError::BlockedPolicy { rid: *rid })
-
            }
-
            Policy::Allow => match entry.scope {
-
                Scope::All => Ok(Namespaces::All),
-
                Scope::Followed => {
-
                    let nodes = self
-
                        .node_policies()
-
                        .map_err(|err| FailedNodes { rid: *rid, err })?;
-
                    let mut followed: HashSet<_> = nodes
-
                        .filter_map(|node| (node.policy == Policy::Allow).then_some(node.id))
-
                        .collect();
-

-
                    if let Ok(repo) = storage.repository(*rid) {
-
                        let delegates = repo
-
                            .delegates()
-
                            .map_err(|err| FailedDelegates { rid: *rid, err })?
-
                            .map(PublicKey::from);
-
                        followed.extend(delegates);
-
                    };
-
                    if followed.is_empty() {
-
                        // Nb. returning All here because the
-
                        // fetching logic will correctly determine
-
                        // followed and delegate remotes.
-
                        Ok(Namespaces::All)
-
                    } else {
-
                        Ok(Namespaces::Followed(followed))
-
                    }
-
                }
-
            },
-
        }
-
    }
-
}
-

-
impl<T> ops::Deref for Config<T> {
-
    type Target = Store<T>;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.store
-
    }
-
}
-

-
impl<T> ops::DerefMut for Config<T> {
-
    fn deref_mut(&mut self) -> &mut Self::Target {
-
        &mut self.store
-
    }
-
}
deleted radicle/src/node/tracking/schema.sql
@@ -1,31 +0,0 @@
-
--
-
-- Service configuration schema.
-
--
-

-
-- Node follow policies.
-
create table if not exists "following" (
-
  -- Node ID.
-
  "id"                 text      primary key not null,
-
  -- Node alias. May override the alias announced by the node.
-
  "alias"              text      default '',
-
  -- Tracking policy for this node.
-
  "policy"             text      default 'allow'
-
  --
-
) strict;
-

-
-- Repository seeding policies.
-
create table if not exists "seeding" (
-
  -- Repository ID.
-
  "id"                 text      primary key not null,
-
  -- Tracking scope for this repository.
-
  --
-
  -- Valid values are:
-
  --
-
  -- "followed"        seed repository delegates and remotes in the `following` table.
-
  -- "all"             seed all remotes.
-
  --
-
  "scope"              text      default 'followed',
-
  -- Tracking policy for this repository.
-
  "policy"             text      default 'allow'
-
  --
-
) strict;
deleted radicle/src/node/tracking/store.rs
@@ -1,436 +0,0 @@
-
#![allow(clippy::type_complexity)]
-
use std::marker::PhantomData;
-
use std::path::Path;
-
use std::{fmt, io, ops::Not as _, str::FromStr, time};
-

-
use sqlite as sql;
-
use thiserror::Error;
-

-
use crate::node::{Alias, AliasStore};
-
use crate::prelude::{Id, NodeId};
-

-
use super::{Node, Policy, Repo, Scope};
-

-
/// How long to wait for the database lock to be released before failing a read.
-
const DB_READ_TIMEOUT: time::Duration = time::Duration::from_secs(3);
-
/// How long to wait for the database lock to be released before failing a write.
-
const DB_WRITE_TIMEOUT: time::Duration = time::Duration::from_secs(6);
-

-
#[derive(Error, Debug)]
-
pub enum Error {
-
    /// I/O error.
-
    #[error("i/o error: {0}")]
-
    Io(#[from] io::Error),
-
    /// An Internal error.
-
    #[error("internal error: {0}")]
-
    Internal(#[from] sql::Error),
-
}
-

-
/// Read-only type witness.
-
pub struct Read;
-
/// Read-write type witness.
-
pub struct Write;
-

-
/// Read only config.
-
pub type ConfigReader = Config<Read>;
-
/// Read-write config.
-
pub type ConfigWriter = Config<Write>;
-

-
/// Tracking configuration.
-
pub struct Config<T> {
-
    db: sql::Connection,
-
    _marker: PhantomData<T>,
-
}
-

-
impl<T> fmt::Debug for Config<T> {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "Config(..)")
-
    }
-
}
-

-
impl Config<Read> {
-
    const SCHEMA: &'static str = include_str!("schema.sql");
-

-
    /// Same as [`Self::open`], but in read-only mode. This is useful to have multiple
-
    /// open databases, as no locking is required.
-
    pub fn reader<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
-
        let mut db =
-
            sql::Connection::open_with_flags(path, sqlite::OpenFlags::new().with_read_only())?;
-
        db.set_busy_timeout(DB_READ_TIMEOUT.as_millis() as usize)?;
-
        db.execute(Self::SCHEMA)?;
-

-
        Ok(Self {
-
            db,
-
            _marker: PhantomData,
-
        })
-
    }
-

-
    /// Create a new in-memory address book.
-
    pub fn memory() -> Result<Self, Error> {
-
        let db = sql::Connection::open_with_flags(
-
            ":memory:",
-
            sqlite::OpenFlags::new().with_read_only(),
-
        )?;
-
        db.execute(Self::SCHEMA)?;
-

-
        Ok(Self {
-
            db,
-
            _marker: PhantomData,
-
        })
-
    }
-
}
-

-
impl Config<Write> {
-
    const SCHEMA: &'static str = include_str!("schema.sql");
-

-
    /// Open a policy store at the given path. Creates a new store if it
-
    /// doesn't exist.
-
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
-
        let mut db = sql::Connection::open(path)?;
-
        db.set_busy_timeout(DB_WRITE_TIMEOUT.as_millis() as usize)?;
-
        db.execute(Self::SCHEMA)?;
-

-
        Ok(Self {
-
            db,
-
            _marker: PhantomData,
-
        })
-
    }
-

-
    /// Create a new in-memory address book.
-
    pub fn memory() -> Result<Self, Error> {
-
        let db = sql::Connection::open(":memory:")?;
-
        db.execute(Self::SCHEMA)?;
-

-
        Ok(Self {
-
            db,
-
            _marker: PhantomData,
-
        })
-
    }
-

-
    /// Get a read-only version of this store.
-
    pub fn read_only(self) -> ConfigReader {
-
        Config {
-
            db: self.db,
-
            _marker: PhantomData,
-
        }
-
    }
-

-
    /// Track a node.
-
    pub fn track_node(&mut self, id: &NodeId, alias: Option<&str>) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare(
-
            "INSERT INTO `following` (id, alias)
-
             VALUES (?1, ?2)
-
             ON CONFLICT DO UPDATE
-
             SET alias = ?2 WHERE alias != ?2",
-
        )?;
-

-
        stmt.bind((1, id))?;
-
        stmt.bind((2, alias.unwrap_or_default()))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Track a repository.
-
    pub fn track_repo(&mut self, id: &Id, scope: Scope) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare(
-
            "INSERT INTO `seeding` (id, scope)
-
             VALUES (?1, ?2)
-
             ON CONFLICT DO UPDATE
-
             SET scope = ?2 WHERE scope != ?2",
-
        )?;
-

-
        stmt.bind((1, id))?;
-
        stmt.bind((2, scope))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Set a node's tracking policy.
-
    pub fn set_node_policy(&mut self, id: &NodeId, policy: Policy) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare(
-
            "INSERT INTO `following` (id, policy)
-
             VALUES (?1, ?2)
-
             ON CONFLICT DO UPDATE
-
             SET policy = ?2 WHERE policy != ?2",
-
        )?;
-

-
        stmt.bind((1, id))?;
-
        stmt.bind((2, policy))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Set a repository's tracking policy.
-
    pub fn set_repo_policy(&mut self, id: &Id, policy: Policy) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare(
-
            "INSERT INTO `seeding` (id, policy)
-
             VALUES (?1, ?2)
-
             ON CONFLICT DO UPDATE
-
             SET policy = ?2 WHERE policy != ?2",
-
        )?;
-

-
        stmt.bind((1, id))?;
-
        stmt.bind((2, policy))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Untrack a node.
-
    pub fn untrack_node(&mut self, id: &NodeId) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare("DELETE FROM `following` WHERE id = ?")?;
-

-
        stmt.bind((1, id))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-

-
    /// Untrack a repository.
-
    pub fn untrack_repo(&mut self, id: &Id) -> Result<bool, Error> {
-
        let mut stmt = self.db.prepare("DELETE FROM `seeding` WHERE id = ?")?;
-

-
        stmt.bind((1, id))?;
-
        stmt.next()?;
-

-
        Ok(self.db.change_count() > 0)
-
    }
-
}
-

-
/// `Read` methods for `Config`. This implies that a
-
/// `Config<Write>` can access these functions as well.
-
impl<T> Config<T> {
-
    /// Check if a node is tracked.
-
    pub fn is_node_tracked(&self, id: &NodeId) -> Result<bool, Error> {
-
        Ok(matches!(
-
            self.node_policy(id)?,
-
            Some(Node {
-
                policy: Policy::Allow,
-
                ..
-
            })
-
        ))
-
    }
-

-
    /// Check if a repository is tracked.
-
    pub fn is_repo_tracked(&self, id: &Id) -> Result<bool, Error> {
-
        Ok(matches!(
-
            self.repo_policy(id)?,
-
            Some(Repo {
-
                policy: Policy::Allow,
-
                ..
-
            })
-
        ))
-
    }
-

-
    /// Get a node's tracking policy.
-
    pub fn node_policy(&self, id: &NodeId) -> Result<Option<Node>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT alias, policy FROM `following` WHERE id = ?")?;
-

-
        stmt.bind((1, id))?;
-

-
        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            let alias = row.read::<&str, _>("alias");
-
            let alias = alias
-
                .is_empty()
-
                .not()
-
                .then_some(alias.to_owned())
-
                .and_then(|s| Alias::from_str(&s).ok());
-
            let policy = row.read::<Policy, _>("policy");
-

-
            return Ok(Some(Node {
-
                id: *id,
-
                alias,
-
                policy,
-
            }));
-
        }
-
        Ok(None)
-
    }
-

-
    /// Get a repository's tracking policy.
-
    pub fn repo_policy(&self, id: &Id) -> Result<Option<Repo>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT scope, policy FROM `seeding` WHERE id = ?")?;
-

-
        stmt.bind((1, id))?;
-

-
        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            return Ok(Some(Repo {
-
                id: *id,
-
                scope: row.read::<Scope, _>("scope"),
-
                policy: row.read::<Policy, _>("policy"),
-
            }));
-
        }
-
        Ok(None)
-
    }
-

-
    /// Get node tracking policies.
-
    pub fn node_policies(&self) -> Result<Box<dyn Iterator<Item = Node>>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT id, alias, policy FROM `following`")?
-
            .into_iter();
-
        let mut entries = Vec::new();
-

-
        while let Some(Ok(row)) = stmt.next() {
-
            let id = row.read("id");
-
            let alias = row.read::<&str, _>("alias").to_owned();
-
            let alias = alias
-
                .is_empty()
-
                .not()
-
                .then_some(alias.to_owned())
-
                .and_then(|s| Alias::from_str(&s).ok());
-
            let policy = row.read::<Policy, _>("policy");
-

-
            entries.push(Node { id, alias, policy });
-
        }
-
        Ok(Box::new(entries.into_iter()))
-
    }
-

-
    // TODO: see if sql can return iterator directly
-
    /// Get repository tracking policies.
-
    pub fn repo_policies(&self) -> Result<Box<dyn Iterator<Item = Repo>>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT id, scope, policy FROM `seeding`")?
-
            .into_iter();
-
        let mut entries = Vec::new();
-

-
        while let Some(Ok(row)) = stmt.next() {
-
            let id = row.read("id");
-
            let scope = row.read("scope");
-
            let policy = row.read::<Policy, _>("policy");
-

-
            entries.push(Repo { id, scope, policy });
-
        }
-
        Ok(Box::new(entries.into_iter()))
-
    }
-
}
-

-
impl<T> AliasStore for Config<T> {
-
    /// Retrieve `alias` of given node.
-
    /// Calls `Self::node_policy` under the hood.
-
    fn alias(&self, nid: &NodeId) -> Option<Alias> {
-
        self.node_policy(nid)
-
            .map(|node| node.and_then(|n| n.alias))
-
            .unwrap_or(None)
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use crate::assert_matches;
-

-
    use super::*;
-
    use crate::test::arbitrary;
-

-
    #[test]
-
    fn test_track_and_untrack_node() {
-
        let id = arbitrary::gen::<NodeId>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_node(&id, Some("eve")).unwrap());
-
        assert!(db.is_node_tracked(&id).unwrap());
-
        assert!(!db.track_node(&id, Some("eve")).unwrap());
-
        assert!(db.untrack_node(&id).unwrap());
-
        assert!(!db.is_node_tracked(&id).unwrap());
-
    }
-

-
    #[test]
-
    fn test_track_and_untrack_repo() {
-
        let id = arbitrary::gen::<Id>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_repo(&id, Scope::All).unwrap());
-
        assert!(db.is_repo_tracked(&id).unwrap());
-
        assert!(!db.track_repo(&id, Scope::All).unwrap());
-
        assert!(db.untrack_repo(&id).unwrap());
-
        assert!(!db.is_repo_tracked(&id).unwrap());
-
    }
-

-
    #[test]
-
    fn test_node_policies() {
-
        let ids = arbitrary::vec::<NodeId>(3);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        for id in &ids {
-
            assert!(db.track_node(id, None).unwrap());
-
        }
-
        let mut entries = db.node_policies().unwrap();
-
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[0]);
-
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[1]);
-
        assert_matches!(entries.next(), Some(Node { id, .. }) if id == ids[2]);
-
    }
-

-
    #[test]
-
    fn test_repo_policies() {
-
        let ids = arbitrary::vec::<Id>(3);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        for id in &ids {
-
            assert!(db.track_repo(id, Scope::All).unwrap());
-
        }
-
        let mut entries = db.repo_policies().unwrap();
-
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[0]);
-
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[1]);
-
        assert_matches!(entries.next(), Some(Repo { id, .. }) if id == ids[2]);
-
    }
-

-
    #[test]
-
    fn test_update_alias() {
-
        let id = arbitrary::gen::<NodeId>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_node(&id, Some("eve")).unwrap());
-
        assert_eq!(
-
            db.node_policy(&id).unwrap().unwrap().alias,
-
            Some(Alias::from_str("eve").unwrap())
-
        );
-
        assert!(db.track_node(&id, None).unwrap());
-
        assert_eq!(db.node_policy(&id).unwrap().unwrap().alias, None);
-
        assert!(!db.track_node(&id, None).unwrap());
-
        assert!(db.track_node(&id, Some("alice")).unwrap());
-
        assert_eq!(
-
            db.node_policy(&id).unwrap().unwrap().alias,
-
            Some(Alias::new("alice"))
-
        );
-
    }
-

-
    #[test]
-
    fn test_update_scope() {
-
        let id = arbitrary::gen::<Id>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_repo(&id, Scope::All).unwrap());
-
        assert_eq!(db.repo_policy(&id).unwrap().unwrap().scope, Scope::All);
-
        assert!(db.track_repo(&id, Scope::Followed).unwrap());
-
        assert_eq!(db.repo_policy(&id).unwrap().unwrap().scope, Scope::Followed);
-
    }
-

-
    #[test]
-
    fn test_repo_policy() {
-
        let id = arbitrary::gen::<Id>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_repo(&id, Scope::All).unwrap());
-
        assert_eq!(db.repo_policy(&id).unwrap().unwrap().policy, Policy::Allow);
-
        assert!(db.set_repo_policy(&id, Policy::Block).unwrap());
-
        assert_eq!(db.repo_policy(&id).unwrap().unwrap().policy, Policy::Block);
-
    }
-

-
    #[test]
-
    fn test_node_policy() {
-
        let id = arbitrary::gen::<NodeId>(1);
-
        let mut db = Config::open(":memory:").unwrap();
-

-
        assert!(db.track_node(&id, None).unwrap());
-
        assert_eq!(db.node_policy(&id).unwrap().unwrap().policy, Policy::Allow);
-
        assert!(db.set_node_policy(&id, Policy::Block).unwrap());
-
        assert_eq!(db.node_policy(&id).unwrap().unwrap().policy, Policy::Block);
-
    }
-
}
modified radicle/src/profile.rs
@@ -20,7 +20,7 @@ use thiserror::Error;
use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{keystore, Keystore, Passphrase};
use crate::crypto::{PublicKey, Signer};
-
use crate::node::{tracking, Alias, AliasStore};
+
use crate::node::{policy, Alias, AliasStore};
use crate::prelude::Did;
use crate::prelude::{Id, NodeId};
use crate::storage::git::transport;
@@ -146,7 +146,7 @@ pub enum Error {
    #[error("profile key `{0}` is not registered with ssh-agent")]
    KeyNotRegistered(PublicKey),
    #[error(transparent)]
-
    TrackingStore(#[from] node::tracking::store::Error),
+
    TrackingStore(#[from] node::policy::store::Error),
    #[error(transparent)]
    DatabaseStore(#[from] node::db::Error),
}
@@ -342,17 +342,17 @@ impl Profile {
    }

    /// Return a read-only handle to the tracking configuration of the node.
-
    pub fn tracking(&self) -> Result<tracking::store::ConfigReader, tracking::store::Error> {
+
    pub fn tracking(&self) -> Result<policy::store::ConfigReader, policy::store::Error> {
        let path = self.home.node().join(node::TRACKING_DB_FILE);
-
        let config = tracking::store::Config::reader(path)?;
+
        let config = policy::store::Config::reader(path)?;

        Ok(config)
    }

    /// Return a read-write handle to the tracking configuration of the node.
-
    pub fn tracking_mut(&self) -> Result<tracking::store::ConfigWriter, tracking::store::Error> {
+
    pub fn tracking_mut(&self) -> Result<policy::store::ConfigWriter, policy::store::Error> {
        let path = self.home.node().join(node::TRACKING_DB_FILE);
-
        let config = tracking::store::Config::open(path)?;
+
        let config = policy::store::Config::open(path)?;

        Ok(config)
    }
@@ -388,7 +388,7 @@ impl std::ops::DerefMut for Profile {
/// Holds multiple alias stores, and will try
/// them one by one when asking for an alias.
pub struct Aliases {
-
    tracking: Option<tracking::store::ConfigReader>,
+
    tracking: Option<policy::store::ConfigReader>,
    db: Option<node::Database>,
}

@@ -488,17 +488,17 @@ impl Home {
    }

    /// Return a read-only handle to the tracking configuration of the node.
-
    pub fn tracking(&self) -> Result<tracking::store::ConfigReader, tracking::store::Error> {
+
    pub fn tracking(&self) -> Result<policy::store::ConfigReader, policy::store::Error> {
        let path = self.node().join(node::TRACKING_DB_FILE);
-
        let config = tracking::store::Config::reader(path)?;
+
        let config = policy::store::Config::reader(path)?;

        Ok(config)
    }

    /// Return a read-write handle to the tracking configuration of the node.
-
    pub fn tracking_mut(&self) -> Result<tracking::store::ConfigWriter, tracking::store::Error> {
+
    pub fn tracking_mut(&self) -> Result<policy::store::ConfigWriter, policy::store::Error> {
        let path = self.node().join(node::TRACKING_DB_FILE);
-
        let config = tracking::store::Config::open(path)?;
+
        let config = policy::store::Config::open(path)?;

        Ok(config)
    }