kasl/commands/export.rs
1//! Data export command for external analysis and backup.
2//!
3//! Provides comprehensive data export functionality supporting multiple output formats and data types for external analysis, backup, and integration.
4//!
5//! ## Usage
6//!
7//! ```bash
8//! # Export tasks to CSV
9//! kasl export tasks --format csv
10//!
11//! # Export today's report to Excel
12//! kasl export report --format xlsx
13//!
14//! # Export with custom filename
15//! kasl export tasks --format json --output my_tasks.json
16//! ```
17
18use crate::{
19 libs::{
20 config::Config,
21 export::{ExportData, ExportFormat, Exporter},
22 formatter::parse_date,
23 messages::Message,
24 },
25 msg_info,
26};
27use anyhow::Result;
28use chrono::NaiveDate;
29use clap::Args;
30use std::path::PathBuf;
31
32/// Command-line arguments for the export command.
33///
34/// The export command provides flexible options for data extraction,
35/// supporting different formats, data types, and output destinations.
36#[derive(Debug, Args)]
37pub struct ExportArgs {
38 /// Type of data to export
39 ///
40 /// Specifies which category of information to include in the export:
41 /// Each data type provides different levels of detail and is suitable
42 /// for different analysis purposes.
43 #[arg(value_enum, default_value = "report")]
44 data: ExportData,
45
46 /// Output format for the exported data
47 ///
48 /// Controls the structure and format of the exported file:
49 /// Format selection affects both file structure and available features.
50 #[arg(short, long, value_enum, default_value = "csv")]
51 format: ExportFormat,
52
53 /// Custom output file path
54 ///
55 /// When specified, the export will be saved to this exact location.
56 /// If not provided, a default filename will be generated based on:
57 /// - Current timestamp for uniqueness
58 /// - Selected data type for clarity
59 /// - Chosen format for proper file extension
60 ///
61 /// Example default: `kasl_export_20250115_143022.csv`
62 #[arg(short, long)]
63 output: Option<PathBuf>,
64
65 /// Target date for data export
66 ///
67 /// Specifies which date's data to export. Accepts:
68 /// - `today`: Current date (default)
69 /// - `YYYY-MM-DD`: Specific date in ISO format
70 ///
71 /// For summary exports, this determines the month to summarize.
72 /// For daily reports and tasks, this specifies the exact date.
73 #[arg(short, long, default_value = "today")]
74 date: String,
75
76 /// Render the daily report as an hourly (SiServer-style) breakdown
77 ///
78 /// When enabled, the report is exported as a per-hour grid: each row
79 /// represents one hour of the workday with a description of the work
80 /// performed, and "Перерыв" is written for hours (or parts of hours) that
81 /// fall within a break or pause.
82 ///
83 /// This option only affects Excel report exports (`report --format excel`);
84 /// it is ignored for other data types and formats.
85 #[arg(long)]
86 hourly: bool,
87}
88
89/// Executes the export: parses the date, resolves the output path, and
90/// hands off to the [`Exporter`].
91///
92/// ```bash
93/// kasl export report --format csv
94/// kasl export tasks --format json --date 2025-01-15
95/// kasl export summary --format excel --output monthly_report.xlsx
96/// kasl export all --format json --output backup_2025_01.json
97/// ```
98pub async fn cmd(args: ExportArgs) -> Result<()> {
99 let date = parse_date(&args.date)?;
100
101 msg_info!(Message::ExportingData(format!("{:?}", args.data), format!("{:?}", args.format)));
102
103 // Resolve the output path: an explicit --output always wins; otherwise, for
104 // report exports, fall back to the configured directory and file-name template.
105 let output = match args.output.clone() {
106 Some(path) => Some(path),
107 None => resolve_report_output(args.data, args.format, date)?,
108 };
109
110 // Initialize exporter with format and output configuration
111 let exporter = Exporter::new(args.format, output).hourly(args.hourly);
112
113 // Delegate to appropriate export handler based on data type
114 exporter.export(args.data, date).await?;
115
116 Ok(())
117}
118
119/// Resolves a default output path for report exports from configuration.
120///
121/// This is only applied to [`ExportData::Report`] exports when no explicit
122/// `--output` was provided and a report output directory is configured. The
123/// file name is built from the configured template (defaulting to
124/// `daily_report_{date}{seq}`), where `{date}` is the report date and `{seq}`
125/// is a per-day sequence suffix (empty for the first file of the day, then
126/// `_2`, `_3`, … for subsequent files). The chosen path is guaranteed not to
127/// overwrite an existing file.
128///
129/// Returns `Ok(None)` to defer to the exporter's built-in default naming when
130/// the export is not a report or no report directory is configured.
131fn resolve_report_output(data: ExportData, format: ExportFormat, date: NaiveDate) -> Result<Option<PathBuf>> {
132 if !matches!(data, ExportData::Report) {
133 return Ok(None);
134 }
135
136 let report_config = match Config::read()?.report {
137 Some(config) => config,
138 None => return Ok(None),
139 };
140
141 let output_dir = match report_config.output_dir {
142 Some(dir) if !dir.trim().is_empty() => PathBuf::from(dir),
143 _ => return Ok(None),
144 };
145
146 std::fs::create_dir_all(&output_dir)?;
147
148 let template = report_config
149 .filename_template
150 .filter(|t| !t.trim().is_empty())
151 .unwrap_or_else(|| "daily_report_{date}{seq}".to_string());
152
153 let extension = match format {
154 ExportFormat::Csv => "csv",
155 ExportFormat::Json => "json",
156 ExportFormat::Excel => "xlsx",
157 };
158 let date_str = date.format("%Y-%m-%d").to_string();
159
160 // Pick the first non-existing file for the day, appending _2, _3, … as needed.
161 for sequence in 1.. {
162 let seq_suffix = if sequence == 1 { String::new() } else { format!("_{}", sequence) };
163 let stem = template.replace("{date}", &date_str).replace("{seq}", &seq_suffix);
164 let candidate = output_dir.join(format!("{}.{}", stem, extension));
165 if !candidate.exists() {
166 return Ok(Some(candidate));
167 }
168 }
169
170 unreachable!("sequence iterator is unbounded")
171}