jobrog/
truncate.rs

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}