Files
Stalwart/crates/store/src/write/blob.rs
2026-03-12 18:26:35 +01:00

297 lines
9.1 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{BlobOp, Operation, ValueClass, ValueOp, key::DeserializeBigEndian, now};
use crate::{
BlobStore, Deserialize, IterateParams, SerializeInfallible, Store, U16_LEN, U32_LEN, U64_LEN,
ValueKey,
write::{BatchBuilder, BlobLink, RegistryClass},
};
use registry::{
schema::prelude::Property,
types::{EnumImpl, id::ObjectId},
};
use std::time::Instant;
use trc::{AddContext, StoreEvent};
use types::{
blob::BlobClass,
blob_hash::{BLOB_HASH_LEN, BlobHash},
};
#[derive(Debug, PartialEq, Eq)]
pub struct BlobQuota {
pub bytes: usize,
pub count: usize,
}
impl Store {
pub async fn blob_exists(&self, hash: impl AsRef<BlobHash> + Sync + Send) -> trc::Result<bool> {
self.key_exists(ValueKey {
account_id: 0,
collection: 0,
document_id: 0,
class: ValueClass::Blob(BlobOp::Commit {
hash: hash.as_ref().clone(),
}),
})
.await
.caused_by(trc::location!())
}
pub async fn blob_has_access(
&self,
hash: impl AsRef<BlobHash> + Sync + Send,
class: impl AsRef<BlobClass> + Sync + Send,
) -> trc::Result<bool> {
let key = match class.as_ref() {
BlobClass::Reserved {
account_id,
expires,
} if *expires > now() => ValueKey {
account_id: *account_id,
collection: 0,
document_id: 0,
class: ValueClass::Blob(BlobOp::Link {
hash: hash.as_ref().clone(),
to: BlobLink::Temporary { until: *expires },
}),
},
BlobClass::Linked {
account_id,
collection,
document_id,
} => ValueKey {
account_id: *account_id,
collection: *collection,
document_id: *document_id,
class: ValueClass::Blob(BlobOp::Link {
hash: hash.as_ref().clone(),
to: BlobLink::Document,
}),
},
_ => return Ok(false),
};
self.key_exists(key).await
}
pub async fn purge_blobs_all_shards(&self, blob_store: BlobStore) -> trc::Result<()> {
for shard_index in 0u8..=255 {
self.purge_blobs(blob_store.clone(), shard_index).await?;
}
Ok(())
}
pub async fn purge_blobs(&self, blob_store: BlobStore, shard_index: u8) -> trc::Result<()> {
let mut total_active = 0;
let mut total_deleted = 0;
let started = Instant::now();
// Validate linked blobs
let mut from_hash = BlobHash::default();
let mut to_hash = BlobHash::new_max();
from_hash.0[0] = shard_index;
to_hash.0[0] = shard_index;
let from_key = ValueKey {
account_id: 0,
collection: 0,
document_id: 0,
class: ValueClass::Blob(BlobOp::Commit { hash: from_hash }),
};
let to_key = ValueKey {
account_id: u32::MAX,
collection: u8::MAX,
document_id: u32::MAX,
class: ValueClass::Blob(BlobOp::Link {
hash: to_hash,
to: BlobLink::Document,
}),
};
let mut state = BlobPurgeState::new();
self.iterate(
IterateParams::new(from_key, to_key).ascending(),
|key, value| {
let hash =
BlobHash::try_from_hash_slice(key.get(0..BLOB_HASH_LEN).ok_or_else(|| {
trc::Error::corrupted_key(key, value.into(), trc::location!())
})?)
.unwrap();
state.update_hash(hash);
state.process_key(key, value)?;
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
state.finalize(BlobHash::default());
// Delete expired or unlinked blobs
for (_, op) in &state.delete_keys {
if let BlobOp::Commit { hash } = op {
blob_store
.delete_blob(hash.as_ref())
.await
.caused_by(trc::location!())?;
}
}
// Delete hashes
let mut batch = BatchBuilder::new();
for (account_id, op) in state.delete_keys {
if batch.is_large_batch() {
self.write(batch.build_all())
.await
.caused_by(trc::location!())?;
batch = BatchBuilder::new();
}
if let Some(account_id) = account_id {
batch.with_account_id(account_id);
}
batch.any_op(Operation::Value {
class: ValueClass::Blob(op),
op: ValueOp::Clear,
});
}
for (account_id, object_id) in state.delete_registry {
if batch.is_large_batch() {
self.write(batch.build_all())
.await
.caused_by(trc::location!())?;
batch = BatchBuilder::new();
}
let item_id = object_id.id().id();
let object_id = object_id.object().to_id();
batch
.clear(ValueClass::Registry(RegistryClass::Index {
index_id: Property::AccountId.to_id(),
object_id,
item_id,
key: (account_id as u64).serialize(),
}))
.clear(ValueClass::Registry(RegistryClass::Item {
object_id,
item_id,
}));
}
if !batch.is_empty() {
self.write(batch.build_all())
.await
.caused_by(trc::location!())?;
}
total_active += state.total_active - 1; // Exclude default hash
total_deleted += state.total_deleted;
trc::event!(
Store(StoreEvent::BlobStorePurged),
Id = shard_index as u16,
Expires = total_deleted,
Total = total_active,
Elapsed = started.elapsed()
);
Ok(())
}
}
struct BlobPurgeState {
last_hash: BlobHash,
last_hash_is_linked: bool,
delete_keys: Vec<(Option<u32>, BlobOp)>,
delete_registry: Vec<(u32, ObjectId)>,
now: u64,
total_deleted: u64,
total_active: u64,
}
impl BlobPurgeState {
fn new() -> Self {
Self {
last_hash: BlobHash::default(),
last_hash_is_linked: true, // Avoid deleting non-existing last_hash on first iteration
delete_keys: Vec::new(),
delete_registry: Vec::new(),
now: now(),
total_deleted: 0,
total_active: 0,
}
}
pub fn update_hash(&mut self, hash: BlobHash) {
if self.last_hash != hash {
self.finalize(hash);
self.last_hash_is_linked = false;
}
}
pub fn finalize(&mut self, new_hash: BlobHash) {
if !self.last_hash_is_linked {
self.total_deleted += 1;
self.delete_keys.push((
None,
BlobOp::Commit {
hash: std::mem::replace(&mut self.last_hash, new_hash),
},
));
} else {
self.total_active += 1;
self.last_hash = new_hash;
}
}
pub fn process_key(&mut self, key: &[u8], value: &[u8]) -> trc::Result<()> {
const TEMP_LINK: usize = BLOB_HASH_LEN + U32_LEN + U64_LEN;
const DOC_LINK: usize = BLOB_HASH_LEN + U64_LEN + 1;
const ID_LINK: usize = BLOB_HASH_LEN + U64_LEN;
match key.len() {
BLOB_HASH_LEN => {
// Main blob entry
Ok(())
}
TEMP_LINK => {
// Temporary link
let until = key.deserialize_be_u64(BLOB_HASH_LEN + U32_LEN)?;
if until <= self.now {
let account_id = key.deserialize_be_u32(BLOB_HASH_LEN)?;
self.delete_keys.push((
Some(account_id),
BlobOp::Link {
hash: self.last_hash.clone(),
to: BlobLink::Temporary { until },
},
));
if value.len() == U16_LEN + U64_LEN {
self.delete_registry
.push((account_id, ObjectId::deserialize(value)?));
}
} else {
self.last_hash_is_linked = true;
}
Ok(())
}
DOC_LINK | ID_LINK => {
// Document/Id link
self.last_hash_is_linked = true;
Ok(())
}
_ => Err(trc::Error::corrupted_key(
key,
value.into(),
trc::location!(),
)),
}
}
}