opendal/layers/
mime_guess.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::raw::*;
19use crate::Result;
20
21/// A layer that can automatically set `Content-Type` based on the file extension in the path.
22///
23/// # MimeGuess
24///
25/// This layer uses [mime_guess](https://crates.io/crates/mime_guess) to automatically
26/// set `Content-Type` based on the file extension in the operation path.
27///
28/// However, please note that this layer will not overwrite the `content_type` you manually set,
29/// nor will it overwrite the `content_type` provided by backend services.
30///
31/// A simple example is that for object storage backends, when you call `stat`, the backend will
32/// provide `content_type` information, and `mime_guess` will not be called, but will use
33/// the `content_type` provided by the backend.
34///
35/// But if you use the [Fs](../services/struct.Fs.html) backend to call `stat`, the backend will
36/// not provide `content_type` information, and our `mime_guess` will be called to provide you with
37/// appropriate `content_type` information.
38///
39/// Another thing to note is that using this layer does not necessarily mean that the result will 100%
40/// contain `content_type` information. If the extension of your path is custom or an uncommon type,
41/// the returned result will still not contain `content_type` information (the specific condition here is
42/// when [mime_guess::from_path::first_raw](https://docs.rs/mime_guess/latest/mime_guess/struct.MimeGuess.html#method.first_raw)
43/// returns `None`).
44///
45/// # Examples
46///
47/// ```no_run
48/// # use opendal::layers::MimeGuessLayer;
49/// # use opendal::services;
50/// # use opendal::Operator;
51/// # use opendal::Result;
52/// # use opendal::Scheme;
53///
54/// # fn main() -> Result<()> {
55/// let _ = Operator::new(services::Memory::default())?
56///     .layer(MimeGuessLayer::default())
57///     .finish();
58/// Ok(())
59/// # }
60/// ```
61#[derive(Debug, Clone, Default)]
62#[non_exhaustive]
63pub struct MimeGuessLayer {}
64
65impl<A: Access> Layer<A> for MimeGuessLayer {
66    type LayeredAccess = MimeGuessAccessor<A>;
67
68    fn layer(&self, inner: A) -> Self::LayeredAccess {
69        MimeGuessAccessor(inner)
70    }
71}
72
73#[derive(Clone, Debug)]
74pub struct MimeGuessAccessor<A: Access>(A);
75
76fn mime_from_path(path: &str) -> Option<&str> {
77    mime_guess::from_path(path).first_raw()
78}
79
80fn opwrite_with_mime(path: &str, op: OpWrite) -> OpWrite {
81    if op.content_type().is_some() {
82        return op;
83    }
84
85    if let Some(mime) = mime_from_path(path) {
86        return op.with_content_type(mime);
87    }
88
89    op
90}
91
92fn rpstat_with_mime(path: &str, rp: RpStat) -> RpStat {
93    rp.map_metadata(|metadata| {
94        if metadata.content_type().is_some() {
95            return metadata;
96        }
97
98        if let Some(mime) = mime_from_path(path) {
99            return metadata.with_content_type(mime.into());
100        }
101
102        metadata
103    })
104}
105
106impl<A: Access> LayeredAccess for MimeGuessAccessor<A> {
107    type Inner = A;
108    type Reader = A::Reader;
109    type Writer = A::Writer;
110    type Lister = A::Lister;
111    type Deleter = A::Deleter;
112    type BlockingReader = A::BlockingReader;
113    type BlockingWriter = A::BlockingWriter;
114    type BlockingLister = A::BlockingLister;
115    type BlockingDeleter = A::BlockingDeleter;
116
117    fn inner(&self) -> &Self::Inner {
118        &self.0
119    }
120
121    async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
122        self.inner().read(path, args).await
123    }
124
125    async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::Writer)> {
126        self.inner()
127            .write(path, opwrite_with_mime(path, args))
128            .await
129    }
130
131    async fn stat(&self, path: &str, args: OpStat) -> Result<RpStat> {
132        self.inner()
133            .stat(path, args)
134            .await
135            .map(|rp| rpstat_with_mime(path, rp))
136    }
137
138    async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> {
139        self.inner().delete().await
140    }
141
142    async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> {
143        self.inner().list(path, args).await
144    }
145
146    fn blocking_read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::BlockingReader)> {
147        self.inner().blocking_read(path, args)
148    }
149
150    fn blocking_write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::BlockingWriter)> {
151        self.inner()
152            .blocking_write(path, opwrite_with_mime(path, args))
153    }
154
155    fn blocking_stat(&self, path: &str, args: OpStat) -> Result<RpStat> {
156        self.inner()
157            .blocking_stat(path, args)
158            .map(|rp| rpstat_with_mime(path, rp))
159    }
160
161    fn blocking_delete(&self) -> Result<(RpDelete, Self::BlockingDeleter)> {
162        self.inner().blocking_delete()
163    }
164
165    fn blocking_list(&self, path: &str, args: OpList) -> Result<(RpList, Self::BlockingLister)> {
166        self.inner().blocking_list(path, args)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::services::Memory;
174    use crate::Metadata;
175    use crate::Operator;
176    use futures::TryStreamExt;
177
178    const DATA: &str = "<html>test</html>";
179    const CUSTOM: &str = "text/custom";
180    const HTML: &str = "text/html";
181
182    #[tokio::test]
183    async fn test_async() {
184        let op = Operator::new(Memory::default())
185            .unwrap()
186            .layer(MimeGuessLayer::default())
187            .finish();
188
189        op.write("test0.html", DATA).await.unwrap();
190        assert_eq!(
191            op.stat("test0.html").await.unwrap().content_type(),
192            Some(HTML)
193        );
194
195        op.write("test1.asdfghjkl", DATA).await.unwrap();
196        assert_eq!(
197            op.stat("test1.asdfghjkl").await.unwrap().content_type(),
198            None
199        );
200
201        op.write_with("test2.html", DATA)
202            .content_type(CUSTOM)
203            .await
204            .unwrap();
205        assert_eq!(
206            op.stat("test2.html").await.unwrap().content_type(),
207            Some(CUSTOM)
208        );
209
210        let entries: Vec<Metadata> = op
211            .lister_with("")
212            .await
213            .unwrap()
214            .and_then(|entry| {
215                let op = op.clone();
216                async move { op.stat(entry.path()).await }
217            })
218            .try_collect()
219            .await
220            .unwrap();
221        assert_eq!(entries[0].content_type(), Some(HTML));
222        assert_eq!(entries[1].content_type(), None);
223        assert_eq!(entries[2].content_type(), Some(CUSTOM));
224    }
225
226    #[test]
227    fn test_blocking() {
228        let op = Operator::new(Memory::default())
229            .unwrap()
230            .layer(MimeGuessLayer::default())
231            .finish()
232            .blocking();
233
234        op.write("test0.html", DATA).unwrap();
235        assert_eq!(op.stat("test0.html").unwrap().content_type(), Some(HTML));
236
237        op.write("test1.asdfghjkl", DATA).unwrap();
238        assert_eq!(op.stat("test1.asdfghjkl").unwrap().content_type(), None);
239
240        op.write_with("test2.html", DATA)
241            .content_type(CUSTOM)
242            .call()
243            .unwrap();
244        assert_eq!(op.stat("test2.html").unwrap().content_type(), Some(CUSTOM));
245
246        let entries: Vec<Metadata> = op
247            .lister_with("")
248            .call()
249            .unwrap()
250            .map(|entry| {
251                let op = op.clone();
252                op.stat(entry.unwrap().path()).unwrap()
253            })
254            .collect();
255        assert_eq!(entries[0].content_type(), Some(HTML));
256        assert_eq!(entries[1].content_type(), None);
257        assert_eq!(entries[2].content_type(), Some(CUSTOM));
258    }
259}