use super::scope::ScopeMatch;
#[derive(Debug, Clone)]
pub struct NewSource {
pub root_id: i64,
pub rel_path: String,
pub size: i64,
pub mtime: i64,
pub partial_hash: String,
pub object_id: Option<i64>,
pub device: Option<i64>,
pub inode: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct Source {
pub id: i64,
pub root_id: i64,
pub root_path: String,
pub rel_path: String,
pub object_id: Option<i64>,
pub size: i64,
pub mtime: i64,
pub excluded: bool,
pub object_excluded: Option<bool>,
#[allow(dead_code)]
pub device: i64,
#[allow(dead_code)]
pub inode: i64,
#[allow(dead_code)]
pub partial_hash: String,
pub basis_rev: i64,
pub root_role: String,
pub root_suspended: bool,
}
impl Source {
pub fn path(&self) -> String {
if self.rel_path.is_empty() {
self.root_path.clone()
} else {
format!("{}/{}", self.root_path, self.rel_path)
}
}
pub fn matches_scope(&self, scopes: &[ScopeMatch]) -> bool {
if scopes.is_empty() {
return true;
}
let full_path = self.path();
scopes.iter().any(|scope| match scope {
ScopeMatch::ExactFile(path) => full_path == *path,
ScopeMatch::UnderDirectory(dir) => {
full_path == *dir
|| (full_path.starts_with(dir)
&& full_path.as_bytes().get(dir.len()) == Some(&b'/'))
}
})
}
pub fn is_excluded(&self) -> bool {
self.excluded || self.object_excluded.unwrap_or(false)
}
pub fn is_from_role(&self, role: &str) -> bool {
self.root_role == role
}
pub fn is_active(&self) -> bool {
!self.root_suspended
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_source(root_path: &str, rel_path: &str) -> Source {
Source {
id: 1,
root_id: 1,
root_path: root_path.to_string(),
rel_path: rel_path.to_string(),
object_id: None,
size: 0,
mtime: 0,
excluded: false,
object_excluded: None,
device: 0,
inode: 0,
partial_hash: String::new(),
basis_rev: 0,
root_role: "source".to_string(),
root_suspended: false,
}
}
#[test]
fn path_combines_root_and_rel() {
let s = make_source("/home/user/photos", "2024/january/photo.jpg");
assert_eq!(s.path(), "/home/user/photos/2024/january/photo.jpg");
}
#[test]
fn path_handles_empty_rel_path() {
let s = make_source("/home/user/photos", "");
assert_eq!(s.path(), "/home/user/photos");
}
#[test]
fn path_handles_single_segment_rel() {
let s = make_source("/root", "file.txt");
assert_eq!(s.path(), "/root/file.txt");
}
#[test]
fn matches_scope_empty_scopes_matches_everything() {
let s = make_source("/any/path", "any/file.txt");
assert!(s.matches_scope(&[]));
}
#[test]
fn matches_scope_exact_file_match() {
let s = make_source("/home/user", "photos/photo.jpg");
let scopes = vec![ScopeMatch::ExactFile(
"/home/user/photos/photo.jpg".to_string(),
)];
assert!(s.matches_scope(&scopes));
}
#[test]
fn matches_scope_exact_file_no_match() {
let s = make_source("/home/user", "photos/other.jpg");
let scopes = vec![ScopeMatch::ExactFile(
"/home/user/photos/photo.jpg".to_string(),
)];
assert!(!s.matches_scope(&scopes));
}
#[test]
fn matches_scope_under_directory() {
let s = make_source("/home/user", "photos/2024/photo.jpg");
let scopes = vec![ScopeMatch::UnderDirectory("/home/user/photos".to_string())];
assert!(s.matches_scope(&scopes));
}
#[test]
fn matches_scope_directory_itself_matches() {
let s = make_source("/home/user", "photos");
let scopes = vec![ScopeMatch::UnderDirectory("/home/user/photos".to_string())];
assert!(s.matches_scope(&scopes));
}
#[test]
fn matches_scope_not_under_similar_prefix() {
let s = make_source("/a", "bc");
let scopes = vec![ScopeMatch::UnderDirectory("/a/b".to_string())];
assert!(!s.matches_scope(&scopes));
}
#[test]
fn matches_scope_not_under_similar_prefix_deeper() {
let s = make_source("/home/user", "photos-backup/file.jpg");
let scopes = vec![ScopeMatch::UnderDirectory("/home/user/photos".to_string())];
assert!(!s.matches_scope(&scopes));
}
#[test]
fn matches_scope_multiple_scopes_any_match() {
let s = make_source("/home/user", "documents/file.txt");
let scopes = vec![
ScopeMatch::UnderDirectory("/home/user/photos".to_string()),
ScopeMatch::UnderDirectory("/home/user/documents".to_string()),
ScopeMatch::ExactFile("/some/other/file.txt".to_string()),
];
assert!(s.matches_scope(&scopes));
}
#[test]
fn matches_scope_multiple_scopes_none_match() {
let s = make_source("/home/user", "videos/movie.mp4");
let scopes = vec![
ScopeMatch::UnderDirectory("/home/user/photos".to_string()),
ScopeMatch::UnderDirectory("/home/user/documents".to_string()),
];
assert!(!s.matches_scope(&scopes));
}
#[test]
fn is_excluded_source_not_excluded() {
let s = make_source("/root", "file.txt");
assert!(!s.is_excluded());
}
#[test]
fn is_excluded_source_level_exclusion() {
let mut s = make_source("/root", "file.txt");
s.excluded = true;
assert!(s.is_excluded());
}
#[test]
fn is_excluded_object_level_exclusion() {
let mut s = make_source("/root", "file.txt");
s.object_id = Some(42);
s.object_excluded = Some(true);
assert!(s.is_excluded());
}
#[test]
fn is_excluded_both_levels() {
let mut s = make_source("/root", "file.txt");
s.excluded = true;
s.object_id = Some(42);
s.object_excluded = Some(true);
assert!(s.is_excluded());
}
#[test]
fn is_excluded_object_not_excluded() {
let mut s = make_source("/root", "file.txt");
s.object_id = Some(42);
s.object_excluded = Some(false);
assert!(!s.is_excluded());
}
#[test]
fn is_excluded_no_object_not_excluded() {
let s = make_source("/root", "file.txt");
assert!(s.object_excluded.is_none());
assert!(!s.is_excluded());
}
#[test]
fn is_from_role_source() {
let s = make_source("/root", "file.txt");
assert!(s.is_from_role("source"));
assert!(!s.is_from_role("archive"));
}
#[test]
fn is_from_role_archive() {
let mut s = make_source("/root", "file.txt");
s.root_role = "archive".to_string();
assert!(s.is_from_role("archive"));
assert!(!s.is_from_role("source"));
}
#[test]
fn is_active_when_not_suspended() {
let s = make_source("/root", "file.txt");
assert!(s.is_active());
}
#[test]
fn is_active_when_suspended() {
let mut s = make_source("/root", "file.txt");
s.root_suspended = true;
assert!(!s.is_active());
}
}