Spaces:
Build error
Build error
| //! 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, | |
| } | |
| } | |
| 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) | |
| } | |
| 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(()) | |
| } | |