#![doc = include_str!("../examples/basic-files.rs")]
#![cfg_attr(feature = "async", doc = "```rust")]
#![cfg_attr(feature = "async", doc = include_str!("../examples/basic-files-async.rs"))]
#![cfg_attr(feature = "async", doc = "```")]
#![forbid(unsafe_code)]
#![warn(
clippy::cargo,
missing_docs,
// clippy::missing_docs_in_private_items,
clippy::pedantic,
future_incompatible,
rust_2018_idioms,
)]
#![allow(
clippy::missing_errors_doc, // TODO clippy::missing_errors_doc
clippy::option_if_let_else,
clippy::module_name_repetitions,
)]
use std::fmt::Debug;
use std::marker::PhantomData;
#[cfg(feature = "async")]
use bonsaidb_core::async_trait::async_trait;
#[cfg(feature = "async")]
use bonsaidb_core::connection::AsyncConnection;
use bonsaidb_core::connection::Connection;
use bonsaidb_core::key::time::TimestampAsNanoseconds;
use bonsaidb_core::schema::{
CollectionName, InsertError, Qualified, Schema, SchemaName, Schematic,
};
pub use bonsaidb_macros::FileConfig;
use derive_where::derive_where;
use serde::de::DeserializeOwned;
use serde::Serialize;
mod schema;
pub mod direct;
#[cfg_attr(feature = "async", async_trait)]
pub trait FileConfig: Sized + Send + Sync + Unpin + 'static {
type Metadata: Serialize + DeserializeOwned + Send + Sync + Debug + Clone;
const BLOCK_SIZE: usize;
fn files_name() -> CollectionName;
fn blocks_name() -> CollectionName;
fn register_collections(schema: &mut Schematic) -> Result<(), bonsaidb_core::Error> {
schema.define_collection::<schema::file::File<Self>>()?;
schema.define_collection::<schema::block::Block<Self>>()?;
Ok(())
}
fn build<NameOrPath: AsRef<str>>(name_or_path: NameOrPath) -> direct::FileBuilder<'static, Self>
where
Self::Metadata: Default,
{
direct::FileBuilder::new(name_or_path, <Self::Metadata as Default>::default())
}
fn build_with_metadata<NameOrPath: AsRef<str>>(
name_or_path: NameOrPath,
metadata: Self::Metadata,
) -> direct::FileBuilder<'static, Self> {
direct::FileBuilder::new(name_or_path, metadata)
}
fn get<Database: Connection + Clone>(
id: u32,
database: &Database,
) -> Result<Option<direct::File<direct::Blocking<Database>, Self>>, bonsaidb_core::Error> {
direct::File::<_, Self>::get(id, database)
}
fn load<Database: Connection + Clone>(
path: &str,
database: &Database,
) -> Result<Option<direct::File<direct::Blocking<Database>, Self>>, Error> {
direct::File::<_, Self>::load(path, database)
}
fn load_or_create<Database: Connection + Clone>(
path: &str,
expect_present: bool,
database: &Database,
) -> Result<direct::File<direct::Blocking<Database>, Self>, Error>
where
Self::Metadata: Default,
{
Self::load_or_create_with_metadata(
path,
<Self::Metadata as Default>::default(),
expect_present,
database,
)
}
fn load_or_create_with_metadata<Database: Connection + Clone>(
path: &str,
metadata: Self::Metadata,
expect_present: bool,
database: &Database,
) -> Result<direct::File<direct::Blocking<Database>, Self>, Error> {
if expect_present {
if let Some(file) = direct::File::<_, Self>::load(path, database)? {
return Ok(file);
}
}
match Self::build_with_metadata(path, metadata).create(database) {
Ok(file) => Ok(file),
Err(Error::AlreadyExists) => {
direct::File::<_, Self>::load(path, database)?.ok_or(Error::Deleted)
}
Err(other) => Err(other),
}
}
fn delete<Database: Connection + Clone>(
path: &str,
database: &Database,
) -> Result<bool, Error> {
if let Some(file) = direct::File::<_, Self>::load(path, database)? {
file.delete()?;
Ok(true)
} else {
Ok(false)
}
}
fn list<Database: Connection + Clone>(
path: &str,
database: &Database,
) -> Result<Vec<direct::File<direct::Blocking<Database>, Self>>, bonsaidb_core::Error> {
direct::File::<_, Self>::list(path, database)
}
fn list_recursive<Database: Connection + Clone>(
path: &str,
database: &Database,
) -> Result<Vec<direct::File<direct::Blocking<Database>, Self>>, bonsaidb_core::Error> {
direct::File::<_, Self>::list_recursive(path, database)
}
fn stats<Database: Connection + Clone>(
database: &Database,
) -> Result<Statistics, bonsaidb_core::Error> {
Self::stats_for_path("/", database)
}
fn stats_for_path<Database: Connection + Clone>(
path: &str,
database: &Database,
) -> Result<Statistics, bonsaidb_core::Error> {
direct::File::<_, Self>::stats_for_path(path, database)
}
#[cfg(feature = "async")]
async fn get_async<Database: AsyncConnection + Clone>(
id: u32,
database: &Database,
) -> Result<Option<direct::File<direct::Async<Database>, Self>>, bonsaidb_core::Error> {
direct::File::<_, Self>::get_async(id, database).await
}
#[cfg(feature = "async")]
async fn load_async<Database: AsyncConnection + Clone>(
path: &str,
database: &Database,
) -> Result<Option<direct::File<direct::Async<Database>, Self>>, Error> {
direct::File::<_, Self>::load_async(path, database).await
}
#[cfg(feature = "async")]
async fn load_or_create_async<Database: AsyncConnection + Clone>(
path: &str,
expect_present: bool,
database: &Database,
) -> Result<direct::File<direct::Async<Database>, Self>, Error>
where
Self::Metadata: Default,
{
Self::load_or_create_with_metadata_async(
path,
<Self::Metadata as Default>::default(),
expect_present,
database,
)
.await
}
#[cfg(feature = "async")]
async fn load_or_create_with_metadata_async<Database: AsyncConnection + Clone>(
path: &str,
metadata: Self::Metadata,
expect_present: bool,
database: &Database,
) -> Result<direct::File<direct::Async<Database>, Self>, Error> {
if expect_present {
if let Some(file) = direct::File::<_, Self>::load_async(path, database).await? {
return Ok(file);
}
}
match Self::build_with_metadata(path, metadata)
.create_async(database)
.await
{
Ok(file) => Ok(file),
Err(Error::AlreadyExists) => {
direct::File::<_, Self>::load_async(path, database)
.await?
.ok_or(Error::Deleted)
}
Err(other) => Err(other),
}
}
#[cfg(feature = "async")]
async fn delete_async<Database: AsyncConnection + Clone>(
path: &str,
database: &Database,
) -> Result<bool, Error> {
if let Some(file) = direct::File::<_, Self>::load_async(path, database).await? {
file.delete().await?;
Ok(true)
} else {
Ok(false)
}
}
#[cfg(feature = "async")]
async fn list_async<Database: AsyncConnection + Clone>(
path: &str,
database: &Database,
) -> Result<Vec<direct::File<direct::Async<Database>, Self>>, bonsaidb_core::Error> {
direct::File::<_, Self>::list_async(path, database).await
}
#[cfg(feature = "async")]
async fn list_recursive_async<Database: AsyncConnection + Clone>(
path: &str,
database: &Database,
) -> Result<Vec<direct::File<direct::Async<Database>, Self>>, bonsaidb_core::Error> {
direct::File::<_, Self>::list_recursive_async(path, database).await
}
#[cfg(feature = "async")]
async fn stats_async<Database: AsyncConnection + Clone>(
database: &Database,
) -> Result<Statistics, bonsaidb_core::Error> {
Self::stats_for_path_async("/", database).await
}
#[cfg(feature = "async")]
async fn stats_for_path_async<Database: AsyncConnection + Clone>(
path: &str,
database: &Database,
) -> Result<Statistics, bonsaidb_core::Error> {
direct::File::<_, Self>::stats_for_path_async(path, database).await
}
}
#[derive(Debug)]
pub struct BonsaiFiles;
impl FileConfig for BonsaiFiles {
type Metadata = Option<()>;
const BLOCK_SIZE: usize = 65_536;
fn files_name() -> CollectionName {
CollectionName::new("bonsaidb", "files")
}
fn blocks_name() -> CollectionName {
CollectionName::new("bonsaidb", "blocks")
}
}
#[derive_where(Default, Debug)]
pub struct FilesSchema<Config: FileConfig = BonsaiFiles>(PhantomData<Config>);
impl<Config: FileConfig> Schema for FilesSchema<Config> {
fn schema_name() -> SchemaName {
SchemaName::from(Config::files_name())
}
fn define_collections(schema: &mut Schematic) -> Result<(), bonsaidb_core::Error> {
Config::register_collections(schema)
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("database error: {0}")]
Database(bonsaidb_core::Error),
#[error("names must not contain '/'")]
InvalidName,
#[error("all paths must start with a leading '/'")]
InvalidPath,
#[error("a file already exists at the path provided")]
AlreadyExists,
#[error("the file was deleted during the operation")]
Deleted,
}
impl<T> From<InsertError<T>> for Error {
fn from(err: InsertError<T>) -> Self {
Self::from(err.error)
}
}
impl From<bonsaidb_core::Error> for Error {
fn from(err: bonsaidb_core::Error) -> Self {
match err {
bonsaidb_core::Error::UniqueKeyViolation { .. } => Self::AlreadyExists,
other => Self::Database(other),
}
}
}
impl From<Error> for bonsaidb_core::Error {
fn from(err: Error) -> Self {
match err {
Error::Database(err) => err,
other => Self::other("bonsaidb-files", other),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Truncate {
RemovingStart,
RemovingEnd,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct Statistics {
pub total_bytes: u64,
pub file_count: usize,
pub last_appended_at: Option<TimestampAsNanoseconds>,
}
#[cfg(test)]
mod tests;