OXYGENHUB
# 99ca93d60bca oxygenhub / crates / hub / src / main.rs
7.8 KiB · Rust · last changed Milestone 0.1: browse 1 month ago source blame history raw
1
//! `oxygenhub`: `init`, `serve`, `admin`, `check` (hub spec §17.3).
2
3
use clap::{Parser, Subcommand};
4
use hub_api::Visibility;
5
use oxygenhub::config::Config;
6
use oxygenhub::repos::RepoName;
7
use oxygenhub::{db, keys};
8
use std::path::PathBuf;
9
10
#[derive(Parser)]
11
#[command(name = "oxygenhub", version, about = "A forge for Oxygen repositories")]
12
struct Cli {
13
    /// The data directory (hub spec §3.2)
14
    #[arg(long, global = true, env = "OXYHUB_DATA", default_value = "data")]
15
    data: PathBuf,
16
    #[command(subcommand)]
17
    cmd: Cmd,
18
}
19
20
#[derive(Subcommand)]
21
enum Cmd {
22
    /// Create the data directory, keys, database and a default hub.toml
23
    Init {
24
        /// What links and clone URLs start with
25
        #[arg(long)]
26
        base_url: Option<String>,
27
        /// Generate a self-signed TLS certificate (developer edition)
28
        #[arg(long)]
29
        self_signed: bool,
30
    },
31
    /// Run the hub
32
    Serve,
33
    /// Offline administration
34
    Admin {
35
        #[command(subcommand)]
36
        cmd: AdminCmd,
37
    },
38
    /// Verify every repository's objects and signatures
39
    Check,
40
}
41
42
#[derive(Subcommand)]
43
enum AdminCmd {
44
    /// Repositories
45
    Repo {
46
        #[command(subcommand)]
47
        cmd: RepoCmd,
48
    },
49
}
50
51
#[derive(Subcommand)]
52
enum RepoCmd {
53
    /// Create an empty repository OWNER/NAME
54
    Create {
55
        slug: String,
56
        #[arg(long, default_value = "")]
57
        description: String,
58
        #[arg(long, default_value = "public")]
59
        visibility: String,
60
        /// Bind to this repository id now (32 hex characters, from `.oxy/id`);
61
        /// otherwise the first push binds it
62
        #[arg(long)]
63
        id: Option<String>,
64
    },
65
    /// List repositories
66
    List,
67
    /// Move a repository to the trash
68
    Delete { slug: String },
69
    /// Change the description, default bookmark or visibility
70
    Set {
71
        slug: String,
72
        #[arg(long)]
73
        description: Option<String>,
74
        #[arg(long)]
75
        default_bookmark: Option<String>,
76
        #[arg(long)]
77
        visibility: Option<String>,
78
    },
79
}
80
81
#[tokio::main]
82
async fn main() -> anyhow::Result<()> {
83
    tracing_subscriber::fmt()
84
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env().add_directive("oxygenhub=info".parse()?))
85
        .init();
86
    let cli = Cli::parse();
87
    match cli.cmd {
88
        Cmd::Init { base_url, self_signed } => init(&cli.data, base_url, self_signed).await,
89
        Cmd::Serve => {
90
            let config = Config::load(&cli.data)?;
91
            let app = oxygenhub::build(config).await?;
92
            oxygenhub::serve(app).await
93
        }
94
        Cmd::Admin { cmd: AdminCmd::Repo { cmd } } => {
95
            let config = Config::load(&cli.data)?;
96
            let app = oxygenhub::build(config).await?;
97
            repo_admin(&app, cmd).await
98
        }
99
        Cmd::Check => {
100
            let config = Config::load(&cli.data)?;
101
            let app = oxygenhub::build(config).await?;
102
            check(&app).await
103
        }
104
    }
105
}
106
107
async fn init(data: &PathBuf, base_url: Option<String>, self_signed: bool) -> anyhow::Result<()> {
108
    std::fs::create_dir_all(data)?;
109
    let toml_path = data.join("hub.toml");
110
    if !toml_path.exists() {
111
        let base = base_url.clone().unwrap_or_else(|| "http://127.0.0.1:7300".into());
112
        let mut text = String::from("# Oxygenhub configuration (docs/spec.md section 17.1)\n\n[site]\nname = \"Oxygenhub\"\n\n[server]\nbind = \"127.0.0.1:7300\"\n");
113
        if self_signed {
114
            text.push_str("tls_bind = \"0.0.0.0:7343\"\n");
115
        }
116
        text.push_str(&format!("base_url = \"{base}\"\n\n[tls]\nself_signed = {self_signed}\n\n[auth]\n# Bearer tokens allowed to push until accounts exist.\npush_tokens = []\n"));
117
        std::fs::write(&toml_path, text)?;
118
        println!("wrote {}", toml_path.display());
119
    }
120
    let mut config = Config::load(data)?;
121
    if let Some(b) = base_url {
122
        config.server.base_url = b;
123
    }
124
    let app = oxygenhub::build(config).await?;
125
    println!("hub key {}", app.hub_key.key.to_hex());
126
    if (self_signed || app.config.tls.self_signed)
127
        && keys::ensure_self_signed(&app.config.tls_cert(), &app.config.tls_key(), app.config.base_url())?
128
    {
129
        println!("wrote self-signed certificate {}", app.config.tls_cert().display());
130
    }
131
    println!("data directory {} is ready", data.display());
132
    Ok(())
133
}
134
135
async fn repo_admin(app: &oxygenhub::Shared, cmd: RepoCmd) -> anyhow::Result<()> {
136
    match cmd {
137
        RepoCmd::Create { slug, description, visibility, id } => {
138
            let name = RepoName::parse(&slug).ok_or_else(|| anyhow::anyhow!("{slug:?} is not a valid owner/name"))?;
139
            let visibility = Visibility::parse(&visibility).ok_or_else(|| anyhow::anyhow!("visibility is public, internal or private"))?;
140
            let id = match id {
141
                Some(h) => {
142
                    let bytes = hex::decode(h.trim()).ok().and_then(|v| <[u8; 16]>::try_from(v).ok());
143
                    Some(bytes.ok_or_else(|| anyhow::anyhow!("--id takes 32 hex characters"))?)
144
                }
145
                None => None,
146
            };
147
            if db::repos::get(&app.db, &name.owner, &name.name).await?.is_some() {
148
                anyhow::bail!("{name} already exists");
149
            }
150
            app.repos.create(&name, id)?;
151
            db::repos::create(&app.db, &name.owner, &name.name, &description, visibility).await?;
152
            println!("created {name}: oxy clone {}/{name}", app.config.base_url());
153
        }
154
        RepoCmd::List => {
155
            for r in db::repos::list(&app.db).await? {
156
                println!("{:<40} {:<8} {}", r.slug(), r.visibility, r.description);
157
            }
158
        }
159
        RepoCmd::Delete { slug } => {
160
            let name = RepoName::parse(&slug).ok_or_else(|| anyhow::anyhow!("{slug:?} is not a valid owner/name"))?;
161
            let row = db::repos::get(&app.db, &name.owner, &name.name).await?.ok_or_else(|| anyhow::anyhow!("{name} does not exist"))?;
162
            let dest = app.repos.trash(&name)?;
163
            db::repos::delete(&app.db, row.id).await?;
164
            println!("moved {name} to {}", dest.display());
165
        }
166
        RepoCmd::Set { slug, description, default_bookmark, visibility } => {
167
            let name = RepoName::parse(&slug).ok_or_else(|| anyhow::anyhow!("{slug:?} is not a valid owner/name"))?;
168
            let row = db::repos::get(&app.db, &name.owner, &name.name).await?.ok_or_else(|| anyhow::anyhow!("{name} does not exist"))?;
169
            if let Some(d) = description {
170
                db::repos::set_description(&app.db, row.id, &d).await?;
171
            }
172
            if let Some(b) = default_bookmark {
173
                db::repos::set_default_bookmark(&app.db, row.id, &b).await?;
174
            }
175
            if let Some(v) = visibility {
176
                let v = Visibility::parse(&v).ok_or_else(|| anyhow::anyhow!("visibility is public, internal or private"))?;
177
                db::repos::set_visibility(&app.db, row.id, v).await?;
178
            }
179
            println!("updated {name}");
180
        }
181
    }
182
    Ok(())
183
}
184
185
async fn check(app: &oxygenhub::Shared) -> anyhow::Result<()> {
186
    let mut problems = 0;
187
    for r in db::repos::list(&app.db).await? {
188
        let name = RepoName::new(&r.owner, &r.name).unwrap();
189
        let report = app.repos.read(&name, oxygenhub::repos::view::verification).await;
190
        match report {
191
            Ok(v) => {
192
                println!(
193
                    "{:<40} {} changes, {} signed, {} sealed, {} unsigned, {} invalid",
194
                    name,
195
                    v.total,
196
                    v.signed,
197
                    v.sealed,
198
                    v.unsigned,
199
                    v.invalid.len()
200
                );
201
                problems += v.invalid.len();
202
            }
203
            Err(e) => {
204
                println!("{name:<40} ERROR {e}");
205
                problems += 1;
206
            }
207
        }
208
    }
209
    if problems > 0 {
210
        anyhow::bail!("{problems} problem(s)");
211
    }
212
    Ok(())
213
}