use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use sha1::Digest;
use walkdir::WalkDir;
const INDEX_FILE: &str = ".osync";
const IGNORE_FILE: &str = ".osyncignore";
pub struct Index {
directory: PathBuf,
files: HashMap<String, String>,
}
impl Index {
fn blank<P: AsRef<Path>>(directory: P) -> Index {
Index {
directory: directory.as_ref().to_path_buf(),
files: HashMap::new(),
}
}
pub fn load<P: AsRef<Path>>(directory: P) -> Result<Index, Box<dyn Error>> {
let index_path = directory.as_ref().join(INDEX_FILE);
if !index_path.exists() {
return Ok(Index::blank(directory));
}
let mut files: HashMap<String, String> = HashMap::new();
let buf = BufReader::new(File::open(index_path)?);
for line in buf.lines() {
let line = line.unwrap();
let parts: Vec<&str> = line.split(':').collect();
files.insert(parts[0].to_string(), parts[1].to_string());
}
Ok(Index {
directory: directory.as_ref().to_path_buf(),
files,
})
}
pub fn compute<P: AsRef<Path>>(directory: P) -> Result<(Index, usize), Box<dyn Error>> {
let mut ignored_files: HashMap<String, bool> = HashMap::new();
if let Ok(file) = File::open(directory.as_ref().join(IGNORE_FILE)) {
let buf = BufReader::new(file);
for line in buf.lines() {
ignored_files.insert(line.unwrap(), true);
}
}
ignored_files.insert(INDEX_FILE.to_string(), true);
ignored_files.insert(IGNORE_FILE.to_string(), true);
let mut files: HashMap<String, String> = HashMap::new();
for entry in WalkDir::new(&directory).into_iter().filter_map(|e| e.ok()) {
let local_path = entry.path().strip_prefix(&directory)?;
let metadata = entry.metadata().unwrap();
if metadata.is_file() && !ignored_files.contains_key(local_path.to_str().unwrap()) {
let bytes = fs::read(entry.path()).expect("unable to read file");
let mut hasher = sha1::Sha1::new();
hasher.update(bytes);
files.insert(
local_path.to_str().unwrap().to_string(),
format!("{:x}", hasher.finalize()),
);
}
}
Ok((
Index {
directory: directory.as_ref().to_path_buf(),
files,
},
ignored_files.len(),
))
}
pub fn save(&self) -> Result<(), Box<dyn Error>> {
let mut file = File::create(self.directory.join(INDEX_FILE))?;
let mut content = String::new();
for (path, hash) in self.files.iter() {
content += format!("{}:{}\n", path, hash).as_str();
}
file.write_all(content.as_bytes()).map_err(|e| e.into())
}
pub fn diff(&self, b: &Index) -> (Vec<String>, Vec<String>) {
let mut changed_files: Vec<String> = Vec::new();
let mut deleted_files: Vec<String> = Vec::new();
for (path, hash) in &b.files {
if self.files.get(path).is_none() || self.files.get(path).unwrap() != hash {
changed_files.push(path.to_string());
}
}
for path in self.files.keys() {
if !b.files.contains_key(path) {
deleted_files.push(path.to_string());
}
}
(changed_files, deleted_files)
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn path(&self) -> PathBuf {
self.directory.clone()
}
pub fn files(&self) -> &HashMap<String, String> {
&self.files
}
pub fn update(&mut self, path: &str) -> Result<(), Box<dyn Error>> {
let bytes = fs::read(self.directory.join(path))?;
let mut hasher = sha1::Sha1::new();
hasher.update(bytes);
self.files
.insert(path.to_string(), format!("{:x}", hasher.finalize()));
Ok(())
}
pub fn remove(&mut self, path: &str) -> Result<(), Box<dyn Error>> {
self.files.remove(path);
Ok(())
}
}
impl<'a> std::ops::Index<&'a str> for Index {
type Output = String;
fn index(&self, index: &'a str) -> &Self::Output {
&self.files[index]
}
}
#[cfg(test)]
mod tests {
use std::fs;
use tempdir::TempDir;
use crate::index::{Index, IGNORE_FILE, INDEX_FILE};
#[test]
fn test_blank() {
let index = Index::blank("Tests");
assert_eq!(index.path().to_str().unwrap(), "Tests");
assert_eq!(index.len(), 0);
assert_eq!(index.is_empty(), true);
}
#[test]
fn test_load() {
let dir = TempDir::new("osync").expect("unable to create temp dir");
fs::write(
dir.path().join(INDEX_FILE),
"test:5d41402abc4b2a76b9719d911017c592",
)
.expect("unable to write index");
let index = Index::load(dir).expect("unable to load index");
assert_eq!(index.len(), 1);
assert_eq!(index["test"], "5d41402abc4b2a76b9719d911017c592");
}
#[test]
fn test_compute_no_files() {
let dir = TempDir::new("osync").expect("unable to create temp dir");
let (index, _) = Index::compute(&dir).expect("unable to compute index");
assert_eq!(index.len(), 0);
assert_eq!(index.is_empty(), true);
}
#[test]
fn test_compute_with_files() {
let dir = TempDir::new("osync").expect("unable to create temp dir");
fs::write(dir.path().join("test"), "hello").expect("unable to write test file");
let (index, _) = Index::compute(&dir).expect("unable to compute index");
assert_eq!(index.len(), 1);
assert_eq!(index.is_empty(), false);
assert_eq!(index["test"], "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
fs::write(dir.path().join(IGNORE_FILE), "test\n").expect("unable to write ignore file");
let (index, ignored) = Index::compute(&dir).expect("unable to compute index");
assert_eq!(index.len(), 0);
assert_eq!(ignored, 3); }
#[test]
fn test_diff() {
let dir = TempDir::new("osync").expect("unable to create temp dir");
fs::write(dir.path().join("test"), "hello").expect("unable to write test file");
fs::write(
dir.path().join(INDEX_FILE),
"test:aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d",
)
.expect("unable to write index");
let previous_index = Index::load(&dir).expect("unable to read index");
let (current_index, _) = Index::compute(&dir).expect("unable to compute index");
let (changed_files, deleted_files) = previous_index.diff(¤t_index);
assert_eq!(changed_files.len(), 0);
assert!(deleted_files.is_empty());
}
}