jotta_osd/object/
meta.rs

1//! Object metadata.
2use derive_more::Display;
3use jotta::{
4    auth::TokenStore,
5    files::{AllocReq, ConflictHandler, UploadRes},
6    path::{PathOnDevice, UserScopedPath},
7    range::OpenByteRange,
8};
9use mime::Mime;
10use serde::{Deserialize, Serialize};
11use serde_with::{serde_as, DisplayFromStr};
12use time::OffsetDateTime;
13use tracing::{error, instrument, warn};
14
15use crate::{errors::Error, serde::NullAsDefault};
16use crate::{path::BucketName, Context};
17
18use super::ObjectName;
19
20/// `Cache-Control` directive.
21#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
22pub struct CacheControl(pub String);
23
24impl Default for CacheControl {
25    fn default() -> Self {
26        Self("public, max-age=3600".into())
27    }
28}
29
30/// Object content type.
31#[serde_as]
32#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Display)]
33pub struct ContentType(#[serde_as(as = "DisplayFromStr")] pub Mime);
34
35impl Default for ContentType {
36    fn default() -> Self {
37        Self(mime::APPLICATION_OCTET_STREAM)
38    }
39}
40
41/// Metadata associated with each object.
42#[derive(Debug, Serialize, Deserialize, Clone)]
43pub struct Meta {
44    /// Size of the object in bytes.
45    pub size: u64,
46    // /// CRC32 checksum.
47    // pub crc32c: u32,
48    /// Creation timestamp.
49    #[serde(with = "time::serde::rfc3339")]
50    pub created: OffsetDateTime,
51    /// Update timestamp.
52    #[serde(with = "time::serde::rfc3339")]
53    pub updated: OffsetDateTime,
54    /// Media type of the object.
55    pub content_type: ContentType,
56    /// Cache control.
57    pub cache_control: CacheControl,
58}
59
60impl Meta {
61    /// Patch the metadata.
62    pub fn patch(&mut self, patch: Patch) {
63        let Patch {
64            content_type,
65            cache_control,
66        } = patch;
67
68        if let Some(content_type) = content_type {
69            self.content_type = content_type;
70        }
71
72        if let Some(cache_control) = cache_control {
73            self.cache_control = cache_control;
74        }
75    }
76}
77
78/// Set the metadata of an object.
79pub(crate) async fn set_raw(
80    ctx: &Context<impl TokenStore>,
81    bucket: &BucketName,
82    object: &ObjectName,
83    meta: &Meta,
84    conflict_handler: ConflictHandler,
85) -> crate::Result<()> {
86    let body = rmp_serde::to_vec(&meta)?;
87    let bytes = body.len().try_into().unwrap();
88
89    let req = AllocReq {
90        path: &PathOnDevice(format!(
91            "{}/{}/{}/meta",
92            ctx.root_on_device(),
93            bucket,
94            object.to_hex()
95        )),
96        bytes,
97        md5: md5::compute(&body),
98        conflict_handler,
99        created: None,
100        modified: None,
101    };
102
103    let upload_url = ctx.fs.allocate(&req).await?.upload_url;
104
105    match ctx.fs.upload_range(&upload_url, body, 0..=bytes).await? {
106        UploadRes::Complete(_) => Ok(()),
107        UploadRes::Incomplete(_) => {
108            warn!("metadata did not completely upload");
109            Err(Error::Fs(jotta::Error::IncompleteUpload))
110        }
111    }
112}
113
114/// A object metadata patch.
115///
116/// `null` will be converted to `Some(Default::Default)` while absent
117/// fields are treated as `None`. This way, `null` can be used to
118/// reset field values.
119#[serde_as]
120#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Default)]
121#[serde(deny_unknown_fields)] // don't make clients think that read-only fields are writable
122pub struct Patch {
123    /// Media type of the object.
124    #[serde_as(as = "NullAsDefault<ContentType>")]
125    #[serde(default)]
126    pub content_type: Option<ContentType>,
127    /// Cache control.
128    #[serde_as(as = "NullAsDefault<CacheControl>")]
129    #[serde(default)]
130    pub cache_control: Option<CacheControl>,
131}
132
133impl Patch {
134    /// Is the patch empty?
135    ///
136    /// ```
137    /// use jotta_osd::object::meta::Patch;
138    ///
139    /// assert!(Patch { content_type: None, cache_control: None }.is_empty());
140    /// ```
141    #[must_use]
142    pub fn is_empty(&self) -> bool {
143        *self == Self::default()
144    }
145}
146
147impl From<Meta> for Patch {
148    fn from(m: Meta) -> Self {
149        let Meta {
150            size: _,
151            created: _,
152            updated: _,
153            content_type,
154            cache_control,
155        } = m;
156
157        Self {
158            content_type: Some(content_type),
159            cache_control: Some(cache_control),
160        }
161    }
162}
163
164/// Patch metadata. If the patch is empty, no patch is made.
165///
166/// # Errors
167///
168/// - network errors
169/// - no remote metadata to patch
170pub async fn patch(
171    ctx: &Context<impl TokenStore>,
172    bucket: &BucketName,
173    object: &ObjectName,
174    patch: Patch,
175) -> crate::Result<Meta> {
176    let mut meta = get(ctx, bucket, object).await?;
177
178    if !patch.is_empty() {
179        meta.patch(patch);
180
181        meta.updated = OffsetDateTime::now_utc();
182
183        set_raw(
184            ctx,
185            bucket,
186            object,
187            &meta,
188            ConflictHandler::CreateNewRevision,
189        )
190        .await?;
191    }
192
193    Ok(meta)
194}
195
196/// Get metadata associated with an object.
197#[instrument(skip(ctx))]
198pub async fn get(
199    ctx: &Context<impl TokenStore>,
200    bucket: &BucketName,
201    name: &ObjectName,
202) -> crate::Result<Meta> {
203    let msg = ctx
204        .fs
205        .file_to_bytes(
206            &UserScopedPath(format!(
207                "{}/{}/{}/meta",
208                ctx.user_scoped_root(),
209                bucket,
210                name.to_hex()
211            )),
212            OpenByteRange::full(),
213        )
214        .await?;
215
216    let meta = rmp_serde::from_slice(&msg).map_err(|e| {
217        error!("parse metadata failed: {}", e);
218        e
219    })?;
220
221    Ok(meta)
222}