use std::time::Duration;
use reqwest::{Method, Response, header::HeaderMap};
use super::core::SessionStorage;
use super::resource::{IntoResourcePath, ResourcePath};
use crate::{Result, cross_log, errors::RequestError};
const LOCK_TOKEN_SCHEME: &str = "opaquelocktoken:";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StorageLock {
path: ResourcePath,
token: String,
timeout: Duration,
}
impl StorageLock {
#[must_use]
pub const fn path(&self) -> &ResourcePath {
&self.path
}
#[must_use]
pub fn token(&self) -> &str {
&self.token
}
#[must_use]
pub const fn timeout(&self) -> Duration {
self.timeout
}
#[must_use]
pub fn if_header(&self) -> String {
format!("(<{}>)", self.token)
}
fn lock_token_header(&self) -> String {
format!("<{}>", self.token)
}
}
impl SessionStorage {
pub async fn lock<P: IntoResourcePath>(
&self,
path: P,
timeout: Duration,
) -> Result<StorageLock> {
let path = path.into_abs_path()?;
let rb = self
.request(lock_method(), &path)
.await?
.header("Timeout", timeout_header(timeout));
let resp = self.client.check_http_status(rb.send().await?).await?;
let token = granted_token(resp.headers()).ok_or_else(|| RequestError::Validation {
message: "homeserver granted a lock without a Lock-Token".into(),
})?;
let timeout = granted_timeout(resp.headers()).unwrap_or(timeout);
cross_log!(debug, "Locked {} for {:?}", path, timeout);
Ok(StorageLock {
path,
token,
timeout,
})
}
pub async fn refresh_lock(&self, lock: &mut StorageLock, timeout: Duration) -> Result<()> {
let rb = self
.request(lock_method(), &lock.path)
.await?
.header("If", lock.if_header())
.header("Timeout", timeout_header(timeout));
let resp = self.client.check_http_status(rb.send().await?).await?;
lock.timeout = granted_timeout(resp.headers()).unwrap_or(timeout);
Ok(())
}
pub async fn unlock(&self, lock: &StorageLock) -> Result<()> {
let rb = self
.request(unlock_method(), &lock.path)
.await?
.header("Lock-Token", lock.lock_token_header());
self.client.check_http_status(rb.send().await?).await?;
Ok(())
}
pub async fn put_locked<B: Into<reqwest::Body>>(
&self,
lock: &StorageLock,
body: B,
) -> Result<Response> {
let rb = self
.request(Method::PUT, &lock.path)
.await?
.header("If", lock.if_header())
.body(body);
self.client.check_http_status(rb.send().await?).await
}
pub async fn delete_locked(&self, lock: &StorageLock) -> Result<Response> {
let rb = self
.request(Method::DELETE, &lock.path)
.await?
.header("If", lock.if_header());
self.client.check_http_status(rb.send().await?).await
}
}
fn lock_method() -> Method {
Method::from_bytes(b"LOCK").expect("LOCK is a valid HTTP method")
}
fn unlock_method() -> Method {
Method::from_bytes(b"UNLOCK").expect("UNLOCK is a valid HTTP method")
}
fn timeout_header(timeout: Duration) -> String {
format!("Second-{}", timeout.as_secs().max(1))
}
fn granted_token(headers: &HeaderMap) -> Option<String> {
let value = headers.get("lock-token")?.to_str().ok()?.trim();
let token = value.strip_prefix('<')?.strip_suffix('>')?;
token
.starts_with(LOCK_TOKEN_SCHEME)
.then(|| token.to_owned())
}
fn granted_timeout(headers: &HeaderMap) -> Option<Duration> {
let value = headers.get("timeout")?.to_str().ok()?.trim();
let seconds = value.strip_prefix("Second-")?.parse().ok()?;
Some(Duration::from_secs(seconds))
}
#[cfg(test)]
mod tests {
use reqwest::header::HeaderValue;
use super::*;
fn headers_with(name: &'static str, value: &'static str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(name, HeaderValue::from_static(value));
headers
}
#[test]
fn granted_token_needs_a_bracketed_lock_token_url() {
let granted = headers_with("lock-token", " <opaquelocktoken:abc> ");
assert_eq!(
granted_token(&granted).as_deref(),
Some("opaquelocktoken:abc")
);
for malformed in ["opaquelocktoken:abc", "<urn:uuid:abc>", ""] {
assert_eq!(granted_token(&headers_with("lock-token", malformed)), None);
}
assert_eq!(granted_token(&HeaderMap::new()), None);
}
#[test]
fn granted_timeout_reads_whole_seconds() {
let granted = headers_with("timeout", "Second-30");
assert_eq!(granted_timeout(&granted), Some(Duration::from_secs(30)));
for malformed in ["Infinite", "Second-", "30"] {
assert_eq!(granted_timeout(&headers_with("timeout", malformed)), None);
}
assert_eq!(granted_timeout(&HeaderMap::new()), None);
}
#[test]
fn timeout_header_rounds_up_to_one_second() {
assert_eq!(timeout_header(Duration::from_secs(45)), "Second-45");
assert_eq!(timeout_header(Duration::from_millis(200)), "Second-1");
}
#[test]
fn lock_presents_its_token_in_both_header_forms() {
let lock = StorageLock {
path: ResourcePath::parse("/pub/state.json").unwrap(),
token: "opaquelocktoken:abc".into(),
timeout: Duration::from_secs(30),
};
assert_eq!(lock.if_header(), "(<opaquelocktoken:abc>)");
assert_eq!(lock.lock_token_header(), "<opaquelocktoken:abc>");
}
}