Spaces:
Build error
Build error
File size: 4,609 Bytes
1295969 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | //! 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(())
}
|