1use std::{collections::HashMap, fs::File, io::BufWriter, marker::PhantomData};
2
3use crate::{
4 builders::LoggerBuilder,
5 endpoint::{Endpoint, EndpointHash},
6 EndpointSuper, LOGGER,
7};
8
9pub struct Logger {
10 pub(crate) fmt: Box<dyn std::any::Any + std::marker::Send + std::marker::Sync>,
11 pub(crate) endpoints: HashMap<EndpointHash, Endpoint>,
12}
13
14impl Logger {
15 pub(crate) fn log(&mut self, target: EndpointHash, message: String) {
16 let endpoint = self
17 .endpoints
18 .get_mut(&target)
19 .expect("Attempted to use un-initialized endpoint");
20
21 if endpoint.disabled {
22 return;
23 }
24
25 if !endpoint.silent {
26 println!("{}", message);
27 }
28
29 if let Some(file) = &mut endpoint.file {
30 use std::io::Write;
31 writeln!(file, "{}", message).unwrap();
32 }
33 }
34}
35
36pub struct KeepAlive(pub(crate) PhantomData<()>);
38
39impl Drop for KeepAlive {
40 fn drop(&mut self) {
41 let mut guard = LOGGER.lock().unwrap();
42 let mut logger = std::mem::take(&mut *guard).unwrap();
43 for endpoint in logger.endpoints.values_mut() {
44 if let Some(file) = &mut endpoint.file {
45 use std::io::Write;
46 file.flush().unwrap();
47 }
48 }
49 }
50}
51
52pub(crate) fn setup_logger<EP: EndpointSuper>(builder: LoggerBuilder<EP>) {
53 if let Some(path) = &builder.base_path {
54 std::fs::create_dir_all(path).unwrap();
55 }
56
57 let mut existing = Vec::new();
58
59 for ep_builder in builder.endpoints.values() {
60 if let Some(path) = &ep_builder.path {
61 let path = builder.base_path.as_ref().unwrap().join(path);
62 if path.exists() {
63 existing.push(path);
64 }
65 }
66 }
67
68 if !existing.is_empty() {
69 #[cfg(feature = "archive")]
70 let mut archives = HashMap::new();
71
72 for path in existing {
73 #[cfg(feature = "archive")]
74 {
75 use std::io::{BufRead, BufReader};
76
77 let mut file = BufReader::new(File::open(&path).unwrap());
78 let mut header = String::new();
79 let date = match file.read_line(&mut header) {
80 Ok(_) if header.trim_start().starts_with('%') && header.trim_end().ends_with('%') => {
81 chrono::DateTime::parse_from_rfc2822(header.trim().trim_start_matches('%').trim_end_matches('%').trim()).expect("Unable to parse `time` field in old log header, aborting since we can't safely archive the target file.")
82 },
83 e => panic!("Failed to read log header, aborting since we can't safely archive the target file. {:?}", e)
84 };
85
86 let archive = archives.entry(date).or_insert_with(|| {
87 zip::ZipWriter::new(
88 File::create(
89 builder
90 .base_path
91 .as_ref()
92 .unwrap()
93 .join(format!("{}.zip", date)),
94 )
95 .unwrap(),
96 )
97 });
98
99 archive
100 .start_file(
101 path.file_name().unwrap().to_str().unwrap(),
102 zip::write::FileOptions::default(),
103 )
104 .unwrap();
105 std::io::copy(&mut file, archive).unwrap();
106 }
107
108 #[cfg(not(feature = "archive"))]
109 {
110 let mut new_path = path.clone();
111 while new_path.exists() {
112 new_path.set_extension(
113 new_path
114 .extension()
115 .unwrap_or_default()
116 .to_str()
117 .unwrap()
118 .to_owned()
119 + ".bk",
120 );
121 }
122
123 std::fs::rename(path, new_path).unwrap();
124 }
125 }
126
127 #[cfg(feature = "archive")]
128 for (_, mut archive) in archives {
129 archive.finish().unwrap();
130 }
131 }
132
133 let creation_date = chrono::Utc::now().to_rfc2822();
134 let mut endpoints = HashMap::new();
135 for (hash, ep_builder) in builder.endpoints {
136 let file = ep_builder.path.map(|path| {
137 use std::io::Write;
138
139 let mut file = BufWriter::new(
140 File::create(builder.base_path.as_ref().unwrap().join(path))
141 .expect("Unable to create log output file."),
142 );
143 writeln!(&mut file, "% {creation_date} %").unwrap();
144 file.flush().unwrap();
145 file
146 });
147
148 endpoints.insert(
149 hash,
150 Endpoint {
151 file,
152 silent: ep_builder.silent,
153 disabled: ep_builder.disabled,
154 },
155 );
156 }
157
158 let mut logger = LOGGER.lock().unwrap();
159 if logger
160 .replace(Logger {
161 fmt: Box::new(builder.fmt),
162 endpoints,
163 })
164 .is_some()
165 {
166 panic!("Logger was already initialized!");
167 }
168}