Full `nix flake check` (CI) runs the treefmt formatting derivation. While the hive-ci runner was offline (#1221), PRs merged without it, leaving 5 files unformatted: hive-ag3nt/src/web_ui.rs, hive-c0re/src/bin/hivectl.rs, hive-c0re/src/knowledge.rs, hive-c0re/src/matrix.rs, hive-forge/src/verbs/attachment_get.rs. `nix fmt` output, pure formatting.
93 lines
3.1 KiB
Rust
93 lines
3.1 KiB
Rust
//! `attachment-get <uuid-or-url> [-o <path>]` — 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/<uuid>` (as it appears in comment markdown).
|
|
//!
|
|
//! Default output path: `/tmp/forge-attachment-<uuid>`. Prints the
|
|
//! resolved path to stdout so callers can capture it
|
|
//! (`path=$(hive-forge attachment-get <uuid>)`).
|
|
|
|
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-<uuid>`.
|
|
/// Pass `-` to write raw bytes to stdout (e.g. for piping to an
|
|
/// image viewer).
|
|
#[arg(short = 'o', long)]
|
|
output: Option<String>,
|
|
}
|
|
|
|
/// # 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/<uuid>`
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the input contains no path segment after
|
|
/// parsing (e.g. an empty string).
|
|
fn extract_uuid(input: &str) -> Result<String> {
|
|
// 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())
|
|
}
|