timelog/
file.rs

1//! Interface for manipulating the end of a file.
2
3use std::fs::{self, File};
4use std::io::{BufRead, Seek};
5use std::path::PathBuf;
6
7#[doc(inline)]
8use crate::error::PathError;
9
10/// Create a new file for appending from the supplied filename.
11///
12/// # Errors
13///
14/// - Return [`PathError::FileAccess`] if unable to append to the file
15pub fn append_open(filename: &str) -> Result<File, PathError> {
16    fs::OpenOptions::new()
17        .create(true)
18        .append(true)
19        .open(filename)
20        .map_err(|e| PathError::FileAccess(filename.to_string(), e.to_string()))
21}
22
23/// Open the supplied file for reading and writing, return a [`File`].
24///
25/// # Errors
26///
27/// - Return [`PathError::FileAccess`] if unable to open the file.
28pub fn rw_open(filename: &str) -> Result<File, PathError> {
29    fs::OpenOptions::new()
30        .read(true)
31        .write(true)
32        .open(filename)
33        .map_err(|e| PathError::FileAccess(filename.to_string(), e.to_string()))
34}
35
36// Which kind of file
37pub(crate) enum FileKind {
38    LogFile,
39    StackFile
40}
41
42impl From<FileKind> for PathError {
43    // Generate a invalid path error from the kind of file.
44    fn from(kind: FileKind) -> PathError {
45        match kind {
46            FileKind::LogFile => PathError::InvalidTimelogPath,
47            FileKind::StackFile => PathError::InvalidStackPath
48        }
49    }
50}
51
52// Test and canonicalize the supplied filename.
53//
54// # Errors
55//
56// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
57// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
58// - Return [`PathError::InvalidStackPath`] if stack path is invalid.
59pub(crate) fn canonical_filename(file: &str, kind: FileKind) -> Result<String, PathError> {
60    if file.is_empty() {
61        return Err(PathError::FilenameMissing);
62    }
63    let mut dir = PathBuf::from(file);
64    let filename = dir
65        .file_name()
66        .ok_or(PathError::FilenameMissing)?
67        .to_os_string();
68    dir.pop();
69
70    let mut candir = fs::canonicalize(dir)
71        .map_err(|e| PathError::InvalidPath(file.to_string(), e.to_string()))?;
72    candir.push(filename);
73
74    candir.into_os_string().into_string().map_err(|_| kind.into())
75}
76
77/// Remove the most recent task from the stack file and return the task string.
78pub fn pop_last_line(file: &mut File) -> Option<String> {
79    let (line, pos) = find_last_line(file)?;
80
81    file.set_len(pos).ok()?;
82    Some(line)
83}
84
85/// Find the last line in the supplied file.
86/// Return an optional tuple of the line and the offset to that line.
87pub fn find_last_line(file: &mut File) -> Option<(String, u64)> {
88    let mut last_pos = 0u64;
89    let mut last_line = String::new();
90
91    if file.metadata().ok()?.len() == 0u64 {
92        return None;
93    }
94
95    let mut reader = std::io::BufReader::new(file);
96    while let Some((line, pos)) = last_line_and_pos(&mut reader) {
97        last_pos = pos;
98        last_line = line.trim_end().to_string();
99    }
100
101    Some((last_line, last_pos))
102}
103
104// Find the last line and the position of the beginning of that line in the stack file.
105fn last_line_and_pos<R>(reader: &mut R) -> Option<(String, u64)>
106where
107    R: BufRead + Seek
108{
109    let pos = reader.stream_position().ok()?;
110    let mut line = String::new();
111    (0 != reader.read_line(&mut line).ok()?).then_some((line, pos))
112}