1extern crate chrono;
2extern crate clap;
3extern crate flate2;
4extern crate two_timer;
5
6use crate::configure::Configuration;
7use crate::log::LogController;
8use crate::util::remainder;
9use crate::util::{base_dir, fatal, log_path, success, warn, yes_or_no};
10use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
11use flate2::write::GzEncoder;
12use flate2::Compression;
13use std::fs::File;
14use std::io::{BufReader, BufWriter, Read, Write};
15use two_timer::{parsable, parse};
16
17const BUFFER_SIZE: usize = 16 * 1024;
18
19fn after_help() -> &'static str {
20 "\
21Over time your log will fill with cruft: work no one is interested in any longer, \
22tags whose meaning you've forgotten. What you want to do at this point is chop off \
23all the old stuff, stash it somewhere you can find it if need be, and \
24retain in your active log only the more recent events. This is what truncate is for. \
25You give it a starting date and it splits your log into two with the active portion \
26containing all moments on that date or after. The older portion is \
27retained in the hidden directory.
28
29All prefixes of 'truncate' excepting 't' are aliases of the subcommand. The 't' alias belongs \
30to the tag subcommand."
31}
32
33pub fn cli(mast: App<'static, 'static>, display_order: usize) -> App<'static, 'static> {
34 mast.subcommand(
35 SubCommand::with_name("truncate")
36 .aliases(&["tr", "tru", "trun", "trunc", "trunca", "truncat"])
37 .about("Truncates the log so it only contains recent events")
38 .after_help(after_help())
39 .arg(
40 Arg::with_name("gzip")
41 .short("g")
42 .long("gzip")
43 .help("Compresses truncated head of log with gzip")
44 .long_help("To conserve space, compress the truncated head of the log with Gzip.")
45 )
46 .setting(AppSettings::TrailingVarArg)
47 .arg(
48 Arg::with_name("date")
49 .help("earliest time to preserve in log")
50 .long_help(
51 "All the <date> arguments are concatenated to produce the cutoff date. Events earlier than this moment will be preserved in the truncated head of the log. Events on or after this date will remain in the active log.",
52 )
53 .value_name("date")
54 .required(true)
55 .multiple(true)
56 )
57 .display_order(display_order)
58 )
59}
60
61pub fn run(directory: Option<&str>, matches: &ArgMatches) {
62 let time_expression = remainder("date", matches);
63 let conf = Configuration::read(None, directory);
64 if parsable(&time_expression) {
65 let (t, _, _) = parse(&time_expression, conf.two_timer_config()).unwrap();
66 let mut log = LogController::new(None, &conf).expect("could not read the log file");
67 if let Some(item) = log.find_line(&t) {
68 let filename = format!("log.head-to-{}", t);
69 let mut filename = filename.as_str().replace(" ", "_").to_owned();
70 if matches.is_present("gzip") {
71 filename += ".gz";
72 }
73 let mut path = base_dir(conf.directory());
74 path.push(&filename);
75 if path.as_path().exists() {
76 let overwrite = yes_or_no(format!(
77 "file {} already exists; overwrite?",
78 path.to_str().unwrap()
79 ));
80 if !overwrite {
81 fatal("could not truncate log", &conf);
82 }
83 }
84 if temp_log_path(conf.directory()).as_path().exists() {
85 let overwrite = yes_or_no(format!(
86 "the temporary log file {} already exists; overwrite?",
87 temp_log_path(conf.directory()).to_str().unwrap()
88 ));
89 if !overwrite {
90 fatal("could not truncate log", &conf);
91 }
92 }
93 let offset = log.larry.offset(item.offset()).unwrap() as usize;
94 let mut bytes_read = 0;
95 let original_file =
96 File::open(log_path(conf.directory())).expect("cannot open log file for reading");
97 let mut reader = BufReader::new(original_file);
98 let head_file =
99 File::create(path).expect(&format!("could not open {} for writing", filename));
100 let mut head_writer = BufWriter::new(head_file);
101 if matches.is_present("gzip") {
102 let mut encoder = GzEncoder::new(head_writer, Compression::best());
103 while bytes_read < offset {
104 let delta = offset - bytes_read;
105 let mut buffer: Vec<u8> = if delta < BUFFER_SIZE {
106 vec![0; delta]
107 } else {
108 vec![0; BUFFER_SIZE]
109 };
110 reader
111 .read_exact(&mut buffer)
112 .expect("failed to read data from log");
113 encoder
114 .write_all(&buffer)
115 .expect("failed to write data to head file");
116 bytes_read += buffer.capacity();
117 buffer.clear();
118 }
119 encoder
120 .finish()
121 .expect("failed to complete compression of head file");
122 } else {
123 while bytes_read < offset {
124 let delta = offset - bytes_read;
125 let mut buffer: Vec<u8> = if delta < BUFFER_SIZE {
126 vec![0; delta]
127 } else {
128 vec![0; BUFFER_SIZE]
129 };
130 reader
131 .read_exact(&mut buffer)
132 .expect("failed to read data from log");
133 head_writer
134 .write_all(&buffer)
135 .expect("failed to write data to head file");
136 bytes_read += buffer.len();
137 }
138 head_writer.flush().expect("failed to close head file");
139 }
140 let tail_file = File::create(temp_log_path(conf.directory()))
141 .expect("could not open log.tmp for writing");
142 let mut tail_writer = BufWriter::new(tail_file);
143 loop {
144 let mut buffer: Vec<u8> = vec![0; BUFFER_SIZE];
145 let bytes_read = reader.read(&mut buffer).expect("failed to read from log");
146 if bytes_read == 0 {
147 tail_writer.flush().expect("failed to close log.tmp");
148 break;
149 }
150 tail_writer
151 .write_all(&buffer)
152 .expect("failed to write to log.tmp");
153 }
154 std::fs::rename(
155 &temp_log_path(conf.directory()),
156 &log_path(conf.directory()),
157 )
158 .expect("failed to copy new log file into place");
159 success(
160 format!("saved truncated portion of log to {}", filename),
161 &conf,
162 );
163 } else {
164 warn(
165 format!(
166 "could not find anything in log on or after '{}'; not truncating",
167 time_expression
168 ),
169 &conf,
170 );
171 }
172 } else {
173 fatal(
174 format!("cannot parse '{}' as a time expression", time_expression),
175 &conf,
176 );
177 }
178}
179
180fn temp_log_path(directory: Option<&str>) -> std::path::PathBuf {
181 let mut path = base_dir(directory);
182 path.push("log.tmp");
183 path
184}