opendal_layer_mime_guess/
lib.rs1#![doc = include_str!("../README.md")]
19#![cfg_attr(docsrs, feature(doc_cfg))]
20#![cfg_attr(docsrs, doc(auto_cfg))]
21#![deny(missing_docs)]
22use std::sync::Arc;
23
24use opendal_core::raw::*;
25use opendal_core::*;
26
27#[derive(Clone, Debug, Default)]
63#[non_exhaustive]
64pub struct MimeGuessLayer {}
65
66impl MimeGuessLayer {
67 pub fn new() -> Self {
69 Self::default()
70 }
71}
72
73impl Layer for MimeGuessLayer {
74 fn apply_service(&self, inner: Servicer) -> Servicer {
75 Arc::new(self.layer(inner))
76 }
77}
78
79impl MimeGuessLayer {
80 fn layer(&self, inner: Servicer) -> MimeGuessAccessor {
81 MimeGuessAccessor(inner)
82 }
83}
84
85#[doc(hidden)]
86#[derive(Debug)]
87pub struct MimeGuessAccessor(Servicer);
88
89fn mime_from_path(path: &str) -> Option<&str> {
90 mime_guess::from_path(path).first_raw()
91}
92
93fn opwrite_with_mime(path: &str, op: OpWrite) -> OpWrite {
94 if op.content_type().is_some() {
95 return op;
96 }
97
98 if let Some(mime) = mime_from_path(path) {
99 return op.into_content_type(mime);
100 }
101
102 op
103}
104
105fn opcompose_with_mime(path: &str, op: OpCompose) -> OpCompose {
106 if op.content_type().is_some() {
107 return op;
108 }
109
110 if let Some(mime) = mime_from_path(path) {
111 return op.into_content_type(mime);
112 }
113
114 op
115}
116
117fn rpstat_with_mime(path: &str, rp: RpStat) -> RpStat {
118 rp.map_metadata(|metadata| {
119 if metadata.content_type().is_some() {
120 return metadata;
121 }
122
123 if let Some(mime) = mime_from_path(path) {
124 let mut metadata = metadata.into_builder();
125 metadata.content_type(mime);
126 return metadata.build();
127 }
128
129 metadata
130 })
131}
132
133impl Service for MimeGuessAccessor {
134 type Reader = oio::Reader;
135 type Writer = oio::Writer;
136 type Lister = oio::Lister;
137 type Deleter = oio::Deleter;
138 type Copier = oio::Copier;
139 type Composer = oio::Composer;
140
141 fn info(&self) -> ServiceInfo {
142 self.0.info()
143 }
144
145 fn capability(&self) -> Capability {
146 self.0.capability()
147 }
148
149 async fn create_dir(
150 &self,
151 ctx: &OperationContext,
152 path: &str,
153 args: OpCreateDir,
154 ) -> Result<RpCreateDir> {
155 self.0.create_dir(ctx, path, args).await
156 }
157
158 fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
159 self.0.read(ctx, path, args)
160 }
161
162 fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
163 self.0.write(ctx, path, opwrite_with_mime(path, args))
164 }
165
166 fn copy(
167 &self,
168 ctx: &OperationContext,
169 from: &str,
170 to: &str,
171 args: OpCopy,
172 ) -> Result<Self::Copier> {
173 self.0.copy(ctx, from, to, args)
174 }
175
176 fn compose(&self, ctx: &OperationContext, to: &str, args: OpCompose) -> Result<Self::Composer> {
177 self.0.compose(ctx, to, opcompose_with_mime(to, args))
178 }
179
180 async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
181 self.0
182 .stat(ctx, path, args)
183 .await
184 .map(|rp| rpstat_with_mime(path, rp))
185 }
186
187 async fn rename(
188 &self,
189 ctx: &OperationContext,
190 from: &str,
191 to: &str,
192 args: OpRename,
193 ) -> Result<RpRename> {
194 self.0.rename(ctx, from, to, args).await
195 }
196
197 async fn restore(
198 &self,
199 ctx: &OperationContext,
200 path: &str,
201 args: OpRestore,
202 ) -> Result<RpRestore> {
203 self.0.restore(ctx, path, args).await
204 }
205
206 fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
207 self.0.delete(ctx)
208 }
209
210 fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
211 self.0.list(ctx, path, args)
212 }
213
214 async fn presign(
215 &self,
216 ctx: &OperationContext,
217 path: &str,
218 args: OpPresign,
219 ) -> Result<RpPresign> {
220 self.0.presign(ctx, path, args).await
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use futures::TryStreamExt;
227
228 use super::*;
229
230 const DATA: &str = "<html>test</html>";
231 const CUSTOM: &str = "text/custom";
232 const HTML: &str = "text/html";
233
234 #[tokio::test]
235 async fn test_async() -> Result<()> {
236 let op = Operator::new(services::Memory::default())?.layer(MimeGuessLayer::new());
237
238 op.write("test0.html", DATA).await?;
239 assert_eq!(op.stat("test0.html").await?.content_type(), Some(HTML));
240
241 op.write("test1.asdfghjkl", DATA).await?;
242 assert_eq!(op.stat("test1.asdfghjkl").await?.content_type(), None);
243
244 op.write_with("test2.html", DATA)
245 .content_type(CUSTOM)
246 .await?;
247 assert_eq!(op.stat("test2.html").await?.content_type(), Some(CUSTOM));
248
249 let entries = op
250 .lister_with("")
251 .await?
252 .and_then(|entry| {
253 let op = op.clone();
254 async move { op.stat(entry.path()).await }
255 })
256 .try_collect::<Vec<_>>()
257 .await?;
258 assert_eq!(entries[0].content_type(), Some(HTML));
259 assert_eq!(entries[1].content_type(), None);
260 assert_eq!(entries[2].content_type(), Some(CUSTOM));
261
262 Ok(())
263 }
264}