1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
//! The Backup module handles creating backups using the methods
//! specified in the GNU coreutils manual.
//!
//! ## About
//! The GNU coreutils [backup options](https://www.gnu.org/software/coreutils/manual/html_node/Backup-options.html#Backup-options)
//! support 4 backup modes:
//! - None
//! - Numbered
//! - Existing
//! - Simple
//!
//! **NOTE:** This module does not figure out default values based on environment
//! variables as defined in the GNU backup options manual page. Whether to adhere to the
//! GNU standard or not is up to the user of this module.
//!
//! ### None
//! Can be specified by either supplying the strings `none` or `off`.
//! Fairly self-explanatory: Never make backups.
//!
//! ### Numbered
//! Can be specified by either supplying the strings `numbered` or `t`.
//! This mode always makes numbered backups. This means that a backup of a file `a.txt`
//! will be backed up to `a.txt~X~` where `X` is the next number backup.
//!
//! For example, if we create a file named `main.rs` and then back it up in this mode, we
//! will get `main.rs~1~`. If we back the file up a second time, we will get `main.rs~2~`.
//!
//! ### Simple
//! Can be specified by either supplying the strings `simple` or `never` (not to be
//! confused with `none`) This mode simple appends a static suffix to the backup (This is
//! how Emacs makes backup files by default).
//!
//! For exmaple, if we create a file named `main.rs` and then back it up in this mode, we
//! will get `main.rs~`. If we back the file up a second time, we will overwrite the old
//! backup and the new backup will be called `main.rs~`.
//!
//! ### Existing
//! Can be specified by either supplying the strings `existing` or `nil`.
//! This mode checks for the existance of previous backups in any mode. If it finds
//! numbered backups, it will continue to make numbered backups. If it finds simple
//! backups, it will continue to make simple backups.

use regex::Regex;
use std::{
    fs,
    io::{Error, ErrorKind},
    path::{Path, PathBuf},
};

/// Convenience Enum to represent the different backup modes. See module documentation for
/// an in-depth overview of what each backup mode means/does.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BackupMode {
    /// No backups will be made.
    None,
    /// Backups will be made of the form `<filename>~<X>~` where `X` is the next backup
    /// number.
    Numbered,
    /// The backup method will be consistant with what already exists for the current
    /// file.
    Existing,
    /// Backups will be made of the form `<filename><suffix>` where `suffix` is any
    /// suffix.
    Simple,
}

impl BackupMode {
    /// Creates an instance of [`BackupMode`] from a string slice. Any invalid input will
    /// result in [`BackupMode::Existing`] to be returned.
    pub fn from_string(string: impl AsRef<str>) -> Self { Self::from(string.as_ref()) }
}

impl From<&str> for BackupMode {
    /// Creates an instance of [`BackupMode`] from a string slice. Any invalid input will
    /// result in [`BackupMode::Existing`] to be returned.
    fn from(string: &str) -> Self {
        match string {
            "none" | "off" => BackupMode::None,
            "numbered" | "t" => BackupMode::Numbered,
            "existing" | "nil" => BackupMode::Existing,
            "simple" | "never" => BackupMode::Simple,
            _ => BackupMode::Existing,
        }
    }
}

/// Creates a numbered backup. Does so by taking the input `file` and poking the parent
/// directory to find a file of the form `<file>~<X>~` where `X` is a number. If none can
/// be found, a backup file is created where `X` is `1`. Else, it creates a backup file
/// where `X` is `X + 1`.
///
/// # Errors
/// If this function encounters any kind of I/O error, an error variant will be returned.
// # Arguments
// * `file` - the file to be backed up
// # Remarks
// Returns a `Result` of either a `PathBuf` to the newly created backup file or an
// `io::Error`
pub fn create_numbered_backup(file: &Path) -> Result<PathBuf, Error> {
    let mut index = 1_u64;
    loop {
        if index == std::u64::MAX {
            return Err(Error::new(
                ErrorKind::AlreadyExists,
                "Cannot create backup: too many backup files",
            ));
        }

        let new = file.with_extension(format!("~{}~", index));
        if !new.exists() {
            match fs::rename(file, &new) {
                Ok(()) => return Ok(new),
                Err(err) => return Err(err),
            };
        }

        index += 1;
    }
}

/// Creates a backup in-keeping with previous backups. Pokes the directory to see whether
/// there are any numbered or simple backups of the input `file`. If numbered backups are
/// found, a numbered backup will be created. Else, a simple backup is created using the
/// input `suffix`
///
/// # Errors
/// If this function encounters any kind of I/O error, an error variant will be returned.
// # Arguments
// * `file` - the file to be backed up
// * `suffix` - the suffix of the backup file
// # Remarks
// Returns a `Result` of either a `PathBuf` to the newly created backup file or an
// `io::Error`
pub fn create_existing_backup(file: &Path, suffix: &str) -> Result<PathBuf, Error> {
    let mut has_numbered_backup = false;
    let regex = Regex::new(r"~\d+~").unwrap();
    let parent = file.parent().unwrap();
    for entry in parent.read_dir().unwrap() {
        if let Ok(entry) = entry {
            if let Some(ext) = entry.path().extension() {
                if regex.is_match(ext.to_str().unwrap()) {
                    has_numbered_backup = true;
                    break;
                }
            }
        }
    }

    if has_numbered_backup {
        create_numbered_backup(file)
    } else {
        create_simple_backup(file, suffix)
    }
}

/// Creates a simple backup. Creates a backup of the form `<file><suffix>`. Overwrites any
/// previous backup files with that same suffix.
///
/// # Errors
/// If this function encounters any kind of I/O error, an error variant will be returned.
// # Arguments
// * `file` - the file to be backed up
// * `suffix` - the suffix of the backup file
// # Remarks
// Returns a `Result` of either a `PathBuf` to the newly created backup file or an
// `io::Error`
pub fn create_simple_backup(file: &Path, suffix: &str) -> Result<PathBuf, Error> {
    let new = PathBuf::from(format!("{}{}", file.display(), suffix));

    match fs::rename(file, &new) {
        Ok(()) => Ok(new),
        Err(error) => Err(error),
    }
}