opendal/services/azfile/
backend.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 std::fmt::Debug;
19use std::fmt::Formatter;
20use std::sync::Arc;
21
22use http::Response;
23use http::StatusCode;
24use log::debug;
25use reqsign::AzureStorageConfig;
26use reqsign::AzureStorageLoader;
27use reqsign::AzureStorageSigner;
28
29use super::core::AzfileCore;
30use super::delete::AzfileDeleter;
31use super::error::parse_error;
32use super::lister::AzfileLister;
33use super::writer::AzfileWriter;
34use super::writer::AzfileWriters;
35use crate::raw::*;
36use crate::services::AzfileConfig;
37use crate::*;
38
39impl From<AzureStorageConfig> for AzfileConfig {
40    fn from(config: AzureStorageConfig) -> Self {
41        AzfileConfig {
42            account_name: config.account_name,
43            account_key: config.account_key,
44            sas_token: config.sas_token,
45            endpoint: config.endpoint,
46            root: None,                // root is not part of AzureStorageConfig
47            share_name: String::new(), // share_name is not part of AzureStorageConfig
48        }
49    }
50}
51
52impl Configurator for AzfileConfig {
53    type Builder = AzfileBuilder;
54
55    #[allow(deprecated)]
56    fn into_builder(self) -> Self::Builder {
57        AzfileBuilder {
58            config: self,
59            http_client: None,
60        }
61    }
62}
63
64/// Azure File services support.
65#[doc = include_str!("docs.md")]
66#[derive(Default, Clone)]
67pub struct AzfileBuilder {
68    config: AzfileConfig,
69
70    #[deprecated(since = "0.53.0", note = "Use `Operator::update_http_client` instead")]
71    http_client: Option<HttpClient>,
72}
73
74impl Debug for AzfileBuilder {
75    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
76        let mut ds = f.debug_struct("AzfileBuilder");
77
78        ds.field("config", &self.config);
79
80        ds.finish()
81    }
82}
83
84impl AzfileBuilder {
85    /// Set root of this backend.
86    ///
87    /// All operations will happen under this root.
88    pub fn root(mut self, root: &str) -> Self {
89        self.config.root = if root.is_empty() {
90            None
91        } else {
92            Some(root.to_string())
93        };
94
95        self
96    }
97
98    /// Set endpoint of this backend.
99    pub fn endpoint(mut self, endpoint: &str) -> Self {
100        if !endpoint.is_empty() {
101            // Trim trailing `/` so that we can accept `http://127.0.0.1:9000/`
102            self.config.endpoint = Some(endpoint.trim_end_matches('/').to_string());
103        }
104
105        self
106    }
107
108    /// Set account_name of this backend.
109    ///
110    /// - If account_name is set, we will take user's input first.
111    /// - If not, we will try to load it from environment.
112    pub fn account_name(mut self, account_name: &str) -> Self {
113        if !account_name.is_empty() {
114            self.config.account_name = Some(account_name.to_string());
115        }
116
117        self
118    }
119
120    /// Set account_key of this backend.
121    ///
122    /// - If account_key is set, we will take user's input first.
123    /// - If not, we will try to load it from environment.
124    pub fn account_key(mut self, account_key: &str) -> Self {
125        if !account_key.is_empty() {
126            self.config.account_key = Some(account_key.to_string());
127        }
128
129        self
130    }
131
132    /// Set file share name of this backend.
133    ///
134    /// # Notes
135    /// You can find more about from: <https://learn.microsoft.com/en-us/rest/api/storageservices/operations-on-shares--file-service>
136    pub fn share_name(mut self, share_name: &str) -> Self {
137        if !share_name.is_empty() {
138            self.config.share_name = share_name.to_string();
139        }
140
141        self
142    }
143
144    /// Specify the http client that used by this service.
145    ///
146    /// # Notes
147    ///
148    /// This API is part of OpenDAL's Raw API. `HttpClient` could be changed
149    /// during minor updates.
150    #[deprecated(since = "0.53.0", note = "Use `Operator::update_http_client` instead")]
151    #[allow(deprecated)]
152    pub fn http_client(mut self, client: HttpClient) -> Self {
153        self.http_client = Some(client);
154        self
155    }
156
157    /// Create a new `AfileBuilder` instance from an [Azure Storage connection string][1].
158    ///
159    /// [1]: https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string
160    ///
161    /// # Example
162    /// ```
163    /// use opendal::Builder;
164    /// use opendal::services::Azfile;
165    ///
166    /// let conn_str = "AccountName=example;DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net";
167    ///
168    /// let mut config = Azfile::from_connection_string(&conn_str)
169    ///     .unwrap()
170    ///     // Add additional configuration if needed
171    ///     .share_name("myShare")
172    ///     .build()
173    ///     .unwrap();
174    /// ```
175    pub fn from_connection_string(conn_str: &str) -> Result<Self> {
176        let config =
177            raw::azure_config_from_connection_string(conn_str, raw::AzureStorageService::File)?;
178
179        Ok(AzfileConfig::from(config).into_builder())
180    }
181}
182
183impl Builder for AzfileBuilder {
184    const SCHEME: Scheme = Scheme::Azfile;
185    type Config = AzfileConfig;
186
187    fn build(self) -> Result<impl Access> {
188        debug!("backend build started: {:?}", &self);
189
190        let root = normalize_root(&self.config.root.unwrap_or_default());
191        debug!("backend use root {}", root);
192
193        let endpoint = match &self.config.endpoint {
194            Some(endpoint) => Ok(endpoint.clone()),
195            None => Err(Error::new(ErrorKind::ConfigInvalid, "endpoint is empty")
196                .with_operation("Builder::build")
197                .with_context("service", Scheme::Azfile)),
198        }?;
199        debug!("backend use endpoint {}", &endpoint);
200
201        let account_name_option = self
202            .config
203            .account_name
204            .clone()
205            .or_else(|| raw::azure_account_name_from_endpoint(endpoint.as_str()));
206
207        let account_name = match account_name_option {
208            Some(account_name) => Ok(account_name),
209            None => Err(
210                Error::new(ErrorKind::ConfigInvalid, "account_name is empty")
211                    .with_operation("Builder::build")
212                    .with_context("service", Scheme::Azfile),
213            ),
214        }?;
215
216        let config_loader = AzureStorageConfig {
217            account_name: Some(account_name),
218            account_key: self.config.account_key.clone(),
219            sas_token: self.config.sas_token.clone(),
220            ..Default::default()
221        };
222
223        let cred_loader = AzureStorageLoader::new(config_loader);
224        let signer = AzureStorageSigner::new();
225        Ok(AzfileBackend {
226            core: Arc::new(AzfileCore {
227                info: {
228                    let am = AccessorInfo::default();
229                    am.set_scheme(Scheme::Azfile)
230                        .set_root(&root)
231                        .set_native_capability(Capability {
232                            stat: true,
233                            stat_has_cache_control: true,
234                            stat_has_content_length: true,
235                            stat_has_content_type: true,
236                            stat_has_content_encoding: true,
237                            stat_has_content_range: true,
238                            stat_has_etag: true,
239                            stat_has_content_md5: true,
240                            stat_has_last_modified: true,
241                            stat_has_content_disposition: true,
242
243                            read: true,
244
245                            write: true,
246                            create_dir: true,
247                            delete: true,
248                            rename: true,
249
250                            list: true,
251                            list_has_etag: true,
252                            list_has_last_modified: true,
253                            list_has_content_length: true,
254
255                            shared: true,
256
257                            ..Default::default()
258                        });
259
260                    // allow deprecated api here for compatibility
261                    #[allow(deprecated)]
262                    if let Some(client) = self.http_client {
263                        am.update_http_client(|_| client);
264                    }
265
266                    am.into()
267                },
268                root,
269                endpoint,
270                loader: cred_loader,
271                signer,
272                share_name: self.config.share_name.clone(),
273            }),
274        })
275    }
276}
277
278/// Backend for azfile services.
279#[derive(Debug, Clone)]
280pub struct AzfileBackend {
281    core: Arc<AzfileCore>,
282}
283
284impl Access for AzfileBackend {
285    type Reader = HttpBody;
286    type Writer = AzfileWriters;
287    type Lister = oio::PageLister<AzfileLister>;
288    type Deleter = oio::OneShotDeleter<AzfileDeleter>;
289
290    fn info(&self) -> Arc<AccessorInfo> {
291        self.core.info.clone()
292    }
293
294    async fn create_dir(&self, path: &str, _: OpCreateDir) -> Result<RpCreateDir> {
295        self.core.ensure_parent_dir_exists(path).await?;
296        let resp = self.core.azfile_create_dir(path).await?;
297        let status = resp.status();
298
299        match status {
300            StatusCode::CREATED => Ok(RpCreateDir::default()),
301            _ => {
302                // we cannot just check status code because 409 Conflict has two meaning:
303                // 1. If a directory by the same name is being deleted when Create Directory is called, the server returns status code 409 (Conflict)
304                // 2. If a directory or file with the same name already exists, the operation fails with status code 409 (Conflict).
305                // but we just need case 2 (already exists)
306                // ref: https://learn.microsoft.com/en-us/rest/api/storageservices/create-directory
307                if resp
308                    .headers()
309                    .get("x-ms-error-code")
310                    .map(|value| value.to_str().unwrap_or(""))
311                    .unwrap_or_else(|| "")
312                    == "ResourceAlreadyExists"
313                {
314                    Ok(RpCreateDir::default())
315                } else {
316                    Err(parse_error(resp))
317                }
318            }
319        }
320    }
321
322    async fn stat(&self, path: &str, _: OpStat) -> Result<RpStat> {
323        let resp = if path.ends_with('/') {
324            self.core.azfile_get_directory_properties(path).await?
325        } else {
326            self.core.azfile_get_file_properties(path).await?
327        };
328
329        let status = resp.status();
330        match status {
331            StatusCode::OK => {
332                let meta = parse_into_metadata(path, resp.headers())?;
333                Ok(RpStat::new(meta))
334            }
335            _ => Err(parse_error(resp)),
336        }
337    }
338
339    async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
340        let resp = self.core.azfile_read(path, args.range()).await?;
341
342        let status = resp.status();
343        match status {
344            StatusCode::OK | StatusCode::PARTIAL_CONTENT => Ok((RpRead::new(), resp.into_body())),
345            _ => {
346                let (part, mut body) = resp.into_parts();
347                let buf = body.to_buffer().await?;
348                Err(parse_error(Response::from_parts(part, buf)))
349            }
350        }
351    }
352
353    async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::Writer)> {
354        self.core.ensure_parent_dir_exists(path).await?;
355        let w = AzfileWriter::new(self.core.clone(), args.clone(), path.to_string());
356        let w = if args.append() {
357            AzfileWriters::Two(oio::AppendWriter::new(w))
358        } else {
359            AzfileWriters::One(oio::OneShotWriter::new(w))
360        };
361        Ok((RpWrite::default(), w))
362    }
363
364    async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> {
365        Ok((
366            RpDelete::default(),
367            oio::OneShotDeleter::new(AzfileDeleter::new(self.core.clone())),
368        ))
369    }
370
371    async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> {
372        let l = AzfileLister::new(self.core.clone(), path.to_string(), args.limit());
373
374        Ok((RpList::default(), oio::PageLister::new(l)))
375    }
376
377    async fn rename(&self, from: &str, to: &str, _: OpRename) -> Result<RpRename> {
378        self.core.ensure_parent_dir_exists(to).await?;
379        let resp = self.core.azfile_rename(from, to).await?;
380        let status = resp.status();
381        match status {
382            StatusCode::OK => Ok(RpRename::default()),
383            _ => Err(parse_error(resp)),
384        }
385    }
386}