use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs;
use std::path::PathBuf;
use super::utils::ConfluenceContext;
use crate::commands::common::{render_success, MutationResult};
pub async fn list_attachments(ctx: &ConfluenceContext<'_>, page_id: &str) -> Result<()> {
#[derive(Deserialize)]
struct AttachmentsResponse {
results: Vec<Attachment>,
}
#[derive(Deserialize)]
struct Attachment {
id: String,
title: String,
#[serde(rename = "fileSize")]
file_size: i64,
#[serde(rename = "mediaType")]
media_type: String,
}
let response: AttachmentsResponse = ctx
.client
.get(&format!("/wiki/api/v2/pages/{}/attachments", page_id))
.await
.with_context(|| format!("Failed to list attachments for page {}", page_id))?;
#[derive(Serialize)]
struct Row<'a> {
id: &'a str,
title: &'a str,
file_size: i64,
media_type: &'a str,
}
let rows: Vec<Row<'_>> = response
.results
.iter()
.map(|a| Row {
id: a.id.as_str(),
title: a.title.as_str(),
file_size: a.file_size,
media_type: a.media_type.as_str(),
})
.collect();
ctx.renderer.render(&rows)
}
pub async fn get_attachment(ctx: &ConfluenceContext<'_>, attachment_id: &str) -> Result<()> {
let attachment: Value = ctx
.client
.get(&format!("/wiki/api/v2/attachments/{}", attachment_id))
.await
.with_context(|| format!("Failed to get attachment {}", attachment_id))?;
println!("{}", serde_json::to_string_pretty(&attachment)?);
Ok(())
}
pub async fn upload_attachment(
ctx: &ConfluenceContext<'_>,
page_id: &str,
file_path: &PathBuf,
comment: Option<&str>,
) -> Result<()> {
let file_content = fs::read(file_path)
.with_context(|| format!("Failed to read file: {}", file_path.display()))?;
let file_name = file_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("attachment");
let form = reqwest::multipart::Form::new()
.part(
"file",
reqwest::multipart::Part::bytes(file_content).file_name(file_name.to_string()),
)
.text("minorEdit", "true");
let form = if let Some(c) = comment {
form.text("comment", c.to_string())
} else {
form
};
let base_url = ctx.client.base_url();
let mut request = ctx
.client
.http_client()
.post(format!(
"{}/wiki/rest/api/content/{}/child/attachment",
base_url, page_id
))
.multipart(form)
.header("X-Atlassian-Token", "no-check");
request = ctx.client.apply_auth(request);
let response = request
.send()
.await
.with_context(|| format!("Failed to upload attachment to page {}", page_id))?;
if !response.status().is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(anyhow::anyhow!(
"Failed to upload attachment: {}",
error_text
));
}
tracing::info!(%page_id, file = %file_name, "Attachment uploaded successfully");
render_success(
ctx.renderer,
&format!("✅ Uploaded attachment '{file_name}' to page {page_id}"),
&MutationResult::with_id(
format!("Uploaded attachment '{file_name}' to page {page_id}"),
page_id,
),
)
}
pub async fn download_attachment(
ctx: &ConfluenceContext<'_>,
attachment_id: &str,
output: &PathBuf,
) -> Result<()> {
#[derive(Deserialize)]
struct AttachmentDetail {
#[serde(rename = "downloadLink")]
download_link: String,
title: String,
}
let attachment: AttachmentDetail = ctx
.client
.get(&format!("/wiki/api/v2/attachments/{}", attachment_id))
.await
.with_context(|| format!("Failed to get attachment {}", attachment_id))?;
let base_url = ctx.client.base_url();
let mut request = ctx
.client
.http_client()
.get(format!("{}{}", base_url, attachment.download_link));
request = ctx.client.apply_auth(request);
let response = request
.send()
.await
.context("Failed to download attachment")?;
if !response.status().is_success() {
return Err(anyhow::anyhow!("Failed to download attachment"));
}
let content = response
.bytes()
.await
.context("Failed to read attachment content")?;
fs::write(output, content)
.with_context(|| format!("Failed to write file: {}", output.display()))?;
tracing::info!(attachment_id = %attachment_id, file = %output.display(), "Attachment downloaded successfully");
println!(
"✅ Downloaded attachment '{}' to {}",
attachment.title,
output.display()
);
Ok(())
}
pub async fn delete_attachment(
ctx: &ConfluenceContext<'_>,
attachment_id: &str,
force: bool,
) -> Result<()> {
if !force {
println!(
"⚠️ This will permanently delete attachment {}. Use --force to confirm.",
attachment_id
);
return Ok(());
}
let _: Value = ctx
.client
.delete(&format!("/wiki/api/v2/attachments/{}", attachment_id))
.await
.with_context(|| format!("Failed to delete attachment {}", attachment_id))?;
tracing::info!(%attachment_id, "Attachment deleted successfully");
render_success(
ctx.renderer,
&format!("✅ Deleted attachment: {attachment_id}"),
&MutationResult::with_id(
format!("Deleted attachment: {attachment_id}"),
attachment_id,
),
)
}