1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Conserve backup system.
// Copyright 2018-2023 Martin Pool.

// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

//! Text output formats for structured data.
//!
//! These are objects that accept iterators of different types of content, and write it to a
//! file (typically stdout).

use std::borrow::Cow;
use std::io::{BufWriter, Write};
use std::sync::Arc;

use time::format_description::well_known::Rfc3339;
use time::UtcOffset;
use tracing::error;

use crate::misc::duration_to_hms;
use crate::termui::TermUiMonitor;
use crate::*;

/// Options controlling the behavior of `show_versions`.
#[derive(Default, Clone, Eq, PartialEq)]
pub struct ShowVersionsOptions {
    /// Show versions in LIFO order by band_id.
    pub newest_first: bool,
    /// Show the total size of files in the tree.  This is
    /// slower because it requires walking the whole index.
    pub tree_size: bool,
    /// Show the date and time that each backup started.
    pub start_time: bool,
    /// Show how much time the backup took, or "incomplete" if it never finished.
    pub backup_duration: bool,
    /// Show times in this zone.
    pub timezone: Option<UtcOffset>,
}

/// Print a list of versions, one per line, on stdout.
pub fn show_versions(
    archive: &Archive,
    options: &ShowVersionsOptions,
    monitor: Arc<TermUiMonitor>,
) -> Result<()> {
    let mut band_ids = archive.list_band_ids()?;
    if options.newest_first {
        band_ids.reverse();
    }
    for band_id in band_ids {
        if !(options.tree_size || options.start_time || options.backup_duration) {
            println!("{}", band_id);
            continue;
        }
        let mut l: Vec<String> = Vec::new();
        l.push(format!("{band_id:<20}"));
        let band = match Band::open(archive, band_id) {
            Ok(band) => band,
            Err(err) => {
                error!("Failed to open band {band_id:?}: {err}");
                continue;
            }
        };
        let info = match band.get_info() {
            Ok(info) => info,
            Err(err) => {
                error!("Failed to read band tail {band_id:?}: {err}");
                continue;
            }
        };

        if options.start_time {
            let mut start_time = info.start_time;
            if let Some(timezone) = options.timezone {
                start_time = start_time.to_offset(timezone);
            }
            l.push(format!(
                "{date:<25}", // "yyyy-mm-ddThh:mm:ss+oooo" => 25
                date = start_time.format(&Rfc3339).unwrap(),
            ));
        }

        if options.backup_duration {
            let duration_str: Cow<str> = if info.is_closed {
                if let Some(end_time) = info.end_time {
                    let duration = end_time - info.start_time;
                    if let Ok(duration) = duration.try_into() {
                        duration_to_hms(duration).into()
                    } else {
                        Cow::Borrowed("negative")
                    }
                } else {
                    Cow::Borrowed("unknown")
                }
            } else {
                Cow::Borrowed("incomplete")
            };
            l.push(format!("{duration_str:>10}"));
        }

        if options.tree_size {
            let tree_mb_str = crate::misc::bytes_to_human_mb(
                archive
                    .open_stored_tree(BandSelectionPolicy::Specified(band_id))?
                    .size(Exclude::nothing(), monitor.clone())?
                    .file_bytes,
            );
            l.push(format!("{tree_mb_str:>14}",));
        }
        monitor.clear_progress_bars(); // to avoid fighting with stdout
        println!("{}", l.join(" "));
    }
    Ok(())
}

pub fn show_index_json(band: &Band, w: &mut dyn Write) -> Result<()> {
    // TODO: Maybe use https://docs.serde.rs/serde/ser/trait.Serializer.html#method.collect_seq.
    let bw = BufWriter::new(w);
    let index_entries: Vec<IndexEntry> = band.index().iter_entries().collect();
    serde_json::ser::to_writer_pretty(bw, &index_entries)
        .map_err(|source| Error::SerializeJson { source })
}

pub fn show_entry_names<E: EntryTrait, I: Iterator<Item = E>>(
    it: I,
    w: &mut dyn Write,
    long_listing: bool,
) -> Result<()> {
    let mut bw = BufWriter::new(w);
    for entry in it {
        if long_listing {
            writeln!(
                bw,
                "{} {} {}",
                entry.unix_mode(),
                entry.owner(),
                entry.apath()
            )?;
        } else {
            writeln!(bw, "{}", entry.apath())?;
        }
    }
    Ok(())
}