use anyhow::{bail, Result};
use bytes::Bytes;
use futures::Stream;
use serde::Serialize;
use tokio::net::TcpStream;
use crate::accounts::Directory;
use crate::names;
use crate::service::Service;
use crate::tree::{BlobContent, TreeEntry};
fn seg(name: &str) -> String {
name.replace('/', "%2F")
}
#[derive(Clone)]
pub struct StorageClient {
base_url: String,
tcp_addr: String,
http: reqwest::Client,
}
impl StorageClient {
pub fn new(base_url: String, tcp_addr: String) -> Self {
StorageClient { base_url: base_url.trim_end_matches('/').to_string(), tcp_addr, http: reqwest::Client::new() }
}
fn url(&self, path: &str) -> String {
format!("{}{path}", self.base_url)
}
pub async fn resolve(&self, dir: &Directory, raw: &str) -> Option<String> {
let path = names::from_wire_form(raw)?;
let name = dir.repo_path(dir.resolve_repo(&path)?)?;
if self.exists(&name).await {
Some(name)
} else {
None
}
}
pub async fn exists(&self, name: &str) -> bool {
self.http
.head(self.url(&format!("/repos/{}", seg(name))))
.send()
.await
.map(|r| r.status().is_success())
.unwrap_or(false)
}
pub async fn init_bare(&self, name: &str) -> Result<()> {
let resp = self.http.post(self.url(&format!("/repos/{}", seg(name)))).send().await?;
if !resp.status().is_success() {
bail!("create repo {name:?}: {}", resp.text().await.unwrap_or_default());
}
Ok(())
}
pub async fn list(&self) -> Result<Vec<String>> {
Ok(self.http.get(self.url("/repos")).send().await?.error_for_status()?.json().await?)
}
pub async fn run_git(&self, name: &str, args: &[&str]) -> Result<()> {
#[derive(Serialize)]
struct Req<'a> {
args: &'a [&'a str],
}
let resp = self.http.post(self.url(&format!("/repos/{}/git", seg(name)))).json(&Req { args }).send().await?;
if !resp.status().is_success() {
bail!("git {args:?} on {name:?}: {}", resp.text().await.unwrap_or_default());
}
Ok(())
}
pub async fn info_refs(&self, name: &str, service: Service) -> Result<Vec<u8>> {
let resp = self
.http
.get(self.url(&format!("/repos/{}/info-refs", seg(name))))
.query(&[("service", format!("git-{}", service.git_arg()))])
.send()
.await?
.error_for_status()?;
Ok(resp.bytes().await?.to_vec())
}
pub async fn stream_git(
&self,
name: &str,
service: Service,
git_protocol_header: Option<&str>,
content_encoding_gzip: bool,
body: reqwest::Body,
) -> Result<impl Stream<Item = reqwest::Result<Bytes>>> {
let mut req = self.http.post(self.url(&format!("/repos/{}/{}", seg(name), service.git_arg()))).body(body);
if let Some(proto) = git_protocol_header {
req = req.header("Git-Protocol", proto);
}
if content_encoding_gzip {
req = req.header(reqwest::header::CONTENT_ENCODING, "gzip");
}
let resp = req.send().await?.error_for_status()?;
Ok(resp.bytes_stream())
}
pub async fn branches(&self, name: &str) -> Result<Vec<String>> {
Ok(self
.http
.get(self.url(&format!("/repos/{}/branches", seg(name))))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn all_files(&self, name: &str, git_ref: &str) -> Result<Vec<String>> {
Ok(self
.http
.get(self.url(&format!("/repos/{}/files/{git_ref}", seg(name))))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn tree(&self, name: &str, git_ref: &str, path: &str) -> Result<Vec<TreeEntry>> {
let url = if path.is_empty() {
self.url(&format!("/repos/{}/tree/{git_ref}", seg(name)))
} else {
self.url(&format!("/repos/{}/tree/{git_ref}/{path}", seg(name)))
};
Ok(self.http.get(url).send().await?.error_for_status()?.json().await?)
}
pub async fn blob(&self, name: &str, git_ref: &str, path: &str) -> Result<BlobContent> {
let url = self.url(&format!("/repos/{}/blob/{git_ref}/{path}", seg(name)));
Ok(self.http.get(url).send().await?.error_for_status()?.json().await?)
}
pub async fn connect_git_exec(&self, name: &str, service: Service) -> Result<TcpStream> {
use tokio::io::AsyncWriteExt;
let mut stream = TcpStream::connect(&self.tcp_addr).await?;
stream.write_all(format!("{} {name}\n", service.git_arg()).as_bytes()).await?;
Ok(stream)
}
}