mike dupont
init: retro-sync API server + viewer + 71 Bach tiles + catalog
1295969
//! BTFS upload module β€” multipart POST to BTFS daemon /api/v0/add.
//!
//! SECURITY:
//! - Set BTFS_API_KEY env var to authenticate to your BTFS node.
//! Every request carries `X-API-Key: {BTFS_API_KEY}` header.
//! - Set BTFS_API_URL to a private internal URL; never expose port 5001 publicly.
//! - The pin() function now propagates errors β€” a failed pin is treated as
//! a data loss condition and must be investigated.
use shared::types::{BtfsCid, Isrc};
use tracing::{debug, info, instrument};
/// Build a reqwest client with a 120-second timeout and the BTFS API key header.
///
/// TLS enforcement: in production (`RETROSYNC_ENV=production`), BTFS_API_URL
/// must use HTTPS. Configure a TLS-terminating reverse proxy (nginx/HAProxy)
/// in front of the BTFS daemon (which only speaks HTTP natively).
/// Example nginx config:
/// server {
/// listen 443 ssl;
/// location / { proxy_pass http://127.0.0.1:5001; }
/// }
fn btfs_client() -> anyhow::Result<(reqwest::Client, Option<String>)> {
let api = std::env::var("BTFS_API_URL").unwrap_or_else(|_| "http://127.0.0.1:5001".into());
let env = std::env::var("RETROSYNC_ENV").unwrap_or_default();
if env == "production" && !api.starts_with("https://") {
anyhow::bail!(
"SECURITY: BTFS_API_URL must use HTTPS in production (got: {api}). \
Configure a TLS reverse proxy in front of the BTFS node. \
See: https://docs.btfs.io/docs/tls-setup"
);
}
if !api.starts_with("https://") {
tracing::warn!(
url=%api,
"BTFS_API_URL uses plaintext HTTP β€” traffic is unencrypted. \
Configure HTTPS for production (set BTFS_API_URL=https://...)."
);
}
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(120))
.build()?;
let api_key = std::env::var("BTFS_API_KEY").ok();
Ok((client, api_key))
}
/// Attach BTFS API key to a request builder if BTFS_API_KEY is set.
fn with_api_key(
builder: reqwest::RequestBuilder,
api_key: Option<&str>,
) -> reqwest::RequestBuilder {
match api_key {
Some(key) => builder.header("X-API-Key", key),
None => builder,
}
}
#[instrument(skip(audio_bytes), fields(bytes = audio_bytes.len()))]
pub async fn upload(audio_bytes: &[u8], title: &str, isrc: &Isrc) -> anyhow::Result<BtfsCid> {
let api = std::env::var("BTFS_API_URL").unwrap_or_else(|_| "http://127.0.0.1:5001".into());
let url = format!("{api}/api/v0/add");
let filename = format!("{}.bin", isrc.0.replace('/', "-"));
let (client, api_key) = btfs_client()?;
let part = reqwest::multipart::Part::bytes(audio_bytes.to_vec())
.file_name(filename)
.mime_str("application/octet-stream")?;
let form = reqwest::multipart::Form::new().part("file", part);
debug!(url=%url, has_api_key=%api_key.is_some(), "Uploading to BTFS");
let req = with_api_key(client.post(&url), api_key.as_deref()).multipart(form);
let resp = req
.send()
.await
.map_err(|e| anyhow::anyhow!("BTFS unreachable at {url}: {e}"))?;
if !resp.status().is_success() {
anyhow::bail!("BTFS /api/v0/add failed: {}", resp.status());
}
let body = resp.text().await?;
let cid_str = body
.lines()
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.filter_map(|v| v["Hash"].as_str().map(|s| s.to_string()))
.next_back()
.ok_or_else(|| anyhow::anyhow!("BTFS returned no CID"))?;
let cid = shared::parsers::recognize_btfs_cid(&cid_str)
.map_err(|e| anyhow::anyhow!("BTFS invalid CID: {e}"))?;
info!(isrc=%isrc, cid=%cid.0, "Uploaded to BTFS");
Ok(cid)
}
#[allow(dead_code)]
pub async fn pin(cid: &BtfsCid) -> anyhow::Result<()> {
// SECURITY: Pin errors propagated β€” a failed pin means content is not
// guaranteed to persist on the BTFS network. Do not silently ignore.
let api = std::env::var("BTFS_API_URL").unwrap_or_else(|_| "http://127.0.0.1:5001".into());
let url = format!("{}/api/v0/pin/add?arg={}", api, cid.0);
let (client, api_key) = btfs_client()?;
let req = with_api_key(client.post(&url), api_key.as_deref());
let resp = req
.send()
.await
.map_err(|e| anyhow::anyhow!("BTFS pin request failed for CID {}: {}", cid.0, e))?;
if !resp.status().is_success() {
anyhow::bail!("BTFS pin failed for CID {} β€” HTTP {}", cid.0, resp.status());
}
info!(cid=%cid.0, "BTFS content pinned successfully");
Ok(())
}