ffsend/src/history.rs
2021-10-14 22:52:02 +02:00

356 lines
10 KiB
Rust

use std::fs;
use std::io::Error as IoError;
use std::path::PathBuf;
use failure::Fail;
use ffsend_api::{
file::remote_file::{FileParseError, RemoteFile},
url::Url,
};
use toml::{de::Error as DeError, ser::Error as SerError};
use version_compare::Cmp;
use crate::util::{print_error, print_warning};
/// The minimum supported history file version.
const VERSION_MIN: &str = "0.0.1";
/// The maximum supported history file version.
const VERSION_MAX: &str = crate_version!();
#[derive(Serialize, Deserialize)]
pub struct History {
/// The application version the history file was built with.
/// Used for compatibility checking.
version: Option<String>,
/// The file history.
files: Vec<RemoteFile>,
/// Whether the list of files has changed.
#[serde(skip)]
changed: bool,
/// An optional path to automatically save the history to.
#[serde(skip)]
autosave: Option<PathBuf>,
}
impl History {
/// Construct a new history.
/// A path may be given to automatically save the history to once changed.
pub fn new(autosave: Option<PathBuf>) -> Self {
let mut history = History::default();
history.autosave = autosave;
history
}
/// Load the history from the given file.
pub fn load(path: PathBuf) -> Result<Self, LoadError> {
// Read the file to a string
let data = fs::read_to_string(&path)?;
// Parse the data, set the autosave path
let mut history: Self = toml::from_str(&data)?;
history.autosave = Some(path);
// Make sure the file version is supported
if history.version.is_none() {
print_warning("History file has no version, ignoring");
history.version = Some(crate_version!().into());
} else {
// Get the version number from the file
let version = history.version.as_ref().unwrap();
if let Ok(true) = version_compare::compare_to(version, VERSION_MIN, Cmp::Lt) {
print_warning("history file version is too old, ignoring");
} else if let Ok(true) = version_compare::compare_to(version, VERSION_MAX, Cmp::Gt) {
print_warning("history file has an unknown version, ignoring");
}
}
// Garbage collect
history.gc();
Ok(history)
}
/// Load the history from the given file.
/// If the file doesn't exist, create a new empty history instance.
///
/// Autosaving will be enabled, and will save to the given file path.
pub fn load_or_new(file: PathBuf) -> Result<Self, LoadError> {
if file.is_file() {
Self::load(file)
} else {
Ok(Self::new(Some(file)))
}
}
/// Save the history to the internal autosave file.
pub fn save(&mut self) -> Result<(), SaveError> {
// Garbage collect
self.gc();
// Get the path
let path = self.autosave.as_ref().ok_or(SaveError::NoPath)?;
// If we have no files, remove the history file if it exists
if self.files.is_empty() {
if path.is_file() {
fs::remove_file(&path).map_err(SaveError::Delete)?;
}
return Ok(());
}
// Ensure the file parent directories are available
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
// Set file permissions on unix based systems
#[cfg(unix)]
{
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
if !path.exists() {
let file = fs::File::create(path).map_err(SaveError::Write)?;
// Set Read/Write permissions for the user
file.set_permissions(Permissions::from_mode(0o600))
.map_err(SaveError::SetPermissions)?;
}
}
// Build the data and write to a file
let data = toml::to_string(self)?;
fs::write(&path, data)?;
// There are no new changes, set the flag
self.changed = false;
Ok(())
}
/// Add the given remote file to the history.
/// If a file with the same ID as the given file exists,
/// the files are merged, see `RemoteFile::merge()`.
///
/// If `overwrite` is set to true, the given file will overwrite
/// properties on the existing file.
pub fn add(&mut self, file: RemoteFile, overwrite: bool) {
// Merge any existing file with the same ID
{
// Find anything to merge
let merge_info: Vec<bool> = self
.files
.iter_mut()
.filter(|f| f.id() == file.id())
.map(|ref mut f| f.merge(&file, overwrite))
.collect();
let merged = !merge_info.is_empty();
let changed = merge_info.iter().any(|i| *i);
// Return if merged, update the changed state
if merged {
if changed {
self.changed = true;
}
return;
}
}
// Add the file to the list
self.files.push(file);
self.changed = true;
}
/// Remove a file, matched by it's file ID.
///
/// If any file was removed, true is returned.
pub fn remove(&mut self, id: &str) -> bool {
// Get the indices of files that have expired
let expired_indices: Vec<usize> = self
.files
.iter()
.enumerate()
.filter(|&(_, f)| f.id() == id)
.map(|(i, _)| i)
.collect();
// Remove these specific files
for i in expired_indices.iter().rev() {
self.files.remove(*i);
}
// Set the changed flag, and return
if expired_indices.is_empty() {
self.changed = true;
}
!expired_indices.is_empty()
}
/// Remove a file by the given URL.
///
/// If any file was removed, true is returned.
pub fn remove_url(&mut self, url: Url) -> Result<bool, FileParseError> {
Ok(self.remove(RemoteFile::parse_url(url, None)?.id()))
}
/// Get all files.
pub fn files(&self) -> &Vec<RemoteFile> {
&self.files
}
/// Get a file from the history, based on the given remote file.
/// The file ID and host will be compared against all files in this history.
/// If multiple files exist within the history that are equal, only one is returned.
/// If no matching file was found, `None` is returned.
pub fn get_file(&self, file: &RemoteFile) -> Option<&RemoteFile> {
self.files
.iter()
.find(|f| f.id() == file.id() && f.host() == file.host())
}
/// Clear all history.
pub fn clear(&mut self) {
self.changed = !self.files.is_empty();
self.files.clear();
}
/// Garbage collect (remove) all files that have been expired,
/// as defined by their `expire_at` property.
///
/// If the expiry property is None (thus unknown), the file will be kept.
///
/// The number of expired files is returned.
pub fn gc(&mut self) -> usize {
// Get a list of expired files
let expired: Vec<RemoteFile> = self
.files
.iter()
.filter(|f| f.has_expired())
.cloned()
.collect();
// Remove the files
for f in &expired {
self.remove(f.id());
}
// Set the changed flag
if !expired.is_empty() {
self.changed = true;
}
// Return the number of expired files
expired.len()
}
}
impl Drop for History {
fn drop(&mut self) {
// Automatically save if enabled and something was changed
if self.autosave.is_some() && self.changed {
// Save and report errors
if let Err(err) = self.save() {
print_error(err.context("failed to auto save history, ignoring"));
}
}
}
}
impl Default for History {
fn default() -> Self {
Self {
version: Some(crate_version!().into()),
files: Vec::new(),
changed: false,
autosave: None,
}
}
}
#[derive(Debug, Fail)]
pub enum Error {
/// An error occurred while loading the history from a file.
#[fail(display = "failed to load history from file")]
Load(#[cause] LoadError),
/// An error occurred while saving the history to a file.
#[fail(display = "failed to save history to file")]
Save(#[cause] SaveError),
}
impl From<LoadError> for Error {
fn from(err: LoadError) -> Self {
Error::Load(err)
}
}
impl From<SaveError> for Error {
fn from(err: SaveError) -> Self {
Error::Save(err)
}
}
#[derive(Debug, Fail)]
pub enum LoadError {
/// Failed to read the file contents from the given file.
#[fail(display = "failed to read from the history file")]
Read(#[cause] IoError),
/// Failed to parse the loaded file.
#[fail(display = "failed to parse the file contents")]
Parse(#[cause] DeError),
}
impl From<IoError> for LoadError {
fn from(err: IoError) -> Self {
LoadError::Read(err)
}
}
impl From<DeError> for LoadError {
fn from(err: DeError) -> Self {
LoadError::Parse(err)
}
}
#[derive(Debug, Fail)]
pub enum SaveError {
/// No autosave file path was present, failed to save.
#[fail(display = "no autosave file path specified")]
NoPath,
/// Failed to serialize the history for saving.
#[fail(display = "failed to serialize the history for saving")]
Serialize(#[cause] SerError),
/// Failed to write to the history file.
#[fail(display = "failed to write to the history file")]
Write(#[cause] IoError),
/// Failed to set file permissions to the history file.
#[fail(display = "failed to set permissions to the history file")]
SetPermissions(#[cause] IoError),
/// Failed to delete the history file, which was tried because there
/// are no history items to save.
#[fail(display = "failed to delete history file, because history is empty")]
Delete(#[cause] IoError),
}
impl From<SerError> for SaveError {
fn from(err: SerError) -> Self {
SaveError::Serialize(err)
}
}
impl From<IoError> for SaveError {
fn from(err: IoError) -> Self {
SaveError::Write(err)
}
}