use std::{io::Write, path::Path, process::ExitCode};
use clap::{Parser, Subcommand};
use directories::ProjectDirs;
use edit::edit;
use perma_core::{Redirect, RedirectParseError, Uri};
use serde::{Deserialize, Serialize};
#[derive(Parser, Debug, Clone)]
struct Cli {
#[clap(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug, Clone)]
enum Command {
Auth {
#[clap(short, long)]
server: Uri,
},
Edit,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct Config {
#[serde(default)]
server: Option<Uri>,
#[serde(default)]
psk: Option<String>,
}
fn read_config(config_dir: &Path) -> Option<Config> {
toml::from_str(&std::fs::read_to_string(config_dir.join("config.toml")).ok()?).ok()
}
fn write_config(config_dir: &Path, config: &Config) -> std::io::Result<()> {
std::fs::write(
config_dir.join("config.toml"),
toml::to_string_pretty(config).unwrap(),
)
}
fn parse_redirects(raw: &str) -> Result<Vec<Redirect>, (&str, RedirectParseError)> {
raw.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(|l| l.parse::<Redirect>().map_err(|e| (l, e)))
.collect::<Result<Vec<_>, _>>()
}
struct Client {
base: Uri,
psk: String,
agent: ureq::Agent,
}
impl Client {
fn new(base: Uri, psk: String) -> Self {
Self {
base,
psk,
agent: ureq::Agent::new_with_defaults(),
}
}
fn get_raw_redirects(&self) -> Result<Option<String>, ureq::Error> {
let file_url = self.base.join("/api/v1/_redirects").unwrap();
let result = self
.agent
.get(file_url.as_str())
.header("Authorization", format!("Bearer sk-{}", self.psk))
.header("Accept", "text/redirects")
.call();
match result {
Ok(mut res) => Ok(Some(res.body_mut().read_to_string()?)),
Err(err) => match err {
ureq::Error::StatusCode(code) if code == 404 => Ok(None),
_ => Err(err),
},
}
}
fn put_raw_redirects(&self, s: impl AsRef<str>) -> Result<(), ureq::Error> {
let file_url = self.base.join("/api/v1/_redirects").unwrap();
self.agent
.put(file_url.as_str())
.header("Authorization", format!("Bearer sk-{}", self.psk))
.content_type("text/redirects")
.send(s.as_ref().as_bytes())?;
Ok(())
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
let project_dirs = ProjectDirs::from("dev.workers", "perma", "perma")
.expect("cannot initialize project directories");
let config_dir = project_dirs.config_local_dir();
let _ = std::fs::create_dir_all(config_dir);
let config = read_config(&config_dir).unwrap_or_default();
match cli.command {
Command::Auth { server } => {
let mut config = config;
config.server = Some(server);
let mut psk = String::new();
print!("PSK: ");
let _ = std::io::stdout().flush();
let _ = std::io::stdin().read_line(&mut psk);
config.psk = Some(psk.trim().to_string());
match write_config(&config_dir, &config) {
Ok(_) => {
eprintln!("Server config saved!");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("Cannot save config: {}", e);
ExitCode::FAILURE
}
}
}
Command::Edit => {
if let (Some(server), Some(psk)) = (config.server, config.psk) {
let client = Client::new(server, psk);
match client.get_raw_redirects() {
Ok(res) => {
let redirects = res.unwrap_or_default();
match edit(&redirects) {
Ok(edited) => {
if let Ok(redirects) = parse_redirects(&edited) {
println!("New redirects:");
println!(
"{}",
redirects
.iter()
.map(|r| {
format!(
"{} -> {} [{}]",
r.from(),
r.to(),
r.code().as_u16()
)
})
.collect::<Vec<_>>()
.join("\n")
);
print!("\nConfirm? [Y/n] ");
let _ = std::io::stdout().flush();
let mut answer = String::new();
std::io::stdin().read_line(&mut answer).unwrap();
let answer = answer.trim().to_string();
if answer.is_empty() || answer.eq_ignore_ascii_case("y") {
match client.put_raw_redirects(&edited) {
Ok(_) => {
println!("\nUpdate redirects successfully");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("\nCannot update redirects: {}", e);
ExitCode::FAILURE
}
}
} else {
eprintln!("\nUser cancelled");
ExitCode::SUCCESS
}
} else {
ExitCode::FAILURE
}
}
Err(e) => {
eprintln!("Edit failed: {}", e);
ExitCode::FAILURE
}
}
}
Err(e) => {
eprintln!("Cannot get redirects: {}", e);
ExitCode::FAILURE
}
}
} else {
eprintln!(
"Please configure CLI with `{} auth` first.",
std::env::args().next().unwrap()
);
ExitCode::FAILURE
}
}
}
}