use std::path::{Path, PathBuf};
use crate::{
common::{CvmfsError, CvmfsResult, canonicalize_path, split_md5},
database_object::DatabaseObject,
directory_entry::{DirectoryEntry, PathHash},
};
use chrono::{DateTime, Utc};
pub const CATALOG_ROOT_PREFIX: &str = "C";
const LISTING_QUERY: &str = "\
SELECT md5path_1, md5path_2, parent_1, parent_2, hash, flags, size, mode, mtime, name, symlink, uid, gid, hardlinks, xattr \
FROM catalog \
WHERE parent_1 = ? AND parent_2 = ? \
ORDER BY name ASC";
const NESTED_COUNT: &str = "SELECT count(*) FROM nested_catalogs;";
const READ_CHUNK: &str = "\
SELECT md5path_1, md5path_2, offset, size, hash \
FROM chunks \
WHERE md5path_1 = ? AND md5path_2 = ? \
ORDER BY offset ASC";
const FIND_MD5_PATH: &str = "SELECT md5path_1, md5path_2, parent_1, parent_2, hash, flags, size, mode, mtime, name, symlink, uid, gid, hardlinks, xattr \
FROM catalog \
WHERE md5path_1 = ? AND md5path_2 = ? \
LIMIT 1;";
const READ_STATISTICS: &str = "SELECT * FROM statistics ORDER BY counter;";
#[derive(Debug, Clone)]
pub struct CatalogReference {
pub root_path: String,
pub catalog_hash: String,
pub catalog_size: u32,
}
#[derive(Debug)]
pub struct Catalog {
pub database: DatabaseObject,
pub schema: f32,
pub schema_revision: f32,
pub revision: i32,
pub previous_revision: String,
pub hash: String,
pub last_modified: DateTime<Utc>,
pub root_prefix: String,
nested_cache: Option<Vec<CatalogReference>>,
}
#[derive(Debug, Default)]
pub struct Statistics {
pub chunked: i64,
pub chunked_size: i64,
pub chunks: i64,
pub dir: i64,
pub external: i64,
pub external_file_size: i64,
pub file_size: i64,
pub nested: i64,
pub regular: i64,
pub special: i64,
pub symlink: i64,
pub xattr: i64,
}
unsafe impl Sync for Catalog {}
impl Catalog {
pub fn new(path: String, hash: String) -> CvmfsResult<Self> {
let database = DatabaseObject::new(&path)?;
let properties = database.read_properties_table()?;
let mut revision = 0;
let mut previous_revision = String::new();
let mut schema = 0.0;
let mut schema_revision = 0.0;
let mut root_prefix = String::from("/");
let mut last_modified = Default::default();
for (key, value) in properties {
match key.as_str() {
"revision" => revision = value.parse().map_err(|_| CvmfsError::ParseError)?,
"schema" => schema = value.parse().map_err(|_| CvmfsError::ParseError)?,
"schema_revision" => {
schema_revision = value.parse().map_err(|_| CvmfsError::ParseError)?
}
"last_modified" => {
last_modified = DateTime::from_timestamp(
value.parse().map_err(|_| CvmfsError::ParseError)?,
0,
)
.ok_or(CvmfsError::InvalidTimestamp)?
}
"previous_revision" => previous_revision.push_str(&value),
"root_prefix" => {
root_prefix.clear();
root_prefix.push_str(&value)
}
_ => {}
}
}
if revision == 0 || schema == 0.0 {
return Err(CvmfsError::CatalogInitialization);
}
let mut catalog = Self {
database,
schema,
schema_revision,
revision,
hash,
last_modified,
root_prefix,
previous_revision,
nested_cache: None,
};
let nested = catalog.list_nested()?;
catalog.nested_cache = Some(nested);
Ok(catalog)
}
pub fn is_root(&self) -> bool {
self.root_prefix.eq("/")
}
pub fn has_nested(&self) -> CvmfsResult<bool> {
Ok(self.nested_count()? > 0)
}
pub fn nested_count(&self) -> CvmfsResult<u32> {
let mut result = self.database.create_prepared_statement(NESTED_COUNT)?;
let mut row = result.query([])?;
let next_row = row
.next()
.map_err(|e| CvmfsError::DatabaseError(format!("{:?}", e)))?
.ok_or(CvmfsError::DatabaseError("No rows found".to_string()))?;
Ok(next_row.get(0)?)
}
pub fn list_nested(&self) -> CvmfsResult<Vec<CatalogReference>> {
let new_version = self.schema <= 1.2 && self.schema_revision > 0.0;
let sql = if new_version {
"SELECT path, sha1, size FROM nested_catalogs"
} else {
"SELECT path, sha1 FROM nested_catalogs"
};
let mut result = self.database.create_prepared_statement(sql)?;
let iterator = result.query_map([], |row| {
Ok(CatalogReference {
root_path: row.get(0)?,
catalog_hash: row.get(1)?,
catalog_size: if new_version { row.get(2)? } else { 0 },
})
})?;
Ok(iterator.collect::<Result<Vec<_>, _>>()?)
}
pub(crate) fn path_sanitized(needle_path: &str, catalog_path: &str) -> bool {
needle_path.len() == catalog_path.len()
|| (needle_path.len() > catalog_path.len()
&& needle_path.as_bytes()[catalog_path.len()] == b'/')
}
pub fn find_nested_for_path(&self, needle_path: &str) -> CvmfsResult<Option<CatalogReference>> {
let catalog_refs = match &self.nested_cache {
Some(cached) => cached.clone(),
None => self.list_nested()?,
};
let mut best_match = None;
let mut best_match_score = 0;
let real_needle_path = canonicalize_path(needle_path);
for nested_catalog in catalog_refs {
if real_needle_path.starts_with(&nested_catalog.root_path)
&& nested_catalog.root_path.len() > best_match_score
&& Self::path_sanitized(needle_path, &nested_catalog.root_path)
{
best_match_score = nested_catalog.root_path.len();
best_match = Some(nested_catalog);
}
}
Ok(best_match)
}
pub fn list_directory_split_md5(
&self,
parent_1: i64,
parent_2: i64,
) -> CvmfsResult<Vec<DirectoryEntry>> {
let mut statement = self.database.create_prepared_statement(LISTING_QUERY)?;
let mut result = Vec::new();
let mut rows = statement.query([parent_1, parent_2])?;
loop {
match rows.next() {
Ok(row) => {
if let Some(row) = row {
result.push(DirectoryEntry::new(row)?);
} else {
break;
}
}
Err(e) => return Err(e.into()),
}
}
Ok(result)
}
pub fn list_directory(&self, path: &str) -> CvmfsResult<Vec<DirectoryEntry>> {
let mut real_path = canonicalize_path(path);
if real_path.eq(Path::new("/")) {
real_path = PathBuf::new();
}
let md5_hash = md5::compute(
real_path.to_str().ok_or(CvmfsError::FileNotFound)?.bytes().collect::<Vec<u8>>(),
);
let parent_hash = split_md5(&md5_hash.0);
self.list_directory_split_md5(parent_hash.hash1, parent_hash.hash2)
}
pub fn get_statistics(&self) -> CvmfsResult<Statistics> {
let mut statement = self.database.create_prepared_statement(READ_STATISTICS)?;
let mut rows = statement.query([])?;
let mut statistics = Statistics::default();
while let Some(row) = rows.next()? {
let name: String = row.get(0)?;
match name.as_str() {
"self_chunked" => statistics.chunked = row.get(1)?,
"self_chunked_size" => statistics.chunked_size = row.get(1)?,
"self_chunks" => statistics.chunks = row.get(1)?,
"self_dir" => statistics.dir = row.get(1)?,
"self_external" => statistics.external = row.get(1)?,
"self_external_file_size" => statistics.external_file_size = row.get(1)?,
"self_file_size" => statistics.file_size = row.get(1)?,
"self_nested" => statistics.nested = row.get(1)?,
"self_regular" => statistics.regular = row.get(1)?,
"self_special" => statistics.special = row.get(1)?,
"self_symlink" => statistics.symlink = row.get(1)?,
"self_xattr" => statistics.xattr = row.get(1)?,
_ => {}
}
}
Ok(statistics)
}
fn read_chunks(&self, directory_entry: &mut DirectoryEntry) -> CvmfsResult<()> {
let mut statement = self.database.create_prepared_statement(READ_CHUNK)?;
let path_hash = directory_entry.path_hash();
let iterator = statement.query([path_hash.hash1, path_hash.hash2])?;
directory_entry.add_chunks(iterator)?;
Ok(())
}
pub fn find_directory_entry(&self, root_path: &str) -> CvmfsResult<DirectoryEntry> {
let real_path = canonicalize_path(root_path);
let md5_path = md5::compute(
real_path.to_str().ok_or(CvmfsError::FileNotFound)?.bytes().collect::<Vec<u8>>(),
)
.0;
self.find_directory_entry_md5(&md5_path)
}
pub fn find_directory_entry_md5(&self, md5_path: &[u8; 16]) -> CvmfsResult<DirectoryEntry> {
let path_hash = split_md5(md5_path);
self.find_directory_entry_split_md5(path_hash)
}
fn find_directory_entry_split_md5(&self, path_hash: PathHash) -> CvmfsResult<DirectoryEntry> {
let mut statement = self.database.create_prepared_statement(FIND_MD5_PATH)?;
let mut rows = statement.query([path_hash.hash1, path_hash.hash2])?;
let row = rows.next()?.ok_or(CvmfsError::FileNotFound)?;
DirectoryEntry::new(row)
}
pub fn load_chunks(&self, entry: &mut DirectoryEntry) -> CvmfsResult<()> {
self.read_chunks(entry)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_sanitized_equal_paths() {
assert!(Catalog::path_sanitized("/a/b", "/a/b"));
}
#[test]
fn path_sanitized_needle_longer_with_slash() {
assert!(Catalog::path_sanitized("/a/b/c", "/a/b"));
}
#[test]
fn path_sanitized_needle_longer_without_slash() {
assert!(!Catalog::path_sanitized("/a/bc", "/a/b"));
}
#[test]
fn path_sanitized_needle_shorter() {
assert!(!Catalog::path_sanitized("/a", "/a/b"));
}
#[test]
fn path_sanitized_root_paths() {
assert!(Catalog::path_sanitized("/", "/"));
}
#[test]
fn path_sanitized_empty_strings() {
assert!(Catalog::path_sanitized("", ""));
}
#[test]
fn path_sanitized_needle_one_char_longer_with_slash() {
assert!(Catalog::path_sanitized("/a/", "/a"));
}
}