free_launch/
invocation.rs1use chrono::{DateTime, Local};
2use color_eyre::Result;
3use csv::WriterBuilder;
4use serde::{Deserialize, Serialize};
5use std::{
6 cmp::Reverse,
7 fs::{self, DirEntry, File, OpenOptions},
8 path::{Path, PathBuf},
9 sync::mpsc::Sender,
10 time::SystemTime,
11};
12use tracing::warn;
13
14use crate::{
15 free_launch::PROJECT_DIRS, item_update::ItemUpdate, launch_entry::LaunchEntry, request::Request,
16};
17
18const MIN_INVOCATION_NUM: usize = 1000;
20const MAX_INVOCATION_RETENTION: usize = 60;
22const INVOCATION_FILE_PREFIX: &str = "free-launch-invocations-";
26const INVOCATION_FILE_SUFFIX: &str = ".csv";
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Invocation {
31 pub timestamp: u64,
32 pub search_query: String,
33 pub action: String,
34 pub item_name: String,
35 pub item_id: String,
36 pub item_path: String,
37}
38
39impl Invocation {
40 pub fn new(search_query: &str, action: String, launch_entry: &LaunchEntry) -> Self {
41 let now: SystemTime = SystemTime::now();
44 let timestamp = now
45 .duration_since(SystemTime::UNIX_EPOCH)
46 .unwrap_or_default()
47 .as_secs();
48
49 Self {
50 timestamp,
51 search_query: search_query.to_owned(),
52 action,
53 item_name: launch_entry.name().to_owned(),
54 item_id: launch_entry.id().map(|i| i.to_owned()).unwrap_or_default(),
55 item_path: launch_entry
56 .file_path()
57 .map(|i| i.to_string_lossy().to_string())
58 .unwrap_or_default(),
59 }
60 }
61
62 pub(crate) fn id(&self) -> &str {
63 &self.item_id
64 }
65
66 pub(crate) fn name(&self) -> &str {
67 &self.item_name
68 }
69
70 fn cache_file(date: DateTime<Local>) -> PathBuf {
71 let date_str = date.format("%Y-%m-%d").to_string();
73 let log_filename = format!(
74 "{INVOCATION_FILE_PREFIX}{}{INVOCATION_FILE_SUFFIX}",
75 date_str
76 );
77 PROJECT_DIRS.cache_dir().join(log_filename)
78 }
80
81 pub(crate) fn log_entry(
82 search_query: &str,
83 action: String,
84 launch_entry: &LaunchEntry,
85 ) -> Result<()> {
86 Invocation::new(search_query, action, launch_entry)
87 .append_to_csv(&Self::cache_file(Local::now()))
88 }
89
90 fn append_to_csv(&self, file_path: &Path) -> Result<()> {
91 let write_headers = !file_path.exists();
93
94 let file = OpenOptions::new()
97 .append(true)
98 .create(true)
99 .open(file_path)?;
100
101 let mut wtr = WriterBuilder::new()
104 .has_headers(write_headers)
105 .from_writer(file);
106
107 wtr.serialize(self)?;
109
110 wtr.flush()?;
112 Ok(())
113 }
114
115 fn cache_files() -> Result<impl Iterator<Item = DirEntry>> {
116 let cache_files = PROJECT_DIRS.cache_dir();
117 Ok(fs::read_dir(cache_files)?
118 .filter_map(Result::ok)
119 .filter(|entry| match entry.metadata() {
120 Ok(metadata) => {
121 metadata.is_file()
122 && entry
123 .file_name()
124 .to_string_lossy()
125 .starts_with(INVOCATION_FILE_PREFIX)
126 }
127 Err(_) => false,
128 }))
129 }
130
131 pub(crate) fn load_from_cache(request_sender: Sender<Box<Request>>) -> Result<usize> {
132 let mut cache_files = Self::cache_files()?.collect::<Vec<DirEntry>>();
134
135 cache_files.sort_by_cached_key(|i| Reverse(i.file_name()));
137 let mut cache_file_iter = cache_files.into_iter();
138
139 let cache_files_to_load = cache_file_iter.by_ref().take(MAX_INVOCATION_RETENTION);
141
142 let mut invocation_count = 0;
143
144 for cache_file in cache_files_to_load {
145 let mut rdr = csv::Reader::from_reader(File::open(cache_file.path())?);
147
148 for result in rdr.deserialize() {
150 let record: Invocation = result?;
152
153 if !record.item_id.is_empty() {
157 match request_sender.send(Box::new(Request::IndexItem {
158 item: ItemUpdate::Invocation(record),
159 })) {
160 Ok(_) => invocation_count += 1,
161 Err(e) => warn!("could not send invocation: {}", e),
162 }
163 }
164 }
165
166 if invocation_count >= MIN_INVOCATION_NUM {
169 break;
170 }
171 }
172
173 for file_to_delete in cache_file_iter {
175 if let Err(e) = fs::remove_file(file_to_delete.path()) {
176 warn!(
177 "Could not remove cache file {}: {}",
178 file_to_delete.path().to_string_lossy(),
179 e
180 )
181 }
182 }
183
184 drop(request_sender);
185
186 Ok(invocation_count)
187 }
188}