140 lines
4.1 KiB
Rust
140 lines
4.1 KiB
Rust
use anyhow::{anyhow, Result};
|
|
use gitea::GiteaAPI;
|
|
use log::{debug, error, info};
|
|
use serde::Deserialize;
|
|
use std::{collections::HashMap, process::exit, sync::Arc};
|
|
use tiny_http::{Request, Response, Server};
|
|
|
|
use crate::sourcehut::SourcehutAPI;
|
|
|
|
mod gitea;
|
|
mod sourcehut;
|
|
|
|
fn default_bind() -> String {
|
|
"0.0.0.0:8000".to_string()
|
|
}
|
|
|
|
fn default_sourcehut_api() -> String {
|
|
"https://git.sr.ht/query".to_string()
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct Config {
|
|
/// Bind address
|
|
#[serde(default = "default_bind")]
|
|
bind: String,
|
|
|
|
/// Sourcehut API endpoint
|
|
#[serde(default = "default_sourcehut_api")]
|
|
sourcehut_api: String,
|
|
|
|
/// Sourcehut API token (from https://meta.sr.ht/oauth2, needs `git.sr.ht/REPOSITORY` grant)
|
|
#[serde(default = "default_sourcehut_api")]
|
|
sourcehut_token: String,
|
|
|
|
/// Base URL for Gitea API (without the /v1)
|
|
gitea_api_base: String,
|
|
/// username:password for basic auth
|
|
gitea_auth: String,
|
|
|
|
/// URL to set as sourcehut webhook target where this service is reacheable
|
|
url: String,
|
|
|
|
/// Comma separated list of repository map from sourcehut to Gitea e.g. "sourcehutrepo:owner/gitearepo"
|
|
repositories: String,
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let _ = dotenvy::dotenv();
|
|
|
|
env_logger::init();
|
|
|
|
let config = envy::prefixed("MIRROR_").from_env::<Config>()?;
|
|
|
|
// Parse repository mapping into a hashmap
|
|
let repository_map: HashMap<&str, &str> = config
|
|
.repositories
|
|
.split(",")
|
|
.map(|s| s.split_once(":").unwrap())
|
|
.collect();
|
|
|
|
if repository_map.is_empty() {
|
|
return Err(anyhow!("No repositories provided"));
|
|
}
|
|
|
|
debug!("Repository map: {:?}", repository_map);
|
|
|
|
// Set up Gitea API and check that provided repositories exist
|
|
let gitea = GiteaAPI::new(&config.gitea_api_base, &config.gitea_auth);
|
|
let repositories = repository_map.values().map(|&s| s).collect::<Vec<_>>();
|
|
gitea.check_repositories(&repositories)?;
|
|
|
|
// Set up webhooks via GQL API to sourcehut
|
|
let mut srht = SourcehutAPI::new(&config.sourcehut_api, &config.sourcehut_token);
|
|
srht.init_webhook(&config.url)?;
|
|
|
|
let api = Arc::new(srht);
|
|
let r = api.clone();
|
|
|
|
// Set up termination handler to delete webhook
|
|
ctrlc::set_handler(move || {
|
|
_ = r.delete_webhook();
|
|
exit(0)
|
|
})
|
|
.expect("Error setting termination handler");
|
|
|
|
// Listen for webhooks and handle them
|
|
let server = Server::http(config.bind).map_err(|err| anyhow!(err))?;
|
|
for request in server.incoming_requests() {
|
|
debug!("Received request from {:?}", request.remote_addr());
|
|
match handle_request(request) {
|
|
Ok(Some(repository)) => {
|
|
// Get mapped repository, if it exists
|
|
if let Some(mapped) = repository_map.get(repository.as_str()) {
|
|
info!("Syncing repository {}", mapped);
|
|
if let Err(err) = gitea.sync(&mapped) {
|
|
error!("Error syncing repository: {}", err);
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
error!("Error handling webhook: {}", err);
|
|
}
|
|
_ => {
|
|
// Invalid request, ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_request(mut request: Request) -> Result<Option<String>> {
|
|
// Make sure the content type is JSON
|
|
let is_json = request
|
|
.headers()
|
|
.iter()
|
|
.find(|h| h.field.as_str() == "Content-Type" && h.value == "application/json")
|
|
.ok_or_else(|| anyhow!("Invalid Content-Type header"));
|
|
|
|
match is_json {
|
|
Ok(_) => {
|
|
let mut content = String::new();
|
|
request.as_reader().read_to_string(&mut content)?;
|
|
let repository = SourcehutAPI::read_webhook_payload(&content)?;
|
|
request.respond(Response::empty(204))?;
|
|
|
|
return Ok(Some(repository.name));
|
|
}
|
|
Err(err) => {
|
|
debug!(
|
|
"Rejecting request because of invalid Content-Type header: {}",
|
|
err
|
|
);
|
|
request.respond(Response::from_string(err.to_string()).with_status_code(400))?
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|