use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use std::fs;
use std::path::{Path, PathBuf};
use crate::config::Config;
use crate::scanner::StaleItem;
pub struct Archiver {
config: Config,
}
#[derive(Debug)]
pub struct ArchivedItem {}
impl Archiver {
pub fn new(config: Config) -> Self {
Self { config }
}
pub fn archive_item_with_note(&self, item: &StaleItem, note: Option<&str>) -> Result<ArchivedItem> {
let now = Utc::now();
let created_time = self.get_creation_time(&item.path)?;
let modified_time = item.last_modified;
let archived_time = now;
let mut original_subdirs = self.find_original_subdirs()?;
for (subdir_name, time) in original_subdirs.iter_mut() {
if self.config.path_format.created_subdir.get_name() == Some(subdir_name) {
*time = created_time;
} else if self.config.path_format.modified_subdir.get_name() == Some(subdir_name) {
*time = modified_time;
} else if self.config.path_format.archived_subdir.get_name() == Some(subdir_name) {
*time = archived_time;
}
}
let mut primary_path = None;
let mut created_paths = std::collections::HashMap::new();
for (i, (subdir_name, time)) in original_subdirs.iter().enumerate() {
let target_path = self.create_path_for_subdir(subdir_name, &item.name, *time)?;
self.ensure_directory_exists(target_path.parent().unwrap())?;
if i == 0 {
fs::rename(&item.path, &target_path)
.context("Failed to move item to graveyard")?;
primary_path = Some(target_path.clone());
} else {
if item.path.is_dir() {
self.copy_dir_all(&primary_path.as_ref().unwrap(), &target_path)?;
} else {
fs::copy(&primary_path.as_ref().unwrap(), &target_path)
.context("Failed to copy item to additional location")?;
}
}
created_paths.insert(subdir_name.clone(), target_path.clone());
println!("ðŠĶ Stored '{}' in: {}", item.name, target_path.display());
}
self.create_remaining_symlinks(&item.name, &created_paths, created_time, modified_time, archived_time)?;
if let Some(note_text) = note {
self.save_epitaphs_with_logic(&item.name, &created_paths, note_text, &created_time, &modified_time, &archived_time)?;
}
println!("â
Archived '{}' to {} locations", item.name, original_subdirs.len());
if note.is_some() {
println!("ð Epitaph saved with the archived item");
}
Ok(ArchivedItem {})
}
fn get_creation_time(&self, path: &Path) -> Result<DateTime<Utc>> {
let metadata = fs::metadata(path)
.context("Failed to get metadata")?;
if let Ok(created) = metadata.created() {
Ok(DateTime::from(created))
} else {
Ok(DateTime::from(metadata.modified()?))
}
}
fn find_original_subdirs(&self) -> Result<Vec<(String, DateTime<Utc>)>> {
let mut originals = Vec::new();
let now = Utc::now();
if self.config.path_format.created_subdir.is_original() {
originals.push((
self.config.path_format.created_subdir.get_name().unwrap().to_string(),
now
));
}
if self.config.path_format.modified_subdir.is_original() {
originals.push((
self.config.path_format.modified_subdir.get_name().unwrap().to_string(),
now
));
}
if self.config.path_format.archived_subdir.is_original() {
originals.push((
self.config.path_format.archived_subdir.get_name().unwrap().to_string(),
now ));
}
if originals.is_empty() {
return Err(anyhow::anyhow!("No subdir configured to store original files"));
}
Ok(originals)
}
fn create_path_for_subdir(&self, subdir_name: &str, name: &str, time: DateTime<Utc>) -> Result<PathBuf> {
let date_path = self.config.format_date_path(&time);
let mut path = self.config.graveyard
.join(subdir_name)
.join(date_path)
.join(name);
path = self.ensure_unique_name(path)?;
Ok(path)
}
fn get_path_for_subdir(&self, subdir_name: &str, name: &str, time: DateTime<Utc>) -> PathBuf {
let date_path = self.config.format_date_path(&time);
self.config.graveyard
.join(subdir_name)
.join(date_path)
.join(name)
}
fn copy_dir_all(&self, src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
self.copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.join(entry.file_name()))?;
}
}
Ok(())
}
fn create_remaining_symlinks(&self, name: &str, _created_paths: &std::collections::HashMap<String, PathBuf>, created_time: DateTime<Utc>, modified_time: DateTime<Utc>, archived_time: DateTime<Utc>) -> Result<()> {
let subdirs = [
("created", &self.config.path_format.created_subdir, created_time),
("modified", &self.config.path_format.modified_subdir, modified_time),
("archived", &self.config.path_format.archived_subdir, archived_time),
];
for (_subdir_type, subdir_config, time) in subdirs {
if !subdir_config.is_enabled() || subdir_config.is_original() {
continue; }
let subdir_name = subdir_config.get_name().unwrap();
if let Some(target_subdir) = subdir_config.get_target() {
let target_time = if self.config.path_format.created_subdir.get_name() == Some(target_subdir) {
created_time
} else if self.config.path_format.modified_subdir.get_name() == Some(target_subdir) {
modified_time
} else if self.config.path_format.archived_subdir.get_name() == Some(target_subdir) {
archived_time
} else {
time
};
let target_path = self.get_path_for_subdir(target_subdir, name, target_time);
let link_path = self.create_path_for_subdir(subdir_name, name, time)?;
self.ensure_directory_exists(link_path.parent().unwrap())?;
self.create_symlink(&target_path, &link_path)?;
println!("ð Created symlink '{}' -> {}", link_path.display(), target_path.display());
}
}
Ok(())
}
fn ensure_unique_name(&self, mut path: PathBuf) -> Result<PathBuf> {
if !path.exists() {
return Ok(path);
}
let original_name = path.file_name()
.context("Invalid path")?
.to_string_lossy()
.to_string();
let mut counter = 1;
loop {
let new_name = if let Some(dot_pos) = original_name.rfind('.') {
format!(
"{}_{}{}",
&original_name[..dot_pos],
counter,
&original_name[dot_pos..]
)
} else {
format!("{}_{}", original_name, counter)
};
path.set_file_name(new_name);
if !path.exists() {
break;
}
counter += 1;
}
Ok(path)
}
fn save_epitaphs_with_logic(&self, name: &str, created_paths: &std::collections::HashMap<String, PathBuf>, note: &str, created_time: &DateTime<Utc>, modified_time: &DateTime<Utc>, archived_time: &DateTime<Utc>) -> Result<()> {
let epitaph_content = format!(
"# Epitaph for {}\n\
# Archived: {}\n\
# Created: {}\n\
# Modified: {}\n\
# Hostname: {}\n\
\n\
{}",
name,
archived_time.format("%Y-%m-%d %H:%M:%S UTC"),
created_time.format("%Y-%m-%d %H:%M:%S UTC"),
modified_time.format("%Y-%m-%d %H:%M:%S UTC"),
self.config.get_hostname(),
note
);
let epitaph_filename = format!("{}.epitaph", name);
let mut primary_epitaph_path = None;
let mut created_epitaph_paths = std::collections::HashMap::new();
for (subdir_name, file_path) in created_paths {
let epitaph_path = file_path.parent().unwrap().join(&epitaph_filename);
if primary_epitaph_path.is_none() {
fs::write(&epitaph_path, &epitaph_content)
.context("Failed to write epitaph file")?;
primary_epitaph_path = Some(epitaph_path.clone());
println!("ð Epitaph written to: {}", epitaph_path.display());
} else {
fs::copy(primary_epitaph_path.as_ref().unwrap(), &epitaph_path)
.context("Failed to copy epitaph file")?;
println!("ð Epitaph copied to: {}", epitaph_path.display());
}
created_epitaph_paths.insert(subdir_name.clone(), epitaph_path);
}
let subdirs = [
("created", &self.config.path_format.created_subdir, *created_time),
("modified", &self.config.path_format.modified_subdir, *modified_time),
("archived", &self.config.path_format.archived_subdir, *archived_time),
];
for (_subdir_type, subdir_config, time) in subdirs {
if !subdir_config.is_enabled() || subdir_config.is_original() {
continue; }
let subdir_name = subdir_config.get_name().unwrap();
if let Some(target_subdir) = subdir_config.get_target() {
let target_time = if self.config.path_format.created_subdir.get_name() == Some(target_subdir) {
*created_time
} else if self.config.path_format.modified_subdir.get_name() == Some(target_subdir) {
*modified_time
} else if self.config.path_format.archived_subdir.get_name() == Some(target_subdir) {
*archived_time
} else {
time
};
let target_epitaph_path = self.get_path_for_subdir(target_subdir, &epitaph_filename, target_time);
let link_epitaph_path = self.create_path_for_subdir(subdir_name, &epitaph_filename, time)?;
self.ensure_directory_exists(link_epitaph_path.parent().unwrap())?;
self.create_symlink(&target_epitaph_path, &link_epitaph_path)?;
println!("ð Created epitaph symlink '{}' -> {}", link_epitaph_path.display(), target_epitaph_path.display());
}
}
Ok(())
}
fn ensure_directory_exists(&self, path: &Path) -> Result<()> {
fs::create_dir_all(path)
.context("Failed to create directory")
}
fn create_symlink(&self, target: &Path, link: &Path) -> Result<()> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, link)
.context("Failed to create symlink")?;
}
#[cfg(windows)]
{
if target.is_dir() {
std::os::windows::fs::symlink_dir(target, link)
.context("Failed to create directory symlink")?;
} else {
std::os::windows::fs::symlink_file(target, link)
.context("Failed to create file symlink")?;
}
}
Ok(())
}
}