1use 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#[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#[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#[derive(Debug, Serialize, Deserialize, Clone)]
43pub struct Meta {
44 pub size: u64,
46 #[serde(with = "time::serde::rfc3339")]
50 pub created: OffsetDateTime,
51 #[serde(with = "time::serde::rfc3339")]
53 pub updated: OffsetDateTime,
54 pub content_type: ContentType,
56 pub cache_control: CacheControl,
58}
59
60impl Meta {
61 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
78pub(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#[serde_as]
120#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Default)]
121#[serde(deny_unknown_fields)] pub struct Patch {
123 #[serde_as(as = "NullAsDefault<ContentType>")]
125 #[serde(default)]
126 pub content_type: Option<ContentType>,
127 #[serde_as(as = "NullAsDefault<CacheControl>")]
129 #[serde(default)]
130 pub cache_control: Option<CacheControl>,
131}
132
133impl Patch {
134 #[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
164pub 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#[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}