loggit/logger/file_handler/
file_manager.rs

1use std::{
2    fs::File,
3    io::{self, BufReader},
4};
5
6use chrono::Timelike;
7use thiserror::Error;
8use zip::{result::ZipError, write::SimpleFileOptions, CompressionMethod, ZipWriter};
9
10use crate::{
11    helper::{self, WriteToFileError},
12    logger::archivation,
13    Config,
14};
15
16use super::{
17    file_formatter::{FileFormatter, FileFormatterTryFromStringError},
18    file_name::{FileName, FileNameFromFileFormatterError},
19};
20
21#[derive(Clone, Debug)]
22pub(crate) struct FileManager {
23    file_format: FileFormatter,
24    file_name: FileName,
25    file_constraints: FileConstraints,
26    curr_file: std::sync::Arc<std::fs::File>,
27}
28
29#[derive(Error, Debug)]
30pub enum FileManagerFromStringError {
31    #[error("string parsing for the file format error: {0}")]
32    FileFormatParsingError(FileFormatterTryFromStringError),
33    #[error("format parsing for the file name error: {0}")]
34    FileNameParsingError(FileNameFromFileFormatterError),
35    #[error("io error {0}")]
36    IoError(std::io::Error)
37}
38
39#[derive(Error, Debug)]
40pub(crate) enum CompressFileError {
41    #[error("error with verifying archivation folder {0}")]
42    UnableToCreateArchivationFolder(std::io::Error),
43    #[error("unable to create a zip file: {0}")]
44    UnableToCreateZipFile(std::io::Error),
45    #[error("unable to open file to compress: {0}")]
46    UnableToOpenFileToCompress(std::io::Error),
47    #[error("unable to start zip archiving: {0}")]
48    UnableToStartZipArchiving(ZipError),
49    #[error("unable to copy the contents of the file: {0}")]
50    UnableToCopyContents(std::io::Error),
51    #[error("unable to write to archive")]
52    UnableToWriteToArchive,
53    #[error("unable to finish archivation: {0}")]
54    UnableToFinishArchivation(ZipError),
55    #[error("unable to get compression settings")]
56    UnableToGetCompressionSettings,
57    #[error("inaccessible archivation directory: {0}")]
58    InaccessibleArchivationDirectory(std::io::Error),
59}
60
61#[derive(Error, Debug)]
62pub(crate) enum VerifyConstraintsError {
63    #[error("unable to verify file existence {0}")]
64    UnableToVerifyFileExistence(std::io::Error),
65    #[error("unable to create file {0} {1}")]
66    UnableToCreateFile(String, std::io::Error),
67    #[error("unable to open file: {0} {1}")]
68    UnableToOpenFile(String, std::io::Error),
69    #[error("unable to get file metadata: {0} {1}")]
70    UnableToGetFileMetadata(String, std::io::Error),
71    #[error("unable to delete old log files: {0} {1}")]
72    UnableToDeleteOldLogFile(String, std::io::Error),
73    #[error("uable to compress file")]
74    UnableToCompressFile,
75    #[error("unable to create a new file: {0}")]
76    UnableToCreateNewFile(CreateNewFileError),
77}
78pub(crate) enum VerifyConstraintsRes {
79    ConstraintsPassed,
80    NewFileCreated,
81}
82#[derive(Debug, Error)]
83pub(crate) enum WriteLogError {
84    #[error("unable to write to the file: {0}")]
85    UnableToWriteToFile(WriteToFileError),
86}
87#[derive(Debug, Error)]
88pub(crate) enum CreateNewFileError {
89    #[error("unable to verify that the file exists: {0}")]
90    UnableToVerifyFileExistence(std::io::Error),
91    #[error("IO error occured: {0}")]
92    UnableToCreateFileIO(std::io::Error),
93    #[error("unable to get the file name: {0}")]
94    UnableToGetFileName(FileNameFromFileFormatterError),
95}
96
97impl FileManager {
98    pub(crate) fn init_from_string(
99        format: &str,
100        config: Config,
101    ) -> Result<FileManager, FileManagerFromStringError> {
102        let f_format = match FileFormatter::try_from_string(format) {
103            Ok(f) => f,
104            Err(e) => {
105                return Err(FileManagerFromStringError::FileFormatParsingError(e));
106            }
107        };
108        let f_name = match FileName::from_file_formatter(f_format.clone(), config.level) {
109            Ok(f) => f,
110            Err(e) => {
111                return Err(FileManagerFromStringError::FileNameParsingError(e));
112            }
113        };
114        let full_file_name: String = f_name.clone().into();
115
116    let mut file = match std::fs::OpenOptions::new()
117        .append(true)
118        .create(true)
119        .open(full_file_name)
120    {
121        Ok(f) => f,
122        Err(e) => {
123            return Err(FileManagerFromStringError::IoError(e));
124        }
125    };
126
127        Ok(FileManager {
128            file_format: f_format,
129            file_name: f_name,
130            file_constraints: Default::default(),
131            curr_file: std::sync::Arc::new(file)
132        })
133    }
134    /// Returns full current file name (that already exists) in a String
135    pub(crate) fn get_file_name(&self) -> String {
136        self.file_name.get_full_file_name()
137    }
138    pub(crate) fn remove_rotations(&mut self) {
139        self.file_constraints.rotation = Vec::new();
140    }
141    pub(crate) fn add_rotation(&mut self, string: &str) -> bool {
142        let rot_type = match RotationType::try_from_string(string) {
143            Some(r) => r,
144            None => {
145                return false;
146            }
147        };
148        let rot = Rotation::init_from_rotation_type(rot_type);
149        self.file_constraints.rotation.push(rot);
150        true
151    }
152    pub(crate) fn set_compression(&mut self, string: &str) -> bool {
153        match CompressionType::try_from_string(string) {
154            Some(r) => {
155                self.file_constraints.compression = Some(r);
156                true
157            }
158            None => false,
159        }
160    }
161    fn set_curr_file(&mut self, curr_file: std::fs::File){
162        self.curr_file = std::sync::Arc::new(curr_file);
163    }
164    pub(crate) fn remove_compression(&mut self) {
165        self.file_constraints.compression = None;
166    }
167
168    pub(crate) fn create_new_file(&mut self, config: &Config) -> Result<(), CreateNewFileError> {
169        loop {
170            match std::path::Path::new(&self.file_name.get_full_file_name()).exists() {
171                false => {
172                    let new_f_name =
173                        match FileName::from_file_formatter(self.file_format.clone(), config.level)
174                        {
175                            Ok(r) => r,
176                            Err(e) => {
177                                return Err(CreateNewFileError::UnableToGetFileName(e));
178                            }
179                        };
180                    self.file_name = new_f_name;
181                    let f_name_str = self.file_name.get_full_file_name();
182                    let file = match std::fs::OpenOptions::new()
183                            .append(true)
184                            .create(true)
185                            .open(f_name_str) 
186                    {
187                        Ok(f) => f,
188                        Err(e) => {
189                            return Err(CreateNewFileError::UnableToCreateFileIO(e));
190                        }
191                    };
192                    self.set_curr_file(file);
193                    return Ok(())
194                }
195                true => {
196                    self.file_name.increase_num();
197                }
198            }
199        }
200    }
201
202    /// compresses a file by the given path in a zip archive
203    fn compress_zip(&self, path: &str) -> Result<(), CompressFileError> {
204        if let Err(e) = archivation::ensure_archive_dir() {
205            return Err(CompressFileError::UnableToCreateArchivationFolder(e));
206        }
207        let zip_file_path = archivation::archive_dir().join(format!("{}.zip", path));
208        let zip_file = std::fs::File::create(&zip_file_path)
209            .map_err(CompressFileError::UnableToCreateZipFile)?;
210        let mut zip = ZipWriter::new(zip_file);
211        let options = SimpleFileOptions::default().compression_method(CompressionMethod::DEFLATE);
212
213        let file =
214            std::fs::File::open(path).map_err(CompressFileError::UnableToOpenFileToCompress)?;
215        let mut reader = BufReader::new(file);
216
217        let entry_name = std::path::Path::new(path).file_name().unwrap_or_default().to_string_lossy();
218        zip.start_file(entry_name, options)
219            .map_err(CompressFileError::UnableToStartZipArchiving)?;
220        std::io::copy(&mut reader, &mut zip).map_err(CompressFileError::UnableToCopyContents)?;
221        zip.finish()
222            .map_err(CompressFileError::UnableToFinishArchivation)?;
223        Ok(())
224
225        //println!("Files compressed successfully to {:?}", zip_file_path);
226    }
227    /// Compresses a file by the given path depending on the set compression algortithm in the
228    /// config
229    pub(crate) fn compress_file(&self, path: &str) -> Result<(), CompressFileError> {
230        if let Err(e) = archivation::ensure_archive_dir() {
231            return Err(CompressFileError::InaccessibleArchivationDirectory(e));
232        }
233        if let Some(compr_t) = &self.file_constraints.compression {
234            match compr_t {
235                CompressionType::Zip => self.compress_zip(path),
236            }
237        } else {
238            Err(CompressFileError::UnableToGetCompressionSettings)
239        }
240    }
241    /// verifying file constraints (rotation time and file size) and if one of the constraints
242    /// doesn't pass, it creates new file (archives the changed file if it's set in the config)
243    pub(crate) fn verify_constraints(
244        &mut self,
245        config: &Config,
246    ) -> Result<VerifyConstraintsRes, VerifyConstraintsError> {
247        let curr_file_name = self.file_name.get_full_file_name();
248        let file = self.curr_file.clone();
249        let f_size = match file.metadata() {
250            Err(e) => {
251                return Err(VerifyConstraintsError::UnableToGetFileMetadata(
252                    curr_file_name.clone(),
253                    e,
254                ));
255            }
256            Ok(data) => data.len(),
257        };
258        let mut last_idx: i32 = -1;
259        // we need last_idx for: if we found not satsfying constraint, than we create a new file,
260        // thus we have to update all the constraints we had, to set the to the original values,
261        // as a consequence, we have last_idx, if it's not -1, than on last_idx rotation we created
262        // new file and update all the constraints to initial values
263        let mut idx: usize = 0;
264        let mut res: Result<VerifyConstraintsRes, VerifyConstraintsError> =
265            Ok(VerifyConstraintsRes::ConstraintsPassed);
266        loop {
267            if (idx) >= (self.file_constraints.rotation.len()) && last_idx == -1 {
268                // if we haven't
269                // met any
270                // unverified
271                // constraints
272                // and reached
273                // the end, we
274                // stop
275                break;
276            }
277            if (idx as i32) == last_idx {
278                // if we reached last index it means we restarted from 0,
279                // then we ran through the right part from last_idx and
280                // though the left as well
281                break;
282            }
283            if idx >= (self.file_constraints.rotation.len()) && last_idx != -1 {
284                // if we meet the
285                // end and
286                // last_idx is not
287                // -1, then we
288                // should go
289                // through the
290                // left part from
291                // last_idx again
292                idx = 0;
293            }
294            if last_idx == 0 {
295                // if last_idx == 0 then the first one wasn't satisfied and it was
296                // immediately handled
297                break;
298            }
299
300            //each rot logic
301
302            let rot = self.file_constraints.rotation[idx];
303            match rot.rotation_type {
304                RotationType::Period(_) | RotationType::Time(_, _) => {
305                    let unix_now = chrono::Utc::now().timestamp()        
306                            .max(0) // never negative
307                            as u64;
308                    if unix_now > rot.next_rotation || last_idx != -1 {
309                        // if current time is ahead of our
310                        // rotation that we set a new one and create
311                        // a new file
312                        let new_rot = Rotation::init_from_rotation_type(rot.rotation_type);
313                        self.file_constraints.rotation[idx] = new_rot;
314                        if last_idx == -1 {
315                            match self.create_new_file(config) {
316                                Ok(_) => {}
317                                Err(e) => {
318                                    return Err(VerifyConstraintsError::UnableToCreateNewFile(e));
319                                }
320                            }
321                            if self.compress_file(&curr_file_name).is_ok() {
322                                if let Err(e) = FileManager::delete_file(&curr_file_name) {
323                                    res = Err(VerifyConstraintsError::UnableToDeleteOldLogFile(
324                                        curr_file_name.clone(),
325                                        e,
326                                    ));
327                                } else {
328                                    res = Ok(VerifyConstraintsRes::NewFileCreated)
329                                }
330                            } else {
331                                res = Err(VerifyConstraintsError::UnableToCompressFile)
332                            }
333                            last_idx = idx as i32;
334                        }
335                    }
336                }
337                RotationType::Size(_) => {
338                    if f_size > rot.next_rotation || last_idx != -1 {
339                        let new_rot = Rotation::init_from_rotation_type(rot.rotation_type);
340                        self.file_constraints.rotation[idx] = new_rot;
341                        if last_idx == -1 {
342                            match self.create_new_file(config) {
343                                Ok(_) => {}
344                                Err(e) => {
345                                    return Err(VerifyConstraintsError::UnableToCreateNewFile(e));
346                                }
347                            }
348                            if self.compress_file(&curr_file_name).is_ok() {
349                                if let Err(e) = FileManager::delete_file(&curr_file_name) {
350                                    res = Err(VerifyConstraintsError::UnableToDeleteOldLogFile(
351                                        curr_file_name.clone(),
352                                        e,
353                                    ));
354                                } else {
355                                    res = Ok(VerifyConstraintsRes::NewFileCreated)
356                                }
357                            } else {
358                                res = Err(VerifyConstraintsError::UnableToCompressFile)
359                            }
360                            last_idx = idx as i32;
361                        }
362                    }
363                }
364            }
365            // end
366            idx += 1;
367        }
368        res
369    }
370    pub(crate) fn delete_file(path: &str) -> io::Result<()> {
371        std::fs::remove_file(path)
372    }
373
374    pub(crate) fn write_log(
375        &mut self,
376        mess: &str,
377        config: Config,
378    ) -> Result<VerifyConstraintsRes, WriteLogError> {
379        let mut ok_res = Ok(VerifyConstraintsRes::ConstraintsPassed);
380        match self.verify_constraints(&config) {
381            Ok(r) => ok_res = Ok(r),
382            Err(e) => {
383                eprintln!("An error occured while verifying constraints: {}", e);
384                eprintln!("Trying to write to an old file");
385                ok_res = Err(e)
386            }
387        }
388
389        let arc_file = self.curr_file.clone();
390        let mut file = (*arc_file).try_clone().map_err(|e| WriteLogError::UnableToWriteToFile(WriteToFileError::UnexpectedError(e)))?;
391
392        let res = helper::write_to_file(&mut file, mess)
393            .map(|_| ok_res.unwrap())
394            .map_err(WriteLogError::UnableToWriteToFile);
395
396        self.set_curr_file(file);
397        res
398    }
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Copy)]
402pub(crate) enum RotationType {
403    Period(u64),  // every 1 week for example
404    Time(u8, u8), //every day at 12:00 for example
405    Size(u64),    //500 MB for example
406}
407
408impl RotationType {
409    pub(crate) fn try_from_string(text: &str) -> Option<RotationType> {
410        if text.contains(":") {
411            // time
412            let sp: Vec<&str> = text.split(":").collect();
413            if sp.len() != 2 {
414                return None;
415            }
416            let h: u8 = match sp[0].parse() {
417                Ok(n) => n,
418                Err(_) => return None,
419            };
420            let m: u8 = match sp[1].parse() {
421                Ok(n) => n,
422                Err(_) => return None,
423            };
424            if !(0..=23).contains(&h) {
425                return None;
426            }
427            if !(0..=59).contains(&m) {
428                return None;
429            }
430            Some(RotationType::Time(h, m))
431        } else if text.ends_with(" KB")
432            || text.ends_with(" MB")
433            || text.ends_with(" GB")
434            || text.ends_with(" TB")
435        {
436            //size
437            let multiply_factor;
438            if text.ends_with(" KB") {
439                multiply_factor = 1024;
440            } else if text.ends_with(" MB") {
441                multiply_factor = 1024 * 1024;
442            } else if text.ends_with(" GB") {
443                multiply_factor = 1024 * 1024 * 1024;
444            } else if text.ends_with(" TB") {
445                multiply_factor = 1024 * 1024 * 1024 * 1024;
446            } else {
447                multiply_factor = 1;
448            }
449
450            let t_len = text.len();
451            let text = &text[0..(t_len - 3)];
452            let num: u64 = match text.parse() {
453                Ok(n) => n,
454                Err(_) => {
455                    return None;
456                }
457            };
458            Some(RotationType::Size(num * multiply_factor))
459        } else if text.ends_with(" hour")
460            || text.ends_with(" day")
461            || text.ends_with(" week")
462            || text.ends_with(" month")
463            || text.ends_with(" year")
464        {
465            // period
466            let multiply_factor: u64;
467            let finish_txt: &str = {
468                if text.ends_with(" hour") {
469                    multiply_factor = 60 * 60;
470                    " hour"
471                } else if text.ends_with(" day") {
472                    multiply_factor = 60 * 60 * 24;
473                    " day"
474                } else if text.ends_with(" week") {
475                    multiply_factor = 60 * 60 * 24 * 7;
476                    " week"
477                } else if text.ends_with(" month") {
478                    multiply_factor = 60 * 60 * 24 * 30;
479                    " month"
480                } else {
481                    multiply_factor = 60 * 60 * 24 * 365;
482                    " year"
483                }
484            };
485            let fin_len = finish_txt.len();
486            let str_len = text.len();
487            let text_to_parse = &text[0..(str_len - fin_len)];
488            let num: u64 = match text_to_parse.parse() {
489                Ok(n) => n,
490                Err(_) => {
491                    return None;
492                }
493            };
494            Some(RotationType::Period(num * multiply_factor))
495        } else {
496            None
497        }
498    }
499}
500
501#[derive(Clone, Copy, Debug)]
502pub(crate) struct Rotation {
503    rotation_type: RotationType,
504    next_rotation: u64,
505}
506impl Rotation {
507    pub(crate) fn init_from_rotation_type(rot_type: RotationType) -> Rotation {
508        match rot_type {
509            RotationType::Period(p) => {
510                let unix_time: u64 = chrono::Utc::now().timestamp().try_into().unwrap_or(0);
511                let next_to_rotate = unix_time + p;
512                Rotation {
513                    rotation_type: rot_type,
514                    next_rotation: next_to_rotate,
515                }
516            }
517            RotationType::Time(h, m) => {
518                let h = h as u64;
519                let m = m as u64;
520                let now = chrono::Local::now();
521                let curr_h: u64 = now.hour().into();
522                let curr_m: u64 = now.minute().into();
523                if curr_h < h || (curr_h == h && curr_m < m) {
524                    // if next rotation is today
525                    let unix: u64 = now.timestamp().max(0) as u64;
526                    let secs_curr = ((curr_h) * 60 * 60) + (curr_m * 60);
527                    let secs_desirable = (h * 60 * 60) + (m * 60);
528                    let diff = secs_desirable - secs_curr;
529                    Rotation {
530                        rotation_type: rot_type,
531                        next_rotation: unix + diff,
532                    }
533                } else {
534                    //tomorrow
535                    let unix: u64 = now.timestamp().max(0) as u64;
536                    let secs_till_tomorrow =
537                        (24 * 60 * 60) - ((curr_h * 60 * 60) + (curr_m * 60));
538                    let secs_desirable = ((h * 60 * 60) + (m * 60));
539                    Rotation {
540                        rotation_type: rot_type,
541                        next_rotation: unix + secs_till_tomorrow + secs_desirable,
542                    }
543                }
544            }
545            RotationType::Size(s) => Rotation {
546                rotation_type: rot_type,
547                next_rotation: s,
548            },
549        }
550    }
551}
552
553#[derive(Clone, Debug)]
554pub(crate) enum CompressionType {
555    Zip,
556}
557
558impl CompressionType {
559    pub(crate) fn try_from_string(text: &str) -> Option<CompressionType> {
560        if text == "zip" {
561            Some(CompressionType::Zip)
562        } else {
563            None
564        }
565    }
566}
567
568#[derive(Clone, Default, Debug)]
569pub(crate) struct FileConstraints {
570    compression: Option<CompressionType>,
571    rotation: Vec<Rotation>,
572}