selene-core 0.3.1

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{fs, ops::Deref, thread, time::Duration};

use lunar_lib::trace;
use serde::{Serialize, de::DeserializeOwned};
use sled::{CompareAndSwapError, Db, IVec, Tree, transaction::TransactionError};

use crate::{data_dir, database::validator::DatabaseReferenceError};

pub type Result<T> = std::result::Result<T, DatabaseError>;

pub mod writer;

mod sled_ops;
pub(crate) use sled_ops::*;

mod database_entry;
pub use database_entry::*;

mod patchable;
pub use patchable::*;

mod mergeable;
pub use mergeable::*;

mod createable;
pub use createable::*;

mod deleteable;
pub use deleteable::*;

mod transaction_args;
pub use transaction_args::*;

pub mod validator;

#[derive(thiserror::Error, Debug)]
pub enum DatabaseError {
    #[error("A key was not found in the database when it was expected")]
    MissingEntry,

    #[error("A key was found in the database when it was not expected")]
    AlreadyInDatabase,

    #[error("More than one key was found in the database when only one was expected")]
    TooManyMatches,

    #[error("A record was out of date. v{0} was expected but v{1} was found")]
    OutdatedVesion(u32, u32),

    #[error("Ciborium serialize error: {0}")]
    CiboriumSer(#[from] ciborium::ser::Error<std::io::Error>),

    #[error("Ciborium deserialize error: {0}")]
    CiboriumDe(#[from] ciborium::de::Error<std::io::Error>),

    #[error("Sled error: {0}")]
    Sled(#[from] sled::Error),

    #[error("{0}")]
    CompareAndSwapError(#[from] CompareAndSwapError),

    #[error("Invalid input: {0}")]
    InvalidInput(String),

    #[error("IoError: {0}")]
    Io(#[from] std::io::Error),

    #[error("A CAS Transaction was retried too many times")]
    TooManyRetries,

    #[error("{0}")]
    ValidationError(#[from] DatabaseReferenceError),
}

impl<E> From<TransactionError<E>> for DatabaseError
where
    E: Into<DatabaseError>,
{
    fn from(value: TransactionError<E>) -> Self {
        match value {
            TransactionError::Abort(err) => err.into(),
            TransactionError::Storage(error) => error.into(),
        }
    }
}

/// Open the library database
///
/// # Warning
///
/// This function locks the database, preventing anything else, including the daemon, from reading or read. Do not hold the database if you do not need to
pub fn library_db() -> Db {
    trace!("Trying to aquire database");
    let database_dir = data_dir().join("library_data");

    loop {
        match sled::open(&database_dir) {
            Ok(db) => return db,
            Err(sled::Error::Io(err))
                if matches!(err.kind(), std::io::ErrorKind::Other)
                    && err.to_string().contains("could not acquire lock on ") =>
            {
                trace!("Waiting to aquire database");
                thread::sleep(Duration::from_millis(500));
                continue;
            }
            Err(err) => panic!("Failed to open database: {err:#?}"),
        }
    }
}

pub(crate) fn track_tree(db: &Db) -> Tree {
    db.open_tree("track").expect("Failed to open 'track' tree")
}
pub(crate) fn album_tree(db: &Db) -> Tree {
    db.open_tree("album").expect("Failed to open 'album' tree")
}
pub(crate) fn artist_tree(db: &Db) -> Tree {
    db.open_tree("artist")
        .expect("Failed to open 'artist' tree")
}
pub(crate) fn collection_tree(db: &Db) -> Tree {
    db.open_tree("collection")
        .expect("Failed to open 'collection' tree")
}

/// Attempts to delete the entire contents of the database
///
/// This function deletes all contents of the database by replacing the existing database entry with [`None`], then deleting the database directory. This will create a new database the next time it is accessed
/// This function should only be used if you know that there are no functions currently trying to read, write, are holding references to the database, like a [`Tree`]
pub fn drop_db() -> sled::Result<()> {
    library_db().flush()?;

    let database_dir = data_dir().join("library_data");

    if let Err(err) = fs::remove_dir_all(&database_dir)
        && !matches!(err.kind(), std::io::ErrorKind::NotFound)
    {
        return Err(err.into());
    };

    Ok(())
}

pub fn db_flush() -> sled::Result<()> {
    library_db().flush()?;
    Ok(())
}

pub(crate) fn deserialize_from_ivec<T: DeserializeOwned>(raw: IVec) -> T {
    ciborium::from_reader(&raw[..]).expect("Ciborium failed to deserialize bytes. This can only happen if bytes were deserialized correctly, or out-of-date bytes were fetched")
}

pub(crate) fn serialize_to_ivec<T: Serialize>(item: &T) -> IVec {
    let mut buf = Vec::new();
    ciborium::into_writer(item, &mut buf)
        .expect("Ciborium failed to serialize. This cannot happen unless a serializer failed");
    IVec::from(buf)
}

pub trait EntryId: Deref<Target = blake3::Hash> + Copy + Eq {
    type Entry: DatabaseEntry;

    fn as_bytes(&self) -> &[u8; 32] {
        self.deref().as_bytes()
    }
}