Everything clients, users and operators meet now carries the fork's name, with no aliases (SPEC.md §2.4, changed here from "protocol identifiers stay"): - JMAP: upstream's registry capability is urn:inbuxa:jmap:registry, beside the fork's own urn:inbuxa:jmap. - WebDAV lock and sync tokens are urn:inbuxa:dav*; clients resync once. - Sieve: vnd.inbuxa.while and vnd.inbuxa.expressions. sieve-rs spells these into its compiler, so it's vendored (vendor/sieve-rs, 0.7.3) and patched in; a unit test fails if Cargo.lock ever moves past the vendored copy. The trusted runtime now names itself too, rather than answering sieve-rs's default. - The web interface's OAuth client is inbuxa-webui. On every start the old stalwart-webui client is removed and any application naming it is moved over. - The spam filter's blobs are INBUXA_SPAM_*; every start moves any left under the old keys, so a trained model survives. - SQL stores and log files default to inbuxa, in the code and in the schema served to the admin (checksum regenerated). - Settings are INBUXA_* only. A STALWART_* variable that's set where its INBUXA_* one isn't stops the server at startup, naming it. - The version-upgrade messages link docs.inbuxa.org's migration page, and the OpenAPI description, smtp crate metadata and web-push test fixtures lose the name. Kept on purpose, allowlisted with reasons: the OAuth key-derivation contexts (renaming them would end every session and invalidate every sealed client id) and the hashed application prefix. Also fixes a latent start-up failure: ensure_client updated an existing first-party client with a revision of 0, which the registry's assertion never matches, so adding a redirect URI or changing the webmail secret failed start-up. And the principal session test now expects legacyProtocols (C-1, added 2026-09-21), which it had missed. Tested: the server builds without warnings; common's 106 unit tests, including the vendoring check; a new integration test for the two start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
895 lines
30 KiB
Rust
895 lines
30 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
*/
|
|
|
|
use super::ETag;
|
|
use super::uri::{DavUriResource, OwnedUri, UriResource, Urn};
|
|
use crate::{DavError, DavErrorCondition, DavMethod};
|
|
use common::KV_LOCK_DAV;
|
|
use common::{Server, auth::AccessToken};
|
|
use dav_proto::schema::property::{ActiveLock, LockScope, WebDavProperty};
|
|
use dav_proto::schema::request::DavPropertyValue;
|
|
use dav_proto::schema::response::{BaseCondition, List, PropResponse};
|
|
use dav_proto::{Condition, Depth, Timeout};
|
|
use dav_proto::{RequestHeaders, schema::request::LockInfo};
|
|
use groupware::cache::GroupwareCache;
|
|
use http_proto::HttpResponse;
|
|
use hyper::StatusCode;
|
|
use std::collections::HashMap;
|
|
use store::ValueKey;
|
|
use store::dispatch::lookup::KeyValue;
|
|
use store::write::serialize::rkyv_deserialize;
|
|
use store::write::{AlignedBytes, Archive, Archiver, now};
|
|
use store::{Serialize, U32_LEN};
|
|
use trc::AddContext;
|
|
use types::collection::Collection;
|
|
use types::dead_property::DeadProperty;
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct ResourceState<'x> {
|
|
pub account_id: u32,
|
|
pub collection: Collection,
|
|
pub document_id: Option<u32>,
|
|
pub etag: Option<String>,
|
|
pub lock_tokens: Vec<String>,
|
|
pub sync_token: Option<String>,
|
|
pub path: &'x str,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
|
pub(crate) struct LockData {
|
|
locks: HashMap<String, LockItems>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
|
#[repr(transparent)]
|
|
pub(crate) struct LockItems(Vec<LockItem>);
|
|
|
|
#[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
|
pub(crate) struct LockItem {
|
|
lock_id: u64,
|
|
owner: u32,
|
|
expires: u64,
|
|
depth_infinity: bool,
|
|
exclusive: bool,
|
|
owner_dav: Option<DeadProperty>,
|
|
}
|
|
|
|
struct LockCache<'x> {
|
|
account_id: u32,
|
|
collection: Collection,
|
|
lock_archive: LockArchive<'x>,
|
|
}
|
|
|
|
enum LockArchive<'x> {
|
|
Unarchived(&'x ArchivedLockData),
|
|
Archived(Archive<AlignedBytes>),
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct LockCaches<'x> {
|
|
caches: Vec<LockCache<'x>>,
|
|
}
|
|
|
|
pub(crate) trait LockRequestHandler: Sync + Send {
|
|
fn handle_lock_request(
|
|
&self,
|
|
access_token: &AccessToken,
|
|
headers: &RequestHeaders<'_>,
|
|
lock_info: LockRequest,
|
|
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
|
|
|
|
fn validate_headers(
|
|
&self,
|
|
access_token: &AccessToken,
|
|
headers: &RequestHeaders<'_>,
|
|
resources: Vec<ResourceState<'_>>,
|
|
locks: LockCaches<'_>,
|
|
method: DavMethod,
|
|
) -> impl Future<Output = crate::Result<()>> + Send;
|
|
}
|
|
|
|
pub(crate) enum LockRequest {
|
|
Lock(LockInfo),
|
|
Unlock,
|
|
Refresh,
|
|
}
|
|
|
|
impl LockRequestHandler for Server {
|
|
async fn handle_lock_request(
|
|
&self,
|
|
access_token: &AccessToken,
|
|
headers: &RequestHeaders<'_>,
|
|
lock_info: LockRequest,
|
|
) -> crate::Result<HttpResponse> {
|
|
let resource = self
|
|
.validate_uri(access_token, headers.uri)
|
|
.await?
|
|
.into_owned_uri()?;
|
|
let resource_hash = resource.lock_key();
|
|
let resource_path = resource
|
|
.resource
|
|
.ok_or(DavError::Code(StatusCode::CONFLICT))?;
|
|
let account_id = resource.account_id;
|
|
if !access_token.is_member(account_id) {
|
|
return Err(DavError::Code(StatusCode::FORBIDDEN));
|
|
}
|
|
|
|
let resources = vec![ResourceState {
|
|
account_id,
|
|
collection: resource.collection,
|
|
path: resource_path,
|
|
..Default::default()
|
|
}];
|
|
|
|
let mut base_path = None;
|
|
let is_lock_request = !matches!(lock_info, LockRequest::Unlock);
|
|
let if_lock_token = headers
|
|
.if_
|
|
.iter()
|
|
.flat_map(|if_| if_.list.iter())
|
|
.find_map(|cond| {
|
|
if let Condition::StateToken { token, .. } = cond {
|
|
Urn::parse(token).and_then(|u| u.try_unwrap_lock())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or_default();
|
|
let mut lock_data = if let Some(lock_data) = self
|
|
.in_memory_store()
|
|
.key_get::<Archive<AlignedBytes>>(resource_hash.as_slice())
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
{
|
|
let lock_data = lock_data
|
|
.unarchive::<LockData>()
|
|
.caused_by(trc::location!())?;
|
|
|
|
self.validate_headers(
|
|
access_token,
|
|
headers,
|
|
resources,
|
|
LockCaches::new_shared(account_id, resource.collection, lock_data),
|
|
if is_lock_request {
|
|
DavMethod::LOCK
|
|
} else {
|
|
DavMethod::UNLOCK
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
if let LockRequest::Lock(lock_info) = &lock_info {
|
|
let mut failed_locks = Vec::new();
|
|
let is_exclusive = matches!(lock_info.lock_scope, LockScope::Exclusive);
|
|
let is_infinity = matches!(headers.depth, Depth::Infinity);
|
|
|
|
for (lock_path, lock_item) in lock_data.find_locks(resource_path, true) {
|
|
if if_lock_token != lock_item.lock_id
|
|
&& (lock_item.exclusive || is_exclusive)
|
|
&& (lock_path.len() == resource_path.len()
|
|
|| lock_item.depth_infinity && resource_path.len() > lock_path.len()
|
|
|| is_infinity && lock_path.len() > resource_path.len())
|
|
{
|
|
let base_path =
|
|
base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default());
|
|
failed_locks.push(format!("{base_path}/{lock_path}").into());
|
|
}
|
|
}
|
|
|
|
if !failed_locks.is_empty() {
|
|
return Err(DavErrorCondition::new(
|
|
StatusCode::LOCKED,
|
|
BaseCondition::LockTokenSubmitted(List(failed_locks)),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
// Validate lock_info
|
|
if lock_info.owner.as_ref().is_some_and(|o| {
|
|
o.size() > self.core.groupware.dead_property_size.unwrap_or(512)
|
|
}) {
|
|
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
|
|
}
|
|
|
|
if self.core.groupware.max_locks_per_user > 0
|
|
&& lock_data
|
|
.locks
|
|
.values()
|
|
.flat_map(|locks| {
|
|
locks
|
|
.0
|
|
.iter()
|
|
.filter(|lock| lock.owner == access_token.account_id())
|
|
})
|
|
.count()
|
|
>= self.core.groupware.max_locks_per_user
|
|
{
|
|
return Err(DavError::Code(StatusCode::TOO_MANY_REQUESTS));
|
|
}
|
|
}
|
|
|
|
rkyv_deserialize(lock_data).caused_by(trc::location!())?
|
|
} else if is_lock_request {
|
|
self.validate_headers(
|
|
access_token,
|
|
headers,
|
|
resources,
|
|
Default::default(),
|
|
DavMethod::LOCK,
|
|
)
|
|
.await?;
|
|
|
|
LockData::default()
|
|
} else {
|
|
return Err(DavErrorCondition::new(
|
|
StatusCode::CONFLICT,
|
|
BaseCondition::LockTokenMatchesRequestUri,
|
|
)
|
|
.into());
|
|
};
|
|
|
|
let now = now();
|
|
let response = if is_lock_request {
|
|
let timeout = if let Timeout::Second(seconds) = headers.timeout {
|
|
std::cmp::min(seconds, self.core.groupware.max_lock_timeout)
|
|
} else {
|
|
self.core.groupware.max_lock_timeout
|
|
};
|
|
let expires = now + timeout;
|
|
|
|
let lock_item = if if_lock_token > 0 {
|
|
if let Some(lock_item) = lock_data
|
|
.locks
|
|
.values_mut()
|
|
.flat_map(|locks| locks.0.iter_mut())
|
|
.find(|lock| lock.lock_id == if_lock_token)
|
|
{
|
|
lock_item
|
|
} else {
|
|
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
|
|
}
|
|
} else {
|
|
let locks = lock_data
|
|
.locks
|
|
.entry(resource_path.to_string())
|
|
.or_insert_with(Default::default);
|
|
locks.0.push(LockItem::default());
|
|
locks.0.last_mut().unwrap()
|
|
};
|
|
|
|
lock_item.expires = expires;
|
|
if let LockRequest::Lock(lock_info) = lock_info {
|
|
// Validate lock_info
|
|
if lock_info.owner.as_ref().is_some_and(|o| {
|
|
o.size() > self.core.groupware.dead_property_size.unwrap_or(512)
|
|
}) {
|
|
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
|
|
}
|
|
|
|
lock_item.lock_id = store::rand::random::<u64>() ^ expires;
|
|
lock_item.owner = access_token.account_id();
|
|
lock_item.depth_infinity = matches!(headers.depth, Depth::Infinity);
|
|
lock_item.owner_dav = lock_info.owner;
|
|
lock_item.exclusive = matches!(lock_info.lock_scope, LockScope::Exclusive);
|
|
}
|
|
|
|
let base_path = base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default());
|
|
let active_lock = lock_item.to_active_lock(format!("{base_path}/{resource_path}"));
|
|
|
|
HttpResponse::new(if if_lock_token == 0 {
|
|
StatusCode::CREATED
|
|
} else {
|
|
StatusCode::OK
|
|
})
|
|
.with_lock_token(&active_lock.lock_token.as_ref().unwrap().0)
|
|
.with_xml_body(
|
|
PropResponse::new(vec![DavPropertyValue::new(
|
|
WebDavProperty::LockDiscovery,
|
|
vec![active_lock],
|
|
)])
|
|
.to_string(),
|
|
)
|
|
} else {
|
|
let lock_id = headers
|
|
.lock_token
|
|
.and_then(Urn::parse)
|
|
.and_then(|urn| urn.try_unwrap_lock())
|
|
.ok_or(DavError::Code(StatusCode::BAD_REQUEST))?;
|
|
|
|
if lock_data.remove_lock(lock_id) {
|
|
HttpResponse::new(StatusCode::NO_CONTENT)
|
|
} else {
|
|
return Err(DavErrorCondition::new(
|
|
StatusCode::CONFLICT,
|
|
BaseCondition::LockTokenMatchesRequestUri,
|
|
)
|
|
.into());
|
|
}
|
|
};
|
|
|
|
// Remove expired locks
|
|
let max_expire = lock_data.remove_expired();
|
|
if max_expire > 0 {
|
|
self.in_memory_store()
|
|
.key_set(
|
|
KeyValue::new(
|
|
resource_hash,
|
|
Archiver::new(lock_data)
|
|
.untrusted()
|
|
.serialize()
|
|
.caused_by(trc::location!())?,
|
|
)
|
|
.expires(max_expire),
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
} else {
|
|
self.in_memory_store()
|
|
.key_delete(resource_hash)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
async fn validate_headers(
|
|
&self,
|
|
access_token: &AccessToken,
|
|
headers: &RequestHeaders<'_>,
|
|
mut resources: Vec<ResourceState<'_>>,
|
|
mut locks_: LockCaches<'_>,
|
|
method: DavMethod,
|
|
) -> crate::Result<()> {
|
|
let no_if_headers = headers.if_.is_empty();
|
|
match method {
|
|
DavMethod::GET | DavMethod::HEAD if no_if_headers => {
|
|
// Return early for GET/HEAD requests without If headers
|
|
return Ok(());
|
|
}
|
|
DavMethod::COPY
|
|
| DavMethod::MOVE
|
|
| DavMethod::POST
|
|
| DavMethod::PUT
|
|
| DavMethod::PATCH
|
|
if headers.overwrite_fail
|
|
&& resources.last().is_some_and(|r| {
|
|
r.etag.is_some() || r.document_id.is_some_and(|id| id != u32::MAX)
|
|
}) =>
|
|
{
|
|
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
// Add lock data to the cache
|
|
for resource in &resources {
|
|
if locks_.is_cached(resource).is_none() {
|
|
locks_.insert_lock_data(self, resource).await?;
|
|
}
|
|
}
|
|
|
|
// Unarchive lock data
|
|
let mut locks = locks_.to_unarchived().caused_by(trc::location!())?;
|
|
|
|
// Validate locks for write operations
|
|
let mut lock_response = Ok(());
|
|
if !matches!(
|
|
method,
|
|
DavMethod::GET | DavMethod::HEAD | DavMethod::LOCK | DavMethod::UNLOCK
|
|
) {
|
|
let mut base_path = None;
|
|
|
|
'outer: for (pos, resource) in resources.iter().enumerate() {
|
|
if pos == 0 && matches!(method, DavMethod::COPY) {
|
|
continue;
|
|
}
|
|
|
|
if let Some(idx) = locks.find_cache_pos(self, resource).await? {
|
|
let mut failed_locks = Vec::new();
|
|
|
|
for (lock_path, lock_item) in locks.find_locks_by_pos(idx, resource, true)? {
|
|
let lock_token = lock_item.urn().to_string();
|
|
if headers.if_.iter().any(|if_| {
|
|
if_.resource
|
|
.is_none_or(|r| {
|
|
r.trim_end_matches('/').ends_with(lock_path)})
|
|
&& if_.list.iter().any(|cond| matches!(cond, Condition::StateToken { token, .. } if token == &lock_token))
|
|
}) {
|
|
break 'outer;
|
|
} else {
|
|
let base_path = base_path.get_or_insert_with(|| {
|
|
headers.base_uri()
|
|
.unwrap_or_default()
|
|
});
|
|
failed_locks.push(format!("{base_path}/{lock_path}").into());
|
|
}
|
|
}
|
|
|
|
if !failed_locks.is_empty() {
|
|
lock_response = Err(DavErrorCondition::new(
|
|
StatusCode::LOCKED,
|
|
BaseCondition::LockTokenSubmitted(List(failed_locks)),
|
|
)
|
|
.into());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// There are no If headers, so we can return early
|
|
if no_if_headers {
|
|
return lock_response;
|
|
}
|
|
|
|
let mut resource_not_found = ResourceState {
|
|
account_id: u32::MAX,
|
|
collection: Collection::None,
|
|
path: "",
|
|
..Default::default()
|
|
};
|
|
|
|
'outer: for if_ in &headers.if_ {
|
|
if if_.list.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let mut resource_state = &mut resource_not_found;
|
|
|
|
if let Some(resource) = if_.resource {
|
|
if let Some(resource) = self
|
|
.validate_uri(access_token, resource)
|
|
.await
|
|
.ok()
|
|
.and_then(|r| {
|
|
let path = r.resource?;
|
|
|
|
Some(ResourceState {
|
|
account_id: r.account_id?,
|
|
collection: if !matches!(r.collection, Collection::FileNode)
|
|
&& path.contains('/')
|
|
{
|
|
r.collection.child_collection().unwrap_or(r.collection)
|
|
} else {
|
|
r.collection
|
|
},
|
|
path,
|
|
..Default::default()
|
|
})
|
|
})
|
|
{
|
|
if let Some(known_resource) = resources.iter_mut().find(|r| {
|
|
r.account_id == resource.account_id
|
|
&& r.collection == resource.collection
|
|
&& r.path == resource.path
|
|
}) {
|
|
resource_state = known_resource;
|
|
} else if access_token.has_access(resource.account_id, resource.collection) {
|
|
resources.push(resource);
|
|
resource_state = resources.last_mut().unwrap();
|
|
}
|
|
}
|
|
} else if let Some(resource) = resources.first_mut() {
|
|
resource_state = resource;
|
|
};
|
|
|
|
// Fill missing data for resource
|
|
if resource_state.collection != Collection::None
|
|
&& (resource_state.etag.is_none()
|
|
|| resource_state.lock_tokens.is_empty()
|
|
|| resource_state.sync_token.is_none())
|
|
{
|
|
let mut needs_lock_token = false;
|
|
let mut needs_sync_token = false;
|
|
let mut needs_etag = false;
|
|
|
|
for cond in &if_.list {
|
|
match cond {
|
|
Condition::StateToken { token, .. } => {
|
|
if token.starts_with("urn:inbuxa:davsync:") {
|
|
needs_sync_token = true;
|
|
} else {
|
|
needs_lock_token = true;
|
|
}
|
|
}
|
|
Condition::ETag { .. } | Condition::Exists { .. } => {
|
|
needs_etag = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch eTag
|
|
if needs_etag && resource_state.etag.is_none() {
|
|
if resource_state.document_id.is_none() {
|
|
resource_state.document_id = self
|
|
.map_uri_resource(
|
|
access_token,
|
|
UriResource {
|
|
collection: resource_state.collection,
|
|
account_id: resource_state.account_id,
|
|
resource: resource_state.path.into(),
|
|
},
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.map(|uri| uri.resource)
|
|
.unwrap_or(u32::MAX)
|
|
.into();
|
|
}
|
|
|
|
if let Some(document_id) =
|
|
resource_state.document_id.filter(|&id| id != u32::MAX)
|
|
&& let Some(archive) = self
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
resource_state.account_id,
|
|
resource_state.collection,
|
|
document_id,
|
|
))
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
{
|
|
resource_state.etag = archive.etag().into();
|
|
}
|
|
}
|
|
|
|
// Fetch lock token
|
|
if needs_lock_token
|
|
&& resource_state.lock_tokens.is_empty()
|
|
&& let Some(idx) = locks.find_cache_pos(self, resource_state).await?
|
|
{
|
|
let found_locks = locks
|
|
.find_locks_by_pos(idx, resource_state, false)?
|
|
.iter()
|
|
.map(|(_, lock)| lock.urn().to_string())
|
|
.collect::<Vec<_>>();
|
|
resource_state.lock_tokens = found_locks;
|
|
}
|
|
|
|
// Fetch sync token
|
|
if needs_sync_token && resource_state.sync_token.is_none() {
|
|
let id = self
|
|
.fetch_dav_resources(
|
|
access_token.account_id(),
|
|
resource_state.account_id,
|
|
resource_state.collection.into(),
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.highest_change_id;
|
|
resource_state.sync_token = Some(Urn::Sync { id, seq: 0 }.to_string());
|
|
}
|
|
}
|
|
|
|
for cond in &if_.list {
|
|
match cond {
|
|
Condition::StateToken { is_not, token } => {
|
|
if let Some(token) = Urn::try_extract_sync_id(token) {
|
|
if !((resource_state
|
|
.sync_token
|
|
.as_deref()
|
|
.and_then(Urn::try_extract_sync_id)
|
|
.is_some_and(|sync_token| sync_token == token))
|
|
^ is_not)
|
|
{
|
|
continue 'outer;
|
|
}
|
|
} else if !((resource_state.lock_tokens.iter().any(|t| t == token))
|
|
^ is_not)
|
|
{
|
|
continue 'outer;
|
|
}
|
|
}
|
|
Condition::ETag { is_not, tag } => {
|
|
if !((resource_state.etag.as_ref().is_some_and(|etag| etag == tag))
|
|
^ is_not)
|
|
{
|
|
continue 'outer;
|
|
}
|
|
}
|
|
Condition::Exists { is_not } => {
|
|
if !((resource_state.etag.is_some()) ^ is_not) {
|
|
continue 'outer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return lock_response;
|
|
}
|
|
|
|
Err(DavError::Code(
|
|
if matches!(method, DavMethod::GET | DavMethod::HEAD)
|
|
&& headers
|
|
.if_
|
|
.iter()
|
|
.any(|if_| if_.list.iter().any(|cond| cond.is_none_match()))
|
|
{
|
|
StatusCode::NOT_MODIFIED
|
|
} else {
|
|
StatusCode::PRECONDITION_FAILED
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
impl LockData {
|
|
pub fn remove_lock(&mut self, lock_id: u64) -> bool {
|
|
for (lock_path, lock_items) in self.locks.iter_mut() {
|
|
for (idx, lock_item) in lock_items.0.iter().enumerate() {
|
|
if lock_item.lock_id == lock_id {
|
|
lock_items.0.swap_remove(idx);
|
|
if lock_items.0.is_empty() {
|
|
let lock_path = lock_path.clone();
|
|
self.locks.remove(&lock_path);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub fn remove_expired(&mut self) -> u64 {
|
|
let mut max_expire = 0;
|
|
let now = now();
|
|
|
|
self.locks.retain(|_, locks| {
|
|
locks.0.retain(|lock| {
|
|
if lock.expires > now {
|
|
max_expire = std::cmp::max(max_expire, lock.expires);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
});
|
|
|
|
!locks.0.is_empty()
|
|
});
|
|
|
|
max_expire
|
|
}
|
|
}
|
|
|
|
impl<'x> LockArchive<'x> {
|
|
fn unarchive(&'x self) -> trc::Result<&'x ArchivedLockData> {
|
|
match self {
|
|
LockArchive::Unarchived(archived_lock_data) => Ok(archived_lock_data),
|
|
LockArchive::Archived(archive) => {
|
|
archive.unarchive::<LockData>().caused_by(trc::location!())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'x> LockCaches<'x> {
|
|
pub(self) fn new_shared(
|
|
account_id: u32,
|
|
collection: Collection,
|
|
lock_data: &'x ArchivedLockData,
|
|
) -> Self {
|
|
Self {
|
|
caches: vec![LockCache {
|
|
account_id,
|
|
collection,
|
|
lock_archive: LockArchive::Unarchived(lock_data),
|
|
}],
|
|
}
|
|
}
|
|
|
|
pub fn to_unarchived(&'x self) -> trc::Result<LockCaches<'x>> {
|
|
let caches = self
|
|
.caches
|
|
.iter()
|
|
.map(|cache| {
|
|
Ok(LockCache {
|
|
account_id: cache.account_id,
|
|
collection: cache.collection,
|
|
lock_archive: LockArchive::Unarchived(
|
|
cache.lock_archive.unarchive().caused_by(trc::location!())?,
|
|
),
|
|
})
|
|
})
|
|
.collect::<trc::Result<Vec<_>>>()?;
|
|
|
|
Ok(LockCaches { caches })
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_cached(&self, resource_state: &ResourceState<'_>) -> Option<usize> {
|
|
self.caches.iter().position(|cache| {
|
|
resource_state.account_id == cache.account_id
|
|
&& resource_state.collection.main_collection() == cache.collection.main_collection()
|
|
})
|
|
}
|
|
|
|
pub async fn find_cache_pos(
|
|
&mut self,
|
|
server: &Server,
|
|
resource_state: &ResourceState<'_>,
|
|
) -> trc::Result<Option<usize>> {
|
|
if let Some(idx) = self.is_cached(resource_state) {
|
|
Ok(Some(idx))
|
|
} else if resource_state.collection != Collection::None {
|
|
if self.insert_lock_data(server, resource_state).await? {
|
|
Ok(Some(self.caches.len() - 1))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
fn find_locks_by_pos(
|
|
&'x self,
|
|
pos: usize,
|
|
resource_state: &'x ResourceState<'_>,
|
|
include_children: bool,
|
|
) -> trc::Result<Vec<(&'x str, &'x ArchivedLockItem)>> {
|
|
self.caches[pos]
|
|
.lock_archive
|
|
.unarchive()
|
|
.map(|l| l.find_locks(resource_state.path, include_children))
|
|
}
|
|
|
|
async fn insert_lock_data(
|
|
&mut self,
|
|
server: &Server,
|
|
resource_state: &ResourceState<'_>,
|
|
) -> trc::Result<bool> {
|
|
if let Some(lock_archive) = server
|
|
.in_memory_store()
|
|
.key_get::<Archive<AlignedBytes>>(resource_state.lock_key().as_slice())
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
{
|
|
self.caches.push(LockCache {
|
|
account_id: resource_state.account_id,
|
|
collection: resource_state.collection,
|
|
lock_archive: LockArchive::Archived(lock_archive),
|
|
});
|
|
|
|
Ok(true)
|
|
} else {
|
|
Ok(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LockItem {
|
|
pub fn to_active_lock(&self, href: String) -> ActiveLock {
|
|
ActiveLock::new(
|
|
href,
|
|
if self.exclusive {
|
|
LockScope::Exclusive
|
|
} else {
|
|
LockScope::Shared
|
|
},
|
|
)
|
|
.with_depth(if self.depth_infinity {
|
|
Depth::Infinity
|
|
} else {
|
|
Depth::Zero
|
|
})
|
|
.with_owner_opt(self.owner_dav.clone())
|
|
.with_timeout(self.expires.saturating_sub(now()))
|
|
.with_lock_token(self.urn().to_string())
|
|
}
|
|
|
|
pub fn urn(&self) -> Urn {
|
|
Urn::Lock(self.lock_id)
|
|
}
|
|
}
|
|
|
|
impl ArchivedLockData {
|
|
pub fn find_locks<'x: 'y, 'y>(
|
|
&'x self,
|
|
resource: &'y str,
|
|
include_children: bool,
|
|
) -> Vec<(&'y str, &'x ArchivedLockItem)> {
|
|
let now = now();
|
|
let mut resource_part = resource;
|
|
let mut found_locks = Vec::new();
|
|
|
|
loop {
|
|
if let Some(locks) = self.locks.get(resource_part) {
|
|
found_locks.extend(
|
|
locks
|
|
.0
|
|
.iter()
|
|
.filter(|lock| {
|
|
lock.expires > now && (resource == resource_part || lock.depth_infinity)
|
|
})
|
|
.map(|lock| (resource_part, lock)),
|
|
);
|
|
}
|
|
|
|
if let Some((resource_part_, _)) = resource_part.rsplit_once('/') {
|
|
resource_part = resource_part_;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if include_children {
|
|
let prefix = format!("{}/", resource);
|
|
for (resource_part, locks) in self.locks.iter() {
|
|
if resource_part.starts_with(&prefix) {
|
|
found_locks.extend(
|
|
locks
|
|
.0
|
|
.iter()
|
|
.filter(|lock| lock.expires > now)
|
|
.map(|lock| (resource_part.as_str(), lock)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
found_locks
|
|
}
|
|
}
|
|
|
|
impl ArchivedLockItem {
|
|
pub fn to_active_lock(&self, href: String) -> ActiveLock {
|
|
ActiveLock::new(
|
|
href,
|
|
if self.exclusive {
|
|
LockScope::Exclusive
|
|
} else {
|
|
LockScope::Shared
|
|
},
|
|
)
|
|
.with_depth(if self.depth_infinity {
|
|
Depth::Infinity
|
|
} else {
|
|
Depth::Zero
|
|
})
|
|
.with_owner_opt(self.owner_dav.as_ref().map(Into::into))
|
|
.with_timeout(u64::from(self.expires).saturating_sub(now()))
|
|
.with_lock_token(self.urn().to_string())
|
|
}
|
|
|
|
pub fn urn(&self) -> Urn {
|
|
Urn::Lock(self.lock_id.into())
|
|
}
|
|
}
|
|
|
|
impl OwnedUri<'_> {
|
|
pub fn lock_key(&self) -> Vec<u8> {
|
|
build_lock_key(self.account_id, self.collection.main_collection())
|
|
}
|
|
}
|
|
|
|
impl ResourceState<'_> {
|
|
pub fn lock_key(&self) -> Vec<u8> {
|
|
build_lock_key(self.account_id, self.collection.main_collection())
|
|
}
|
|
}
|
|
|
|
pub(crate) fn build_lock_key(account_id: u32, collection: Collection) -> Vec<u8> {
|
|
let mut result = Vec::with_capacity(U32_LEN + 2);
|
|
result.push(KV_LOCK_DAV);
|
|
result.extend_from_slice(account_id.to_be_bytes().as_slice());
|
|
result.push(u8::from(collection));
|
|
result
|
|
}
|
|
|
|
impl PartialEq for ResourceState<'_> {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.account_id == other.account_id
|
|
&& self.collection == other.collection
|
|
&& self.document_id == other.document_id
|
|
}
|
|
}
|
|
|
|
impl Eq for ResourceState<'_> {}
|