use crate::config::Config;
use anyhow::{Context, Result};
use reqwest::{
blocking::Client,
header::{HeaderMap, HeaderValue, AUTHORIZATION},
};
use std::fs;
use std::path::Path;
use std::time::Duration;
pub struct GcsService {
client: Client,
token: String,
registry_path: String,
release_bundle_name: String,
checksum_file_name: String,
}
impl GcsService {
pub fn new(token: String, registry_path: String) -> Self {
Self::with_bundle_names(
token,
registry_path,
"release.tar.gz".to_string(),
"checksums.txt".to_string(),
)
}
pub fn with_bundle_names(
token: String,
registry_path: String,
release_bundle_name: String,
checksum_file_name: String,
) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(120)) .connect_timeout(Duration::from_secs(30)) .build()
.expect("Failed to build HTTP client");
Self {
client,
token,
registry_path,
release_bundle_name,
checksum_file_name,
}
}
pub fn download_binary(&self, version: &str, output_path: &Path) -> Result<()> {
let normalized_version = Config::normalize_version(version);
let url = format!(
"{}/releases/{}/{}",
self.registry_path, normalized_version, self.release_bundle_name
);
let mut request = self.client.get(&url);
if !self.token.is_empty() {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", self.token))?,
);
request = request.headers(headers);
}
let response = request.send().context("Failed to download binary")?;
if !response.status().is_success() {
anyhow::bail!("Failed to download binary: HTTP {}", response.status());
}
let content = response
.bytes()
.context("Failed to read response content")?;
fs::write(output_path, content).context("Failed to save binary")?;
Ok(())
}
pub fn verify_version(&self, version: &str) -> Result<bool> {
let normalized_version = Config::normalize_version(version);
let url = format!(
"{}/releases/{}/{}",
self.registry_path, normalized_version, self.checksum_file_name
);
let mut request = self.client.head(&url);
if !self.token.is_empty() {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", self.token))?,
);
request = request.headers(headers);
}
let response = request.send().context("Failed to verify version")?;
Ok(response.status().is_success())
}
pub fn get_latest_version(&self) -> Result<String> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let url = format!("{}/latest-version.txt?t={}", self.registry_path, timestamp);
let mut request = self.client.get(&url);
let mut headers = HeaderMap::new();
headers.insert(
"Cache-Control",
HeaderValue::from_static("no-cache, no-store, must-revalidate"),
);
headers.insert("Pragma", HeaderValue::from_static("no-cache"));
headers.insert("Expires", HeaderValue::from_static("0"));
if !self.token.is_empty() {
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", self.token))?,
);
}
request = request.headers(headers);
let response = request.send().context("Failed to fetch latest version")?;
if !response.status().is_success() {
anyhow::bail!("Failed to fetch latest version: HTTP {}", response.status());
}
let version = response
.text()
.context("Failed to read latest version from response")?;
Ok(Config::format_version(version.trim()))
}
pub fn download_signature(&self, version: &str, output_path: &Path) -> Result<()> {
let normalized_version = Config::normalize_version(version);
let url = format!(
"{}/releases/{}/{}.sig",
self.registry_path, normalized_version, self.release_bundle_name
);
tracing::debug!("Attempting to download signature from URL: {}", url);
let mut request = self.client.get(&url);
if !self.token.is_empty() {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", self.token))?,
);
request = request.headers(headers);
}
let response = request.send().context("Failed to download signature")?;
if !response.status().is_success() {
anyhow::bail!("Signature not found: HTTP {}", response.status());
}
let content = response.bytes().context("Failed to read signature")?;
fs::write(output_path, content).context("Failed to save signature")?;
Ok(())
}
pub fn download_release_bundle(&self, version: &str, output_path: &Path) -> Result<()> {
let normalized_version = Config::normalize_version(version);
let url = format!(
"{}/releases/{}/{}",
self.registry_path, normalized_version, self.release_bundle_name
);
tracing::debug!("Attempting to download from URL: {}", url);
let mut request = self.client.get(&url);
if !self.token.is_empty() {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", self.token))?,
);
request = request.headers(headers);
}
let response = request
.send()
.context("Failed to download release bundle")?;
if !response.status().is_success() {
anyhow::bail!(
"Failed to download release bundle: HTTP {}",
response.status()
);
}
let content = response
.bytes()
.context("Failed to read response content")?;
fs::write(output_path, content).context("Failed to save release bundle")?;
Ok(())
}
}