use std::io::Write;
use colored::Colorize;
use serde::Serialize;
use crate::output::formatting::{write_field, write_formatted, write_optional_field};
use crate::types::{Attachment, OutputFormat};
pub fn write_attachments<W: Write + ?Sized>(
attachments: &[Attachment],
format: OutputFormat,
out: &mut W,
) {
write_formatted(attachments, format, out, |attachments, out| {
if attachments.is_empty() {
let _ = writeln!(out, "No attachments.");
return;
}
for a in attachments {
let patch = if a.is_patch { " [PATCH]" } else { "" };
let obsolete = if a.is_obsolete { " [OBSOLETE]" } else { "" };
let private = if a.is_private { " [PRIVATE]" } else { "" };
let _ = writeln!(
out,
"{} #{} - {}{}{}{}",
"Attachment".bold(),
a.id,
a.summary.bold(),
patch.cyan(),
obsolete.red(),
private.red(),
);
write_field(
out,
"File",
&format!("{} ({}, {} bytes)", a.file_name, a.content_type, a.size),
);
write_optional_field(out, "Creator", a.creator.as_deref());
write_optional_field(out, "Created", a.creation_time.as_deref());
let _ = writeln!(out);
}
});
}
#[derive(Debug, Serialize)]
#[non_exhaustive]
pub struct AttachmentBatchResult {
pub out_dir: String,
pub bug_results: Vec<BugDownloadResult>,
pub attachment_results: Vec<AttachmentDownloadResult>,
pub summary: BatchSummary,
}
#[derive(Debug, Serialize)]
#[non_exhaustive]
pub struct BugDownloadResult {
pub bug_id: u64,
pub status: TargetStatus,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub files: Vec<DownloadedFile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
#[non_exhaustive]
pub struct AttachmentDownloadResult {
pub attachment_id: u64,
pub status: TargetStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub bug_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bytes: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
#[non_exhaustive]
pub struct DownloadedFile {
pub attachment_id: u64,
pub path: String,
pub bytes: usize,
}
#[derive(Debug, Serialize)]
#[non_exhaustive]
pub struct BatchSummary {
pub succeeded: usize,
pub failed: usize,
pub total_bytes: usize,
}
impl BatchSummary {
pub fn from_results(
bug_results: &[BugDownloadResult],
attachment_results: &[AttachmentDownloadResult],
) -> Self {
let succeeded = bug_results.iter().map(|b| b.files.len()).sum::<usize>()
+ attachment_results
.iter()
.filter(|a| a.status == TargetStatus::Ok)
.count();
let failed = bug_results
.iter()
.filter(|b| b.status == TargetStatus::Error)
.count()
+ attachment_results
.iter()
.filter(|a| a.status == TargetStatus::Error)
.count();
let total_bytes = bug_results
.iter()
.flat_map(|b| &b.files)
.map(|f| f.bytes)
.sum::<usize>()
+ attachment_results
.iter()
.filter_map(|a| a.bytes)
.sum::<usize>();
Self {
succeeded,
failed,
total_bytes,
}
}
}
#[derive(Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
pub enum TargetStatus {
Ok,
Error,
}
pub fn write_attachment_batch<O, E>(
result: &AttachmentBatchResult,
format: OutputFormat,
out: &mut O,
err: &mut E,
) where
O: Write + ?Sized,
E: Write + ?Sized,
{
match format {
OutputFormat::Json => crate::output::formatting::write_json(result, out),
OutputFormat::Table => write_attachment_batch_table(result, out, err),
}
}
pub(super) fn write_attachment_batch_table<O: Write + ?Sized, E: Write + ?Sized>(
r: &AttachmentBatchResult,
out: &mut O,
err: &mut E,
) {
for bug in &r.bug_results {
match bug.status {
TargetStatus::Ok => {
let _ = writeln!(
out,
"{} {}: {} files saved",
"Bug".bold(),
format!("#{}", bug.bug_id).bold(),
bug.files.len(),
);
for file in &bug.files {
let _ = writeln!(out, " → {} ({} bytes)", file.path, file.bytes);
}
}
TargetStatus::Error => {
let _ = writeln!(
err,
"Bug #{}: {}",
bug.bug_id,
bug.error.as_deref().unwrap_or("error"),
);
for file in &bug.files {
let _ = writeln!(out, " → {} ({} bytes) [partial]", file.path, file.bytes);
}
}
}
}
for att in &r.attachment_results {
match att.status {
TargetStatus::Ok => {
let _ = writeln!(
out,
"{} #{}: {} ({} bytes)",
"Attachment".bold(),
att.attachment_id,
att.path.as_deref().unwrap_or("?"),
att.bytes.unwrap_or(0),
);
}
TargetStatus::Error => {
let _ = writeln!(
err,
"Attachment #{}: {}",
att.attachment_id,
att.error.as_deref().unwrap_or("error"),
);
}
}
}
let _ = writeln!(
out,
"{} {} succeeded, {} failed, {} total bytes",
"Summary:".bold(),
r.summary.succeeded,
r.summary.failed,
r.summary.total_bytes,
);
}
#[cfg(test)]
#[path = "attachment_tests.rs"]
mod tests;