Skip to main content

solid_pod_rs/storage/
mod.rs

1//! Storage abstraction for Solid pods.
2//!
3//! The `Storage` trait is the sole interface between the Solid
4//! protocol layer and concrete persistence backends. Implementations
5//! must be `Send + Sync + 'static` and safe for concurrent access.
6//!
7//! Two backends ship with the crate:
8//!
9//! - `memory::MemoryBackend` — in-memory, ideal for tests.
10//! - `fs::FsBackend` — filesystem-rooted, uses `tokio::fs` and
11//!   `notify` for change events.
12
13use async_trait::async_trait;
14use bytes::Bytes;
15use serde::{Deserialize, Serialize};
16
17use crate::error::PodError;
18
19#[cfg(feature = "fs-backend")]
20pub mod fs;
21
22#[cfg(feature = "memory-backend")]
23pub mod memory;
24
25/// Metadata describing a resource stored in a pod.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ResourceMeta {
28    /// Strong ETag (typically hex-encoded SHA-256).
29    pub etag: String,
30    /// Last modification time, UTC.
31    pub modified: chrono::DateTime<chrono::Utc>,
32    /// Size of the body in bytes.
33    pub size: u64,
34    /// MIME type, e.g. `"application/ld+json"`.
35    pub content_type: String,
36    /// `Link` header values.
37    ///
38    /// Each entry is a single `Link` value (no outer commas), e.g.
39    /// `<http://www.w3.org/ns/ldp#Resource>; rel="type"`.
40    pub links: Vec<String>,
41}
42
43impl ResourceMeta {
44    /// Construct a default `ResourceMeta` with the current UTC time.
45    pub fn new(etag: impl Into<String>, size: u64, content_type: impl Into<String>) -> Self {
46        ResourceMeta {
47            etag: etag.into(),
48            modified: chrono::Utc::now(),
49            size,
50            content_type: content_type.into(),
51            links: Vec::new(),
52        }
53    }
54}
55
56/// Change events emitted by storage watchers.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub enum StorageEvent {
59    /// A resource was created at the given path.
60    Created(String),
61    /// A resource was updated at the given path.
62    Updated(String),
63    /// A resource was deleted at the given path.
64    Deleted(String),
65}
66
67/// The storage abstraction. Implementations back a Solid pod with
68/// arbitrary persistence.
69///
70/// All paths use forward slashes and are rooted at `/`. Container
71/// paths end with `/`.
72#[async_trait]
73pub trait Storage: Send + Sync + 'static {
74    /// Fetch a resource body + metadata.
75    async fn get(&self, path: &str) -> Result<(Bytes, ResourceMeta), PodError>;
76
77    /// Write (create-or-replace) a resource.
78    ///
79    /// Returns the new metadata including the computed ETag.
80    async fn put(
81        &self,
82        path: &str,
83        body: Bytes,
84        content_type: &str,
85    ) -> Result<ResourceMeta, PodError>;
86
87    /// Delete a resource.
88    async fn delete(&self, path: &str) -> Result<(), PodError>;
89
90    /// List direct children of a container.
91    ///
92    /// Returned paths are relative to the container. A trailing `/`
93    /// indicates a sub-container.
94    async fn list(&self, container: &str) -> Result<Vec<String>, PodError>;
95
96    /// Fetch metadata without the body.
97    async fn head(&self, path: &str) -> Result<ResourceMeta, PodError>;
98
99    /// Return whether a resource exists.
100    async fn exists(&self, path: &str) -> Result<bool, PodError>;
101
102    /// Create an empty container at `path`.
103    ///
104    /// JSS parity: `PUT` with `Link: <ldp:BasicContainer>; rel="type"`
105    /// creates a container rather than a resource. The default
106    /// implementation writes a `.meta` marker via [`Storage::put`];
107    /// filesystem backends should create a directory instead.
108    async fn create_container(&self, path: &str) -> Result<ResourceMeta, PodError> {
109        let container = if path.ends_with('/') {
110            path.to_string()
111        } else {
112            format!("{path}/")
113        };
114        let meta_path = format!("{container}.meta", container = container.trim_end_matches('/'));
115        self.put(
116            &meta_path,
117            Bytes::new(),
118            "application/ld+json",
119        )
120        .await
121    }
122
123    /// Register a watcher for a resource or container.
124    ///
125    /// The returned channel receives `StorageEvent` messages for
126    /// changes under `path`. Closing the receiver detaches the
127    /// watcher.
128    async fn watch(
129        &self,
130        path: &str,
131    ) -> Result<tokio::sync::mpsc::Receiver<StorageEvent>, PodError>;
132}