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::Result;
19use crate::raw::*;
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///
53/// # fn main() -> Result<()> {
54/// let _ = Operator::new(services::Memory::default())?
55///     .layer(MimeGuessLayer::default())
56///     .finish();
57/// Ok(())
58/// # }
59/// ```
60#[derive(Debug, Clone, Default)]
61#[non_exhaustive]
62pub struct MimeGuessLayer {}
63
64impl<A: Access> Layer<A> for MimeGuessLayer {
65    type LayeredAccess = MimeGuessAccessor<A>;
66
67    fn layer(&self, inner: A) -> Self::LayeredAccess {
68        MimeGuessAccessor(inner)
69    }
70}
71
72#[derive(Clone, Debug)]
73pub struct MimeGuessAccessor<A: Access>(A);
74
75fn mime_from_path(path: &str) -> Option<&str> {
76    mime_guess::from_path(path).first_raw()
77}
78
79fn opwrite_with_mime(path: &str, op: OpWrite) -> OpWrite {
80    if op.content_type().is_some() {
81        return op;
82    }
83
84    if let Some(mime) = mime_from_path(path) {
85        return op.with_content_type(mime);
86    }
87
88    op
89}
90
91fn rpstat_with_mime(path: &str, rp: RpStat) -> RpStat {
92    rp.map_metadata(|metadata| {
93        if metadata.content_type().is_some() {
94            return metadata;
95        }
96
97        if let Some(mime) = mime_from_path(path) {
98            return metadata.with_content_type(mime.into());
99        }
100
101        metadata
102    })
103}
104
105impl<A: Access> LayeredAccess for MimeGuessAccessor<A> {
106    type Inner = A;
107    type Reader = A::Reader;
108    type Writer = A::Writer;
109    type Lister = A::Lister;
110    type Deleter = A::Deleter;
111
112    fn inner(&self) -> &Self::Inner {
113        &self.0
114    }
115
116    async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
117        self.inner().read(path, args).await
118    }
119
120    async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::Writer)> {
121        self.inner()
122            .write(path, opwrite_with_mime(path, args))
123            .await
124    }
125
126    async fn stat(&self, path: &str, args: OpStat) -> Result<RpStat> {
127        self.inner()
128            .stat(path, args)
129            .await
130            .map(|rp| rpstat_with_mime(path, rp))
131    }
132
133    async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> {
134        self.inner().delete().await
135    }
136
137    async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> {
138        self.inner().list(path, args).await
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use futures::TryStreamExt;
145
146    use super::*;
147    use crate::Metadata;
148    use crate::Operator;
149    use crate::services::Memory;
150
151    const DATA: &str = "<html>test</html>";
152    const CUSTOM: &str = "text/custom";
153    const HTML: &str = "text/html";
154
155    #[tokio::test]
156    async fn test_async() {
157        let op = Operator::new(Memory::default())
158            .unwrap()
159            .layer(MimeGuessLayer::default())
160            .finish();
161
162        op.write("test0.html", DATA).await.unwrap();
163        assert_eq!(
164            op.stat("test0.html").await.unwrap().content_type(),
165            Some(HTML)
166        );
167
168        op.write("test1.asdfghjkl", DATA).await.unwrap();
169        assert_eq!(
170            op.stat("test1.asdfghjkl").await.unwrap().content_type(),
171            None
172        );
173
174        op.write_with("test2.html", DATA)
175            .content_type(CUSTOM)
176            .await
177            .unwrap();
178        assert_eq!(
179            op.stat("test2.html").await.unwrap().content_type(),
180            Some(CUSTOM)
181        );
182
183        let entries: Vec<Metadata> = op
184            .lister_with("")
185            .await
186            .unwrap()
187            .and_then(|entry| {
188                let op = op.clone();
189                async move { op.stat(entry.path()).await }
190            })
191            .try_collect()
192            .await
193            .unwrap();
194        assert_eq!(entries[0].content_type(), Some(HTML));
195        assert_eq!(entries[1].content_type(), None);
196        assert_eq!(entries[2].content_type(), Some(CUSTOM));
197    }
198}