Something went wrong. Try again.
A minimal atproto Personal Data Server, on SQLite. github.com/eth0net/manapds
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162//! Storage: the data directory and the SQLite files under it.//!//! The layout is the reference implementation's, down to the file names and//! the migration ledger, so that stopping that server and starting this one on//! the same directory is all a takeover takes. See `docs/roadmap.md` for what//! that constraint buys.
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use crate::syntax::Did;
/// What went wrong reading or writing storage.#[derive(Debug, thiserror::Error)]pub enum Error { /// SQLite refused, or the file underneath it did. #[error("sqlite: {0}")] Sqlite(#[from] rusqlite::Error), /// A directory could not be made or read. #[error("{0}")] Io(#[from] std::io::Error), /// A database holding something no schema this server knows would write. #[error("{0}")] Malformed(&'static str), /// A database migrated past what this server can read. #[error("schema is at {0}, which is newer than this server")] TooNew(String), /// An invite code that cannot be spent, refused inside the write that /// would have spent it. #[error("invite code is not available")] InviteUnavailable, /// A handle somebody else holds, refused the same way. The unique index is /// what settles a race the read before it cannot. #[error("handle is already taken")] HandleTaken, /// An email somebody else holds, refused by the index that pairs with it. #[error("email is already taken")] EmailTaken, /// An app password name the account already uses, refused by the key that /// pairs the two. #[error("app password name is already taken")] AppPasswordTaken, /// A blob whose bytes are not what its CID says. #[error("{0} is not the CID of the bytes under it")] WrongCid(ipld_core::cid::Cid), /// A key file holding something that is not a key. #[error("signing key: {0}")] Key(#[from] crate::crypto::Error),}
impl Error { /// Whether waiting and asking again is what would help, which is the one /// thing a caller can act on: SQLite takes one writer at a time. #[must_use] pub fn busy(&self) -> bool { matches!(self, Self::Sqlite(error) if matches!( error.sqlite_error_code(), Some(rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked) )) }}
mod accounts;mod actor;pub mod blobs;mod db;mod did_cache;pub mod keys;mod sequencer;
pub use accounts::{Account, Accounts, AppPassword, Registration, Session};pub use actor::{Actor, Backlink, Blob, Indexed, Record, Root};pub use blobs::Blobs;pub use did_cache::{Cached, DidCache};pub use sequencer::{Entry, Event, Sequencer};
/// Milliseconds and a `Z`, which is what the other server writes and what/// makes a text column sort as time. Every time these databases hold.#[must_use]pub fn stamp(at: jiff::Timestamp) -> String { format!("{at:.3}")}
/// Makes a directory nobody else can look into, which is what everything/// holding an account's data wants to be.fn directory(path: &Path) -> Result<(), std::io::Error> { let mut builder = std::fs::DirBuilder::new(); builder.recursive(true); #[cfg(unix)] { use std::os::unix::fs::DirBuilderExt; builder.mode(0o700); } builder.create(path)}
/// The data directory, and where each thing under it lives.#[derive(Clone, Debug)]pub struct Directory(PathBuf);
impl Directory { /// Names a directory without touching it. pub fn new(path: impl Into<PathBuf>) -> Self { Self(path.into()) }
/// Accounts, handles, passwords, tokens and invite codes. #[must_use] pub fn accounts(&self) -> PathBuf { self.0.join("account.sqlite") }
/// The outgoing event log. #[must_use] pub fn sequencer(&self) -> PathBuf { self.0.join("sequencer.sqlite") }
/// Resolved DID documents, held so the directory is not asked twice. #[must_use] pub fn did_cache(&self) -> PathBuf { self.0.join("did_cache.sqlite") }
/// One account's directory, under a shard so that no directory holds every /// account at once. #[must_use] pub fn actor(&self, did: &Did) -> PathBuf { let hash = Sha256::digest(did.as_str().as_bytes()); self.0 .join("actors") .join(format!("{:02x}", hash[0])) .join(did.as_str()) }
/// One account's repository, records, blobs and preferences. #[must_use] pub fn actor_store(&self, did: &Did) -> PathBuf { self.actor(did).join("store.sqlite") }
/// One account's signing key, as raw bytes. #[must_use] pub fn actor_key(&self, did: &Did) -> PathBuf { self.actor(did).join("key") }
/// Every account's blobs. Named for the blocks it does not hold, because /// that is what the deployment scripts already set. #[must_use] pub fn blobs(&self) -> Blobs { Blobs::new(self.0.join("blocks")) }}
impl AsRef<Path> for Directory { fn as_ref(&self) -> &Path { &self.0 }}