//! `attachment-get [-o ]` — download a Forgejo //! attachment to a local file (or stdout with `-o -`). Accepts either //! the bare attachment UUID or any URL form that ends with //! `/attachments/` (as it appears in comment markdown). //! //! Default output path: `/tmp/forge-attachment-`. Prints the //! resolved path to stdout so callers can capture it //! (`path=$(hive-forge attachment-get )`). use std::io::Write as _; use std::path::PathBuf; use anyhow::{Result, bail}; use clap::Args as ClapArgs; use crate::client::Client; #[derive(ClapArgs)] pub struct Args { /// Attachment UUID or URL. Accepts a bare UUID (`abc-123-...`), /// a root-relative path (`/attachments/abc-123-...`), or a full /// URL (`http://localhost:3000/attachments/abc-123-...`). attachment: String, /// Output path. Defaults to `/tmp/forge-attachment-`. /// Pass `-` to write raw bytes to stdout (e.g. for piping to an /// image viewer). #[arg(short = 'o', long)] output: Option, } /// # Errors /// /// Returns an error if the attachment UUID cannot be extracted from /// the input, if the download fails (network, 4xx/5xx), or if the /// output path cannot be written. pub fn run(client: &Client, args: Args) -> Result<()> { let uuid = extract_uuid(&args.attachment)?; let url = client.attachment_url(&uuid); let bytes = client.get_bytes_raw(&url)?; match args.output.as_deref() { Some("-") => { std::io::stdout() .write_all(&bytes) .map_err(|e| anyhow::anyhow!("write stdout: {e}"))?; } Some(path) => { let p = PathBuf::from(path); std::fs::write(&p, &bytes) .map_err(|e| anyhow::anyhow!("write {}: {e}", p.display()))?; println!("{}", p.display()); } None => { let p = PathBuf::from(format!("/tmp/forge-attachment-{uuid}")); std::fs::write(&p, &bytes) .map_err(|e| anyhow::anyhow!("write {}: {e}", p.display()))?; println!("{}", p.display()); } } Ok(()) } /// Extract the bare UUID from whatever the caller passes. /// /// Accepted forms: /// - bare UUID: `abc-123-...` /// - root-relative path: `/attachments/abc-123-...` /// - full loopback URL: `http://localhost:3000/attachments/abc-123-...` /// - any URL ending with `/attachments/` /// /// # Errors /// /// Returns an error if the input contains no path segment after /// parsing (e.g. an empty string). fn extract_uuid(input: &str) -> Result { // If it contains a slash, treat everything after the last slash as // the UUID. This handles all URL/path forms. let uuid = if input.contains('/') { input .rsplit('/') .next() .filter(|s| !s.is_empty()) .ok_or_else(|| { anyhow::anyhow!("hive-forge attachment-get: cannot extract UUID from {input:?}") })? } else { input }; if uuid.is_empty() { bail!("hive-forge attachment-get: empty UUID in {input:?}"); } Ok(uuid.to_owned()) }