use std::{
borrow::Cow,
fmt::{self, Debug, Display},
str::FromStr,
time::SystemTime,
};
use rusqlite::ToSql;
use uuid::Uuid;
use crate::{
block::FileId as StoreFileId, errors::InternalError, hash::MerkleHash, sql::SqlStoreGuard,
};
pub const DEFAULT_ROOT_NAME: &str = "default";
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct FilesystemId {
filesystem_id: Uuid,
}
impl FilesystemId {
pub(crate) fn new() -> Self {
Self {
filesystem_id: Uuid::new_v4(),
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for FilesystemId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("FilesystemId")
.field(&self.filesystem_id.to_string())
.finish()
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl Display for FilesystemId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.filesystem_id)
}
}
impl FromStr for FilesystemId {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let filesystem_id = Uuid::parse_str(s).map_err(|_| InternalError::InvalidUuid {
uuid: s.to_string(),
})?;
Ok(Self { filesystem_id })
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct RootId {
root_id: Uuid,
}
impl RootId {
pub(crate) fn new() -> Self {
Self {
root_id: Uuid::new_v4(),
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for RootId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("RootId")
.field(&self.root_id.to_string())
.finish()
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl Display for RootId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.root_id)
}
}
impl FromStr for RootId {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let root_id = Uuid::parse_str(s).map_err(|_| InternalError::InvalidUuid {
uuid: s.to_string(),
})?;
Ok(Self { root_id })
}
}
#[doc(hidden)]
impl ToSql for RootId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::from(self.root_id.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Root {
pub id: RootId,
pub name: Option<String>,
pub created: SystemTime,
}
#[derive(Debug, Default, Clone)]
pub enum RootBy<'a> {
#[default]
Default,
Id(RootId),
Name(Cow<'a, str>),
}
impl PartialEq for RootBy<'_> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(RootBy::Default, RootBy::Default) => true,
(RootBy::Id(a), RootBy::Id(b)) => a == b,
(RootBy::Name(a), RootBy::Name(b)) => a == b,
(RootBy::Default, RootBy::Name(name)) | (RootBy::Name(name), RootBy::Default) => {
name == DEFAULT_ROOT_NAME
}
_ => false,
}
}
}
impl Eq for RootBy<'_> {}
#[cfg_attr(coverage_nightly, coverage(off))]
impl Display for RootBy<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RootBy::Default => write!(f, "{}", DEFAULT_ROOT_NAME),
RootBy::Id(root_id) => write!(f, "{}", root_id),
RootBy::Name(name) => write!(f, "{}", name),
}
}
}
impl From<RootId> for RootBy<'_> {
fn from(root_id: RootId) -> Self {
Self::Id(root_id)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl From<String> for RootBy<'_> {
fn from(name: String) -> Self {
RootBy::from(Cow::Owned(name))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl<'a> From<&'a str> for RootBy<'a> {
fn from(name: &'a str) -> Self {
RootBy::from(Cow::Borrowed(name))
}
}
impl<'a> From<Cow<'a, str>> for RootBy<'a> {
fn from(name: Cow<'a, str>) -> Self {
if name == DEFAULT_ROOT_NAME {
Self::Default
} else {
Self::Name(name)
}
}
}
const ROOT_PAGE_SIZE: u32 = 100;
#[derive(Debug)]
pub struct Roots<'conn, 'fs> {
store: &'fs mut SqlStoreGuard<'conn>,
page: Vec<Root>,
offset: u32,
limit: u32,
}
impl<'conn, 'fs> Roots<'conn, 'fs> {
pub(crate) fn new(store: &'fs mut SqlStoreGuard<'conn>) -> Self {
Self {
store,
page: Vec::new(),
offset: 0,
limit: ROOT_PAGE_SIZE,
}
}
}
impl<'conn, 'fs> Iterator for Roots<'conn, 'fs> {
type Item = crate::Result<Root>;
fn next(&mut self) -> Option<Self::Item> {
match self.page.pop() {
Some(root) => Some(Ok(root)),
None => {
match self
.store
.exec(|store| store.get_roots_page(self.limit, self.offset))
{
Ok(new_page) => {
if new_page.is_empty() {
return None;
}
self.page = new_page;
self.offset += self.page.len() as u32;
self.page.pop().map(Ok)
}
Err(err) => Some(Err(err)),
}
}
}
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ContentId {
merkle_hash: MerkleHash,
filesystem_id: FilesystemId,
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for ContentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("ContentId")
.field(&self.merkle_hash.to_string())
.finish()
}
}
impl ContentId {
pub(crate) fn new(merkle_hash: MerkleHash, filesystem_id: FilesystemId) -> Self {
Self {
merkle_hash,
filesystem_id,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileId {
file_id: crate::block::FileId,
filesystem_id: FilesystemId,
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl Debug for FileId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", i64::from(self.file_id))
}
}
impl FileId {
pub(crate) fn new(file_id: StoreFileId, filesystem_id: FilesystemId) -> Self {
Self {
file_id,
filesystem_id,
}
}
}
#[doc(hidden)]
impl ToSql for FileId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
self.file_id.to_sql()
}
}
#[cfg(test)]
mod tests {
use super::*;
use xpct::{be_err, be_ok, equal, expect};
#[test]
fn default_string_becomes_default_enum_variant() {
let default_root: RootBy = DEFAULT_ROOT_NAME.into();
expect!(default_root).to(equal(RootBy::Default));
let not_default_root: RootBy = "not-default".into();
expect!(not_default_root).to(equal(RootBy::Name("not-default".into())));
}
#[test]
fn root_by_equality() {
let root_id = RootId::new();
expect!(RootBy::Default).to(equal(RootBy::Default));
expect!(RootBy::Name("foo".into())).to(equal(RootBy::Name("foo".into())));
expect!(RootBy::Id(root_id)).to(equal(RootBy::Id(root_id)));
expect!(RootBy::Default).to(equal(RootBy::Name(DEFAULT_ROOT_NAME.into())));
expect!(RootBy::Default).to_not(equal(RootBy::Id(root_id)));
expect!(RootBy::Default).to_not(equal(RootBy::Name("not-default".into())));
expect!(RootBy::Name("foo".into())).to_not(equal(RootBy::Name("bar".into())));
expect!(RootBy::Id(root_id)).to_not(equal(RootBy::Id(RootId::new())));
}
#[test]
fn parse_filesystem_id() {
let uuid = "1ca7ac38-24a4-46e8-ae39-b45290be662c";
expect!(FilesystemId::from_str(uuid))
.to(be_ok())
.map(|id| id.to_string())
.to(equal(uuid));
let invalid_filesystem_id_str = "not-a-uuid";
expect!(FilesystemId::from_str(invalid_filesystem_id_str)).to(be_err());
}
}