feat: add Peer Files UI for browsing and downloading federated content
- New PeerFiles.vue view shows federated peers and their shared catalogs - Peer Files card in Cloud.vue shows when federation peers exist - New content.download-peer RPC fetches content from peer via Tor - Route: /dashboard/cloud/peers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -139,6 +139,71 @@ impl RpcHandler {
|
||||
Ok(serde_json::json!({ "updated": true }))
|
||||
}
|
||||
|
||||
/// Download content from a peer over Tor, returning base64-encoded data.
|
||||
pub(super) async fn handle_content_download_peer(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let onion = params
|
||||
.get("onion")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing onion address"))?;
|
||||
let content_id = params
|
||||
.get("content_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing content_id"))?;
|
||||
|
||||
if !onion.ends_with(".onion") || onion.len() < 10 {
|
||||
return Err(anyhow::anyhow!("Invalid onion address"));
|
||||
}
|
||||
|
||||
let socks_proxy = reqwest::Proxy::all("socks5h://127.0.0.1:9050")
|
||||
.context("Failed to create SOCKS proxy")?;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.proxy(socks_proxy)
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.context("Failed to build Tor HTTP client")?;
|
||||
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
let local_did = crate::identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
|
||||
|
||||
let url = format!("http://{}/content/{}", onion, content_id);
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("X-Federation-DID", &local_did)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to connect to peer over Tor")?;
|
||||
|
||||
if response.status() == reqwest::StatusCode::PAYMENT_REQUIRED {
|
||||
let body: serde_json::Value = response.json().await.unwrap_or_default();
|
||||
return Ok(serde_json::json!({
|
||||
"error": "payment_required",
|
||||
"price_sats": body.get("price_sats").and_then(|v| v.as_u64()).unwrap_or(0),
|
||||
}));
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow::anyhow!("Peer returned: {}", response.status()));
|
||||
}
|
||||
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.context("Failed to read response body")?;
|
||||
|
||||
use base64::Engine;
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"data": encoded,
|
||||
"size": bytes.len(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Browse a peer's content catalog over Tor.
|
||||
pub(super) async fn handle_content_browse_peer(
|
||||
&self,
|
||||
|
||||
@@ -417,6 +417,7 @@ impl RpcHandler {
|
||||
"content.set-pricing" => self.handle_content_set_pricing(params).await,
|
||||
"content.set-availability" => self.handle_content_set_availability(params).await,
|
||||
"content.browse-peer" => self.handle_content_browse_peer(params).await,
|
||||
"content.download-peer" => self.handle_content_download_peer(params).await,
|
||||
|
||||
// DWN (Decentralized Web Node)
|
||||
"dwn.status" => self.handle_dwn_status().await,
|
||||
|
||||
Reference in New Issue
Block a user