diff --git a/core/src/raw/ops.rs b/core/src/raw/ops.rs index 68dc5f1e4856..978808f2b461 100644 --- a/core/src/raw/ops.rs +++ b/core/src/raw/ops.rs @@ -19,7 +19,7 @@ //! //! By using ops, users can add more context for operation. -use crate::raw::*; +use crate::{options, raw::*}; use chrono::{DateTime, Utc}; use std::collections::HashMap; use std::time::Duration; @@ -65,6 +65,14 @@ impl OpDelete { } } +impl From for OpDelete { + fn from(value: options::DeleteOptions) -> Self { + Self { + version: value.version, + } + } +} + /// Args for `delete` operation. /// /// The path must be normalized. @@ -469,6 +477,54 @@ impl OpReader { } } +impl From for (OpRead, OpReader) { + fn from(value: options::ReadOptions) -> Self { + ( + OpRead { + range: value.range, + if_match: value.if_match, + if_none_match: value.if_none_match, + if_modified_since: value.if_modified_since, + if_unmodified_since: value.if_unmodified_since, + override_content_type: value.override_content_type, + override_cache_control: value.override_cache_control, + override_content_disposition: value.override_content_disposition, + version: value.version, + }, + OpReader { + // Ensure concurrent is at least 1 + concurrent: value.concurrent.max(1), + chunk: value.chunk, + gap: value.gap, + }, + ) + } +} + +impl From for (OpRead, OpReader) { + fn from(value: options::ReaderOptions) -> Self { + ( + OpRead { + range: BytesRange::default(), + if_match: value.if_match, + if_none_match: value.if_none_match, + if_modified_since: value.if_modified_since, + if_unmodified_since: value.if_unmodified_since, + override_content_type: None, + override_cache_control: None, + override_content_disposition: None, + version: value.version, + }, + OpReader { + // Ensure concurrent is at least 1 + concurrent: value.concurrent.max(1), + chunk: value.chunk, + gap: value.gap, + }, + ) + } +} + /// Args for `stat` operation. #[derive(Debug, Clone, Default)] pub struct OpStat { @@ -578,6 +634,21 @@ impl OpStat { } } +impl From for OpStat { + fn from(value: options::StatOptions) -> Self { + Self { + if_match: value.if_match, + if_none_match: value.if_none_match, + if_modified_since: value.if_modified_since, + if_unmodified_since: value.if_unmodified_since, + override_content_type: value.override_content_type, + override_cache_control: value.override_cache_control, + override_content_disposition: value.override_content_disposition, + version: value.version, + } + } +} + /// Args for `write` operation. #[derive(Debug, Clone, Default)] pub struct OpWrite { @@ -754,6 +825,27 @@ impl OpWriter { } } +impl From for (OpWrite, OpWriter) { + fn from(value: options::WriteOptions) -> Self { + ( + OpWrite { + append: value.append, + // Ensure concurrent is at least 1 + concurrent: value.concurrent.max(1), + content_type: value.content_type, + content_disposition: value.content_disposition, + content_encoding: value.content_encoding, + cache_control: value.cache_control, + if_match: value.if_match, + if_none_match: value.if_none_match, + if_not_exists: value.if_not_exists, + user_metadata: value.user_metadata, + }, + OpWriter { chunk: value.chunk }, + ) + } +} + /// Args for `copy` operation. #[derive(Debug, Clone, Default)] pub struct OpCopy {} diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index 3f7afc58f206..f4bd6731c651 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -72,5 +72,7 @@ pub use scheme::Scheme; mod capability; pub use capability::Capability; +pub mod options; + mod context; pub(crate) use context::*; diff --git a/core/src/types/operator/operator.rs b/core/src/types/operator/operator.rs index ffe4b45f7c0d..aabdab86391d 100644 --- a/core/src/types/operator/operator.rs +++ b/core/src/types/operator/operator.rs @@ -290,101 +290,7 @@ impl Operator { /// /// # Options /// - /// ## `if_match` - /// - /// Set `if_match` for this `stat` request. - /// - /// This feature can be used to check if the file's `ETag` matches the given `ETag`. - /// - /// If file exists, and it's etag doesn't match, an error with kind [`ErrorKind::ConditionNotMatch`] - /// will be returned. - /// - /// ``` - /// # use opendal::Result; - /// use opendal::Operator; - /// - /// # async fn test(op: Operator, etag: &str) -> Result<()> { - /// let mut metadata = op.stat_with("path/to/file").if_match(etag).await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// ## `if_none_match` - /// - /// Set `if_none_match` for this `stat` request. - /// - /// This feature can be used to check if the file's `ETag` doesn't match the given `ETag`. - /// - /// If file exists, and it's etag match, an error with kind [`ErrorKind::ConditionNotMatch`] - /// will be returned. - /// - /// ``` - /// # use opendal::Result; - /// use opendal::Operator; - /// - /// # async fn test(op: Operator, etag: &str) -> Result<()> { - /// let mut metadata = op.stat_with("path/to/file").if_none_match(etag).await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// ## `if_modified_since` - /// - /// set `if_modified_since` for this `stat` request. - /// - /// This feature can be used to check if the file has been modified since the given time. - /// - /// If file exists, and it's not modified after the given time, an error with kind [`ErrorKind::ConditionNotMatch`] - /// will be returned. - /// - /// ``` - /// # use opendal::Result; - /// use opendal::Operator; - /// use chrono::Utc; - /// - /// # async fn test(op: Operator) -> Result<()> { - /// let mut metadata = op.stat_with("path/to/file").if_modified_since(Utc::now()).await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// ## `if_unmodified_since` - /// - /// set `if_unmodified_since` for this `stat` request. - /// - /// This feature can be used to check if the file has NOT been modified since the given time. - /// - /// If file exists, and it's modified after the given time, an error with kind [`ErrorKind::ConditionNotMatch`] - /// will be returned. - /// - /// ``` - /// # use opendal::Result; - /// use opendal::Operator; - /// use chrono::Utc; - /// - /// # async fn test(op: Operator) -> Result<()> { - /// let mut metadata = op.stat_with("path/to/file").if_unmodified_since(Utc::now()).await?; - /// # Ok(()) - /// # } - /// ``` - /// - /// ## `version` - /// - /// Set `version` for this `stat` request. - /// - /// This feature can be used to retrieve the metadata of a specific version of the given path - /// - /// If the version doesn't exist, an error with kind [`ErrorKind::NotFound`] will be returned. - /// - /// ``` - /// # use opendal::Result; - /// # use opendal::Operator; - /// - /// # async fn test(op: Operator, version: &str) -> Result<()> { - /// let mut metadata = op.stat_with("path/to/file").version(version).await?; - /// # Ok(()) - /// # } - /// ``` + /// Check [`options::StatOptions`] for all available options. /// /// # Examples /// @@ -445,18 +351,29 @@ impl Operator { /// we can do about this. pub fn stat_with(&self, path: &str) -> FutureStat>> { let path = normalize_path(path); - OperatorFuture::new( self.inner().clone(), path, - OpStat::default(), - |inner, path, args| async move { - let rp = inner.stat(&path, args).await?; - Ok(rp.into_metadata()) - }, + options::StatOptions::default(), + Self::stat_inner, ) } + /// Get given path's metadata with extra options. + pub async fn stat_options(&self, path: &str, opts: options::StatOptions) -> Result { + let path = normalize_path(path); + Self::stat_inner(self.accessor.clone(), path, opts).await + } + + async fn stat_inner( + acc: Accessor, + path: String, + opts: options::StatOptions, + ) -> Result { + let rp = acc.stat(&path, opts.into()).await?; + Ok(rp.into_metadata()) + } + /// Check if this path exists or not. /// /// # Example @@ -594,16 +511,7 @@ impl Operator { /// /// # Options /// - /// Visit [`FutureRead`] for all available options. - /// - /// - [`range`](./operator_futures/type.FutureRead.html#method.version): Set `range` for the read. - /// - [`concurrent`](./operator_futures/type.FutureRead.html#method.concurrent): Set `concurrent` for the read. - /// - [`chunk`](./operator_futures/type.FutureRead.html#method.chunk): Set `chunk` for the read. - /// - [`version`](./operator_futures/type.FutureRead.html#method.version): Set `version` for the read. - /// - [`if_match`](./operator_futures/type.FutureRead.html#method.if_match): Set `if-match` for the read. - /// - [`if_none_match`](./operator_futures/type.FutureRead.html#method.if_none_match): Set `if-none-match` for the read. - /// - [`if_modified_since`](./operator_futures/type.FutureRead.html#method.if_modified_since): Set `if-modified-since` for the read. - /// - [`if_unmodified_since`](./operator_futures/type.FutureRead.html#method.if_unmodified_since): Set `if-unmodified-since` for the read. + /// Visit [`options::ReadOptions`] for all available options. /// /// # Examples /// @@ -625,26 +533,35 @@ impl Operator { OperatorFuture::new( self.inner().clone(), path, - (OpRead::default(), OpReader::default()), - |inner, path, (args, options)| async move { - if !validate_path(&path, EntryMode::FILE) { - return Err( - Error::new(ErrorKind::IsADirectory, "read path is a directory") - .with_operation("read") - .with_context("service", inner.info().scheme()) - .with_context("path", &path), - ); - } - - let range = args.range(); - let context = ReadContext::new(inner, path, args, options); - let r = Reader::new(context); - let buf = r.read(range.to_range()).await?; - Ok(buf) - }, + options::ReadOptions::default(), + Self::read_inner, ) } + /// Read the whole path into a bytes with extra options. + pub async fn read_options(&self, path: &str, opts: options::ReadOptions) -> Result { + let path = normalize_path(path); + Self::read_inner(self.inner().clone(), path, opts).await + } + + async fn read_inner(acc: Accessor, path: String, opts: options::ReadOptions) -> Result { + if !validate_path(&path, EntryMode::FILE) { + return Err( + Error::new(ErrorKind::IsADirectory, "read path is a directory") + .with_operation("read") + .with_context("service", acc.info().scheme()) + .with_context("path", &path), + ); + } + + let (args, opts) = opts.into(); + let range = args.range(); + let context = ReadContext::new(acc, path, args, opts); + let r = Reader::new(context); + let buf = r.read(range.to_range()).await?; + Ok(buf) + } + /// Create a new reader which can read the whole path. /// /// # Notes @@ -681,16 +598,7 @@ impl Operator { /// /// # Options /// - /// Visit [`FutureReader`] for all available options. - /// - /// - [`version`](./operator_futures/type.FutureReader.html#method.version): Set `version` for the reader. - /// - [`concurrent`](./operator_futures/type.FutureReader.html#method.concurrent): Set `concurrent` for the reader. - /// - [`chunk`](./operator_futures/type.FutureReader.html#method.chunk): Set `chunk` for the reader. - /// - [`gap`](./operator_futures/type.FutureReader.html#method.gap): Set `gap` for the reader. - /// - [`if_match`](./operator_futures/type.FutureReader.html#method.if_match): Set `if-match` for the reader. - /// - [`if_none_match`](./operator_futures/type.FutureReader.html#method.if_none_match): Set `if-none-match` for the reader. - /// - [`if_modified_since`](./operator_futures/type.FutureReader.html#method.if_modified_since): Set `if-modified-since` for the reader. - /// - [`if_unmodified_since`](./operator_futures/type.FutureReader.html#method.if_unmodified_since): Set `if-unmodified-since` for the reader. + /// Visit [`options::ReaderOptions`] for all available options. /// /// # Examples /// @@ -709,23 +617,39 @@ impl Operator { OperatorFuture::new( self.inner().clone(), path, - (OpRead::default(), OpReader::default()), - |inner, path, (args, options)| async move { - if !validate_path(&path, EntryMode::FILE) { - return Err( - Error::new(ErrorKind::IsADirectory, "read path is a directory") - .with_operation("Operator::reader") - .with_context("service", inner.info().scheme()) - .with_context("path", path), - ); - } - - let context = ReadContext::new(inner, path, args, options); - Ok(Reader::new(context)) - }, + options::ReaderOptions::default(), + Self::reader_inner, ) } + /// Create a new reader with extra options + pub async fn reader_options(&self, path: &str, opts: options::ReaderOptions) -> Result { + let path = normalize_path(path); + Self::reader_inner(self.inner().clone(), path, opts).await + } + + /// Allow this unused async since we don't want + /// to change our public API. + #[allow(clippy::unused_async)] + async fn reader_inner( + acc: Accessor, + path: String, + options: options::ReaderOptions, + ) -> Result { + if !validate_path(&path, EntryMode::FILE) { + return Err( + Error::new(ErrorKind::IsADirectory, "read path is a directory") + .with_operation("Operator::reader") + .with_context("service", acc.info().scheme()) + .with_context("path", path), + ); + } + + let (args, opts) = options.into(); + let context = ReadContext::new(acc, path, args, opts); + Ok(Reader::new(context)) + } + /// Write bytes into path. /// /// # Notes @@ -765,119 +689,98 @@ impl Operator { self.write_with(path, bs).await } - /// Copy a file from `from` to `to`. + /// Write data with extra options. /// /// # Notes /// - /// - `from` and `to` must be a file. - /// - `to` will be overwritten if it exists. - /// - If `from` and `to` are the same, an `IsSameFile` error will occur. - /// - `copy` is idempotent. For same `from` and `to` input, the result will be the same. + /// ## Streaming Write /// - /// # Examples + /// This method performs a single bulk write operation for all bytes. For finer-grained + /// memory control or lazy writing, consider using [`Operator::writer_with`] instead. /// - /// ``` - /// # use opendal::Result; - /// # use opendal::Operator; + /// ## Multipart Uploads /// - /// # async fn test(op: Operator) -> Result<()> { - /// op.copy("path/to/file", "path/to/file2").await?; - /// # Ok(()) - /// # } - /// ``` - pub async fn copy(&self, from: &str, to: &str) -> Result<()> { - let from = normalize_path(from); - - if !validate_path(&from, EntryMode::FILE) { - return Err( - Error::new(ErrorKind::IsADirectory, "from path is a directory") - .with_operation("Operator::copy") - .with_context("service", self.info().scheme()) - .with_context("from", from), - ); - } - - let to = normalize_path(to); - - if !validate_path(&to, EntryMode::FILE) { - return Err( - Error::new(ErrorKind::IsADirectory, "to path is a directory") - .with_operation("Operator::copy") - .with_context("service", self.info().scheme()) - .with_context("to", to), - ); - } - - if from == to { - return Err( - Error::new(ErrorKind::IsSameFile, "from and to paths are same") - .with_operation("Operator::copy") - .with_context("service", self.info().scheme()) - .with_context("from", from) - .with_context("to", to), - ); - } - - self.inner().copy(&from, &to, OpCopy::new()).await?; - - Ok(()) - } - - /// Rename a file from `from` to `to`. + /// OpenDAL handles multipart uploads through the [`Writer`] abstraction, managing all + /// the upload details automatically. You can customize the upload behavior by configuring + /// `chunk` size and `concurrent` operations via [`Operator::writer_with`]. /// - /// # Notes + /// # Options /// - /// - `from` and `to` must be a file. - /// - `to` will be overwritten if it exists. - /// - If `from` and `to` are the same, an `IsSameFile` error will occur. + /// Visit [`FutureWrite`] for all available options. + /// + /// - [`append`](./operator_futures/type.FutureWrite.html#method.append): Sets append mode for this write request. + /// - [`chunk`](./operator_futures/type.FutureWrite.html#method.chunk): Sets chunk size for buffered writes. + /// - [`concurrent`](./operator_futures/type.FutureWrite.html#method.concurrent): Sets concurrent write operations for this writer. + /// - [`cache_control`](./operator_futures/type.FutureWrite.html#method.cache_control): Sets cache control for this write request. + /// - [`content_type`](./operator_futures/type.FutureWrite.html#method.content_type): Sets content type for this write request. + /// - [`content_disposition`](./operator_futures/type.FutureWrite.html#method.content_disposition): Sets content disposition for this write request. + /// - [`content_encoding`](./operator_futures/type.FutureWrite.html#method.content_encoding): Sets content encoding for this write request. + /// - [`if_match`](./operator_futures/type.FutureWrite.html#method.if_match): Sets if-match for this write request. + /// - [`if_none_match`](./operator_futures/type.FutureWrite.html#method.if_none_match): Sets if-none-match for this write request. + /// - [`if_not_exist`](./operator_futures/type.FutureWrite.html#method.if_not_exist): Sets if-not-exist for this write request. + /// - [`user_metadata`](./operator_futures/type.FutureWrite.html#method.user_metadata): Sets user metadata for this write request. /// /// # Examples /// /// ``` /// # use opendal::Result; /// # use opendal::Operator; + /// use bytes::Bytes; /// /// # async fn test(op: Operator) -> Result<()> { - /// op.rename("path/to/file", "path/to/file2").await?; + /// let _ = op.write_with("path/to/file", vec![0; 4096]) + /// .if_not_exists(true) + /// .await?; /// # Ok(()) /// # } /// ``` - pub async fn rename(&self, from: &str, to: &str) -> Result<()> { - let from = normalize_path(from); - - if !validate_path(&from, EntryMode::FILE) { - return Err( - Error::new(ErrorKind::IsADirectory, "from path is a directory") - .with_operation("Operator::move_") - .with_context("service", self.info().scheme()) - .with_context("from", from), - ); - } + pub fn write_with( + &self, + path: &str, + bs: impl Into, + ) -> FutureWrite>> { + let path = normalize_path(path); + let bs = bs.into(); - let to = normalize_path(to); + OperatorFuture::new( + self.inner().clone(), + path, + (options::WriteOptions::default(), bs), + |inner, path, (opts, bs)| Self::write_inner(inner, path, bs, opts), + ) + } - if !validate_path(&to, EntryMode::FILE) { - return Err( - Error::new(ErrorKind::IsADirectory, "to path is a directory") - .with_operation("Operator::move_") - .with_context("service", self.info().scheme()) - .with_context("to", to), - ); - } + /// Write data with extra options. + pub async fn write_options( + &self, + path: &str, + bs: impl Into, + opts: options::WriteOptions, + ) -> Result { + let path = normalize_path(path); + Self::write_inner(self.inner().clone(), path, bs.into(), opts).await + } - if from == to { + async fn write_inner( + acc: Accessor, + path: String, + bs: Buffer, + opts: options::WriteOptions, + ) -> Result { + if !validate_path(&path, EntryMode::FILE) { return Err( - Error::new(ErrorKind::IsSameFile, "from and to paths are same") - .with_operation("Operator::move_") - .with_context("service", self.info().scheme()) - .with_context("from", from) - .with_context("to", to), + Error::new(ErrorKind::IsADirectory, "write path is a directory") + .with_operation("Operator::write") + .with_context("service", acc.info().scheme()) + .with_context("path", &path), ); } - self.inner().rename(&from, &to, OpRename::new()).await?; - - Ok(()) + let (args, opts) = opts.into(); + let context = WriteContext::new(acc, path, args, opts); + let mut w = Writer::new(context).await?; + w.write(bs).await?; + w.close().await } /// Create a writer for streaming data to the given path. @@ -932,19 +835,7 @@ impl Operator { /// /// ## Options /// - /// Visit [`FutureWriter`] for all available options. - /// - /// - [`append`](./operator_futures/type.FutureWriter.html#method.append): Sets append mode for this write request. - /// - [`chunk`](./operator_futures/type.FutureWriter.html#method.chunk): Sets chunk size for buffered writes. - /// - [`concurrent`](./operator_futures/type.FutureWriter.html#method.concurrent): Sets concurrent write operations for this writer. - /// - [`cache_control`](./operator_futures/type.FutureWriter.html#method.cache_control): Sets cache control for this write request. - /// - [`content_type`](./operator_futures/type.FutureWriter.html#method.content_type): Sets content type for this write request. - /// - [`content_disposition`](./operator_futures/type.FutureWriter.html#method.content_disposition): Sets content disposition for this write request. - /// - [`content_encoding`](./operator_futures/type.FutureWriter.html#method.content_encoding): Sets content encoding for this write request. - /// - [`if_match`](./operator_futures/type.FutureWriter.html#method.if_match): Sets if-match for this write request. - /// - [`if_none_match`](./operator_futures/type.FutureWriter.html#method.if_none_match): Sets if-none-match for this write request. - /// - [`if_not_exist`](./operator_futures/type.FutureWriter.html#method.if_not_exist): Sets if-not-exist for this write request. - /// - [`user_metadata`](./operator_futures/type.FutureWriter.html#method.user_metadata): Sets user metadata for this write request. + /// Visit [`options::WriteOptions`] for all available options. /// /// ## Examples /// @@ -970,97 +861,154 @@ impl Operator { OperatorFuture::new( self.inner().clone(), path, - (OpWrite::default(), OpWriter::default()), - |inner, path, (args, options)| async move { - if !validate_path(&path, EntryMode::FILE) { - return Err( - Error::new(ErrorKind::IsADirectory, "write path is a directory") - .with_operation("Operator::writer") - .with_context("service", inner.info().scheme().into_static()) - .with_context("path", &path), - ); - } - - let context = WriteContext::new(inner, path, args, options); - let w = Writer::new(context).await?; - Ok(w) - }, + options::WriteOptions::default(), + Self::writer_inner, ) } - /// Write data with extra options. + /// Create a writer for streaming data to the given path with more options. /// - /// # Notes + /// ## Options /// - /// ## Streaming Write + /// Visit [`options::WriteOptions`] for all available options. + pub async fn writer_options(&self, path: &str, opts: options::WriteOptions) -> Result { + let path = normalize_path(path); + Self::writer_inner(self.inner().clone(), path, opts).await + } + + async fn writer_inner( + acc: Accessor, + path: String, + opts: options::WriteOptions, + ) -> Result { + if !validate_path(&path, EntryMode::FILE) { + return Err( + Error::new(ErrorKind::IsADirectory, "write path is a directory") + .with_operation("Operator::writer") + .with_context("service", acc.info().scheme().into_static()) + .with_context("path", &path), + ); + } + + let (args, opts) = opts.into(); + let context = WriteContext::new(acc, path, args, opts); + let w = Writer::new(context).await?; + Ok(w) + } + + /// Copy a file from `from` to `to`. /// - /// This method performs a single bulk write operation for all bytes. For finer-grained - /// memory control or lazy writing, consider using [`Operator::writer_with`] instead. + /// # Notes /// - /// ## Multipart Uploads + /// - `from` and `to` must be a file. + /// - `to` will be overwritten if it exists. + /// - If `from` and `to` are the same, an `IsSameFile` error will occur. + /// - `copy` is idempotent. For same `from` and `to` input, the result will be the same. /// - /// OpenDAL handles multipart uploads through the [`Writer`] abstraction, managing all - /// the upload details automatically. You can customize the upload behavior by configuring - /// `chunk` size and `concurrent` operations via [`Operator::writer_with`]. + /// # Examples /// - /// # Options + /// ``` + /// # use opendal::Result; + /// # use opendal::Operator; /// - /// Visit [`FutureWrite`] for all available options. + /// # async fn test(op: Operator) -> Result<()> { + /// op.copy("path/to/file", "path/to/file2").await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn copy(&self, from: &str, to: &str) -> Result<()> { + let from = normalize_path(from); + + if !validate_path(&from, EntryMode::FILE) { + return Err( + Error::new(ErrorKind::IsADirectory, "from path is a directory") + .with_operation("Operator::copy") + .with_context("service", self.info().scheme()) + .with_context("from", from), + ); + } + + let to = normalize_path(to); + + if !validate_path(&to, EntryMode::FILE) { + return Err( + Error::new(ErrorKind::IsADirectory, "to path is a directory") + .with_operation("Operator::copy") + .with_context("service", self.info().scheme()) + .with_context("to", to), + ); + } + + if from == to { + return Err( + Error::new(ErrorKind::IsSameFile, "from and to paths are same") + .with_operation("Operator::copy") + .with_context("service", self.info().scheme()) + .with_context("from", from) + .with_context("to", to), + ); + } + + self.inner().copy(&from, &to, OpCopy::new()).await?; + + Ok(()) + } + + /// Rename a file from `from` to `to`. /// - /// - [`append`](./operator_futures/type.FutureWrite.html#method.append): Sets append mode for this write request. - /// - [`chunk`](./operator_futures/type.FutureWrite.html#method.chunk): Sets chunk size for buffered writes. - /// - [`concurrent`](./operator_futures/type.FutureWrite.html#method.concurrent): Sets concurrent write operations for this writer. - /// - [`cache_control`](./operator_futures/type.FutureWrite.html#method.cache_control): Sets cache control for this write request. - /// - [`content_type`](./operator_futures/type.FutureWrite.html#method.content_type): Sets content type for this write request. - /// - [`content_disposition`](./operator_futures/type.FutureWrite.html#method.content_disposition): Sets content disposition for this write request. - /// - [`content_encoding`](./operator_futures/type.FutureWrite.html#method.content_encoding): Sets content encoding for this write request. - /// - [`if_match`](./operator_futures/type.FutureWrite.html#method.if_match): Sets if-match for this write request. - /// - [`if_none_match`](./operator_futures/type.FutureWrite.html#method.if_none_match): Sets if-none-match for this write request. - /// - [`if_not_exist`](./operator_futures/type.FutureWrite.html#method.if_not_exist): Sets if-not-exist for this write request. - /// - [`user_metadata`](./operator_futures/type.FutureWrite.html#method.user_metadata): Sets user metadata for this write request. + /// # Notes + /// + /// - `from` and `to` must be a file. + /// - `to` will be overwritten if it exists. + /// - If `from` and `to` are the same, an `IsSameFile` error will occur. /// /// # Examples /// /// ``` /// # use opendal::Result; /// # use opendal::Operator; - /// use bytes::Bytes; /// /// # async fn test(op: Operator) -> Result<()> { - /// let _ = op.write_with("path/to/file", vec![0; 4096]) - /// .if_not_exists(true) - /// .await?; + /// op.rename("path/to/file", "path/to/file2").await?; /// # Ok(()) /// # } /// ``` - pub fn write_with( - &self, - path: &str, - bs: impl Into, - ) -> FutureWrite>> { - let path = normalize_path(path); - let bs = bs.into(); + pub async fn rename(&self, from: &str, to: &str) -> Result<()> { + let from = normalize_path(from); - OperatorFuture::new( - self.inner().clone(), - path, - (OpWrite::default(), OpWriter::default(), bs), - |inner, path, (args, options, bs)| async move { - if !validate_path(&path, EntryMode::FILE) { - return Err( - Error::new(ErrorKind::IsADirectory, "write path is a directory") - .with_operation("Operator::write_with") - .with_context("service", inner.info().scheme().into_static()) - .with_context("path", &path), - ); - } + if !validate_path(&from, EntryMode::FILE) { + return Err( + Error::new(ErrorKind::IsADirectory, "from path is a directory") + .with_operation("Operator::move_") + .with_context("service", self.info().scheme()) + .with_context("from", from), + ); + } - let context = WriteContext::new(inner, path, args, options); - let mut w = Writer::new(context).await?; - w.write(bs).await?; - w.close().await - }, - ) + let to = normalize_path(to); + + if !validate_path(&to, EntryMode::FILE) { + return Err( + Error::new(ErrorKind::IsADirectory, "to path is a directory") + .with_operation("Operator::move_") + .with_context("service", self.info().scheme()) + .with_context("to", to), + ); + } + + if from == to { + return Err( + Error::new(ErrorKind::IsSameFile, "from and to paths are same") + .with_operation("Operator::move_") + .with_context("service", self.info().scheme()) + .with_context("from", from) + .with_context("to", to), + ); + } + + self.inner().rename(&from, &to, OpRename::new()).await?; + + Ok(()) } /// Delete the given path. @@ -1128,16 +1076,29 @@ impl Operator { OperatorFuture::new( self.inner().clone(), path, - OpDelete::default(), - |inner, path, args| async move { - let (_, mut deleter) = inner.delete_dyn().await?; - deleter.delete_dyn(&path, args)?; - deleter.flush_dyn().await?; - Ok(()) - }, + options::DeleteOptions::default(), + Self::delete_inner, ) } + /// Delete the given path with extra options. + /// + /// ## Options + /// + /// Visit [`options::DeleteOptions`] for all available options. + pub async fn delete_options(&self, path: &str, opts: options::DeleteOptions) -> Result<()> { + let path = normalize_path(path); + Self::delete_inner(self.inner().clone(), path, opts).await + } + + async fn delete_inner(acc: Accessor, path: String, opts: options::DeleteOptions) -> Result<()> { + let (_, mut deleter) = acc.delete_dyn().await?; + let args = opts.into(); + deleter.delete_dyn(&path, args)?; + deleter.flush_dyn().await?; + Ok(()) + } + /// Delete an infallible iterator of paths. /// /// Also see: diff --git a/core/src/types/operator/operator_futures.rs b/core/src/types/operator/operator_futures.rs index 72ef6bd62049..2e8a537d39a1 100644 --- a/core/src/types/operator/operator_futures.rs +++ b/core/src/types/operator/operator_futures.rs @@ -91,32 +91,47 @@ where /// Future that generated by [`Operator::stat_with`]. /// /// Users can add more options by public functions provided by this struct. -pub type FutureStat = OperatorFuture; +pub type FutureStat = OperatorFuture; impl>> FutureStat { /// Set the If-Match for this operation. - pub fn if_match(self, v: &str) -> Self { - self.map(|args| args.with_if_match(v)) + /// + /// Refer to [`options::StatOptions::if_match`] for more details. + pub fn if_match(mut self, v: &str) -> Self { + self.args.if_match = Some(v.to_string()); + self } /// Set the If-None-Match for this operation. - pub fn if_none_match(self, v: &str) -> Self { - self.map(|args| args.with_if_none_match(v)) + /// + /// Refer to [`options::StatOptions::if_none_match`] for more details. + pub fn if_none_match(mut self, v: &str) -> Self { + self.args.if_none_match = Some(v.to_string()); + self } /// Set the If-Modified-Since for this operation. - pub fn if_modified_since(self, v: DateTime) -> Self { - self.map(|args| args.with_if_modified_since(v)) + /// + /// Refer to [`options::StatOptions::if_modified_since`] for more details. + pub fn if_modified_since(mut self, v: DateTime) -> Self { + self.args.if_modified_since = Some(v); + self } /// Set the If-Unmodified-Since for this operation. - pub fn if_unmodified_since(self, v: DateTime) -> Self { - self.map(|args| args.with_if_unmodified_since(v)) + /// + /// Refer to [`options::StatOptions::if_unmodified_since`] for more details. + pub fn if_unmodified_since(mut self, v: DateTime) -> Self { + self.args.if_unmodified_since = Some(v); + self } /// Set the version for this operation. - pub fn version(self, v: &str) -> Self { - self.map(|args| args.with_version(v)) + /// + /// Refer to [`options::StatOptions::version`] for more details. + pub fn version(mut self, v: &str) -> Self { + self.args.version = Some(v.to_string()); + self } } @@ -221,7 +236,7 @@ impl>> FuturePresignWrite { /// Future that generated by [`Operator::read_with`]. /// /// Users can add more options by public functions provided by this struct. -pub type FutureRead = OperatorFuture<(OpRead, OpReader), Buffer, F>; +pub type FutureRead = OperatorFuture; impl>> FutureRead { /// Set `range` for this `read` request. @@ -241,8 +256,9 @@ impl>> FutureRead { /// # Ok(()) /// # } /// ``` - pub fn range(self, range: impl RangeBounds) -> Self { - self.map(|(args, op_reader)| (args.with_range(range.into()), op_reader)) + pub fn range(mut self, range: impl RangeBounds) -> Self { + self.args.range = range.into(); + self } /// Set `concurrent` for the reader. @@ -263,8 +279,9 @@ impl>> FutureRead { /// # Ok(()) /// # } /// ``` - pub fn concurrent(self, concurrent: usize) -> Self { - self.map(|(args, op_reader)| (args, op_reader.with_concurrent(concurrent))) + pub fn concurrent(mut self, concurrent: usize) -> Self { + self.args.concurrent = concurrent.max(1); + self } /// OpenDAL will use services' preferred chunk size by default. Users can set chunk based on their own needs. @@ -280,8 +297,9 @@ impl>> FutureRead { /// # Ok(()) /// # } /// ``` - pub fn chunk(self, chunk_size: usize) -> Self { - self.map(|(args, op_reader)| (args, op_reader.with_chunk(chunk_size))) + pub fn chunk(mut self, chunk_size: usize) -> Self { + self.args.chunk = Some(chunk_size); + self } /// Set `version` for this `read` request. @@ -299,8 +317,9 @@ impl>> FutureRead { /// # Ok(()) /// # } /// ``` - pub fn version(self, v: &str) -> Self { - self.map(|(args, op_reader)| (args.with_version(v), op_reader)) + pub fn version(mut self, v: &str) -> Self { + self.args.version = Some(v.to_string()); + self } /// Set `if_match` for this `read` request. @@ -318,8 +337,9 @@ impl>> FutureRead { /// # Ok(()) /// # } /// ``` - pub fn if_match(self, v: &str) -> Self { - self.map(|(args, op_reader)| (args.with_if_match(v), op_reader)) + pub fn if_match(mut self, v: &str) -> Self { + self.args.if_match = Some(v.to_string()); + self } /// Set `if_none_match` for this `read` request. @@ -337,8 +357,9 @@ impl>> FutureRead { /// # Ok(()) /// # } /// ``` - pub fn if_none_match(self, v: &str) -> Self { - self.map(|(args, op_reader)| (args.with_if_none_match(v), op_reader)) + pub fn if_none_match(mut self, v: &str) -> Self { + self.args.if_none_match = Some(v.to_string()); + self } /// ## `if_modified_since` @@ -360,8 +381,9 @@ impl>> FutureRead { /// # Ok(()) /// # } /// ``` - pub fn if_modified_since(self, v: DateTime) -> Self { - self.map(|(args, op_reader)| (args.with_if_modified_since(v), op_reader)) + pub fn if_modified_since(mut self, v: DateTime) -> Self { + self.args.if_modified_since = Some(v); + self } /// Set `if_unmodified_since` for this `read` request. @@ -381,8 +403,9 @@ impl>> FutureRead { /// # Ok(()) /// # } /// ``` - pub fn if_unmodified_since(self, v: DateTime) -> Self { - self.map(|(args, op_reader)| (args.with_if_unmodified_since(v), op_reader)) + pub fn if_unmodified_since(mut self, v: DateTime) -> Self { + self.args.if_unmodified_since = Some(v); + self } } @@ -393,7 +416,7 @@ impl>> FutureRead { /// # Notes /// /// `(OpRead, ())` is a trick to make sure `FutureReader` is different from `FutureRead` -pub type FutureReader = OperatorFuture<(OpRead, OpReader), Reader, F>; +pub type FutureReader = OperatorFuture; impl>> FutureReader { /// Set `version` for this `reader`. @@ -411,8 +434,9 @@ impl>> FutureReader { /// # Ok(()) /// # } /// ``` - pub fn version(self, v: &str) -> Self { - self.map(|(op_read, op_reader)| (op_read.with_version(v), op_reader)) + pub fn version(mut self, v: &str) -> Self { + self.args.version = Some(v.to_string()); + self } /// Set `concurrent` for the reader. @@ -433,8 +457,9 @@ impl>> FutureReader { /// # Ok(()) /// # } /// ``` - pub fn concurrent(self, concurrent: usize) -> Self { - self.map(|(op_read, op_reader)| (op_read, op_reader.with_concurrent(concurrent))) + pub fn concurrent(mut self, concurrent: usize) -> Self { + self.args.concurrent = concurrent.max(1); + self } /// OpenDAL will use services' preferred chunk size by default. Users can set chunk based on their own needs. @@ -453,8 +478,9 @@ impl>> FutureReader { /// # Ok(()) /// # } /// ``` - pub fn chunk(self, chunk_size: usize) -> Self { - self.map(|(op_read, op_reader)| (op_read, op_reader.with_chunk(chunk_size))) + pub fn chunk(mut self, chunk_size: usize) -> Self { + self.args.chunk = Some(chunk_size); + self } /// Controls the optimization strategy for range reads in [`Reader::fetch`]. @@ -484,8 +510,9 @@ impl>> FutureReader { /// # Ok(()) /// # } /// ``` - pub fn gap(self, gap_size: usize) -> Self { - self.map(|(op_read, op_reader)| (op_read, op_reader.with_gap(gap_size))) + pub fn gap(mut self, gap_size: usize) -> Self { + self.args.gap = Some(gap_size); + self } /// Set `if-match` for this `read` request. @@ -503,8 +530,9 @@ impl>> FutureReader { /// # Ok(()) /// # } /// ``` - pub fn if_match(self, etag: &str) -> Self { - self.map(|(op_read, op_reader)| (op_read.with_if_match(etag), op_reader)) + pub fn if_match(mut self, etag: &str) -> Self { + self.args.if_match = Some(etag.to_string()); + self } /// Set `if-none-match` for this `read` request. @@ -522,8 +550,9 @@ impl>> FutureReader { /// # Ok(()) /// # } /// ``` - pub fn if_none_match(self, etag: &str) -> Self { - self.map(|(op_read, op_reader)| (op_read.with_if_none_match(etag), op_reader)) + pub fn if_none_match(mut self, etag: &str) -> Self { + self.args.if_none_match = Some(etag.to_string()); + self } /// Set `if-modified-since` for this `read` request. @@ -543,8 +572,9 @@ impl>> FutureReader { /// # Ok(()) /// # } /// ``` - pub fn if_modified_since(self, v: DateTime) -> Self { - self.map(|(op_read, op_reader)| (op_read.with_if_modified_since(v), op_reader)) + pub fn if_modified_since(mut self, v: DateTime) -> Self { + self.args.if_modified_since = Some(v); + self } /// Set `if-unmodified-since` for this `read` request. @@ -564,32 +594,21 @@ impl>> FutureReader { /// # Ok(()) /// # } /// ``` - pub fn if_unmodified_since(self, v: DateTime) -> Self { - self.map(|(op_read, op_reader)| (op_read.with_if_unmodified_since(v), op_reader)) + pub fn if_unmodified_since(mut self, v: DateTime) -> Self { + self.args.if_unmodified_since = Some(v); + self } } /// Future that generated by [`Operator::write_with`]. /// /// Users can add more options by public functions provided by this struct. -pub type FutureWrite = OperatorFuture<(OpWrite, OpWriter, Buffer), Metadata, F>; +pub type FutureWrite = OperatorFuture<(options::WriteOptions, Buffer), Metadata, F>; impl>> FutureWrite { /// Sets append mode for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_can_append`] before using this feature. - /// - /// ### Behavior - /// - /// - By default, write operations overwrite existing files - /// - When append is set to true: - /// - New data will be appended to the end of existing file - /// - If file doesn't exist, it will be created - /// - If not supported, will return an error - /// - /// This operation allows adding data to existing files instead of overwriting them. + /// Refer to [`options::WriteOptions::append`] for more details. /// /// ### Example /// @@ -605,35 +624,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn append(self, v: bool) -> Self { - self.map(|(args, options, bs)| (args.with_append(v), options, bs)) + pub fn append(mut self, v: bool) -> Self { + self.args.0.append = v; + self } /// Sets chunk size for buffered writes. /// - /// ### Capability - /// - /// Check [`Capability::write_multi_min_size`] and [`Capability::write_multi_max_size`] for size limits. - /// - /// ### Behavior - /// - /// - By default, OpenDAL sets optimal chunk size based on service capabilities - /// - When chunk size is set: - /// - Data will be buffered until reaching chunk size - /// - One API call will be made per chunk - /// - Last chunk may be smaller than chunk size - /// - Important considerations: - /// - Some services require minimum chunk sizes (e.g. S3's EntityTooSmall error) - /// - Smaller chunks increase API calls and costs - /// - Larger chunks increase memory usage, but improve performance and reduce costs - /// - /// ### Performance Impact - /// - /// Setting appropriate chunk size can: - /// - Reduce number of API calls - /// - Improve overall throughput - /// - Lower operation costs - /// - Better utilize network bandwidth + /// Refer to [`options::WriteOptions::chunk`] for more details. /// /// ### Example /// @@ -653,34 +651,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn chunk(self, v: usize) -> Self { - self.map(|(args, options, bs)| (args, options.with_chunk(v), bs)) + pub fn chunk(mut self, v: usize) -> Self { + self.args.0.chunk = Some(v); + self } /// Sets concurrent write operations for this writer. /// - /// ## Behavior - /// - /// - By default, OpenDAL writes files sequentially - /// - When concurrent is set: - /// - Multiple write operations can execute in parallel - /// - Write operations return immediately without waiting if tasks space are available - /// - Close operation ensures all writes complete in order - /// - Memory usage increases with concurrency level - /// - If not supported, falls back to sequential writes - /// - /// This feature significantly improves performance when: - /// - Writing large files - /// - Network latency is high - /// - Storage service supports concurrent uploads like multipart uploads - /// - /// ## Performance Impact - /// - /// Setting appropriate concurrency can: - /// - Increase write throughput - /// - Reduce total write time - /// - Better utilize available bandwidth - /// - Trade memory for performance + /// Refer to [`options::WriteOptions::concurrent`] for more details. /// /// ## Example /// @@ -697,30 +675,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn concurrent(self, v: usize) -> Self { - self.map(|(args, options, bs)| (args.with_concurrent(v), options, bs)) + pub fn concurrent(mut self, v: usize) -> Self { + self.args.0.concurrent = v.max(1); + self } /// Sets Cache-Control header for this write operation. /// - /// ### Capability - /// - /// Check [`Capability::write_with_cache_control`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, sets Cache-Control as system metadata on the target file - /// - The value should follow HTTP Cache-Control header format - /// - If not supported, the value will be ignored - /// - /// This operation allows controlling caching behavior for the written content. - /// - /// ### Use Cases - /// - /// - Setting browser cache duration - /// - Configuring CDN behavior - /// - Optimizing content delivery - /// - Managing cache invalidation + /// Refer to [`options::WriteOptions::cache_control`] for more details. /// /// ### Example /// @@ -740,28 +702,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - /// - /// ### References - /// - /// - [MDN Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) - /// - [RFC 7234 Section 5.2](https://tools.ietf.org/html/rfc7234#section-5.2) - pub fn cache_control(self, v: &str) -> Self { - self.map(|(args, options, bs)| (args.with_cache_control(v), options, bs)) + pub fn cache_control(mut self, v: &str) -> Self { + self.args.0.cache_control = Some(v.to_string()); + self } /// Sets `Content-Type` header for this write operation. /// - /// ## Capability - /// - /// Check [`Capability::write_with_content_type`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, sets Content-Type as system metadata on the target file - /// - The value should follow MIME type format (e.g. "text/plain", "image/jpeg") - /// - If not supported, the value will be ignored - /// - /// This operation allows specifying the media type of the content being written. + /// Refer to [`options::WriteOptions::content_type`] for more details. /// /// ## Example /// @@ -779,29 +727,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn content_type(self, v: &str) -> Self { - self.map(|(args, options, bs)| (args.with_content_type(v), options, bs)) + pub fn content_type(mut self, v: &str) -> Self { + self.args.0.content_type = Some(v.to_string()); + self } - /// ## `content_disposition` - /// /// Sets Content-Disposition header for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_with_content_disposition`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, sets Content-Disposition as system metadata on the target file - /// - The value should follow HTTP Content-Disposition header format - /// - Common values include: - /// - `inline` - Content displayed within browser - /// - `attachment` - Content downloaded as file - /// - `attachment; filename="example.jpg"` - Downloaded with specified filename - /// - If not supported, the value will be ignored - /// - /// This operation allows controlling how the content should be displayed or downloaded. + /// Refer to [`options::WriteOptions::content_disposition`] for more details. /// /// ### Example /// @@ -820,28 +753,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn content_disposition(self, v: &str) -> Self { - self.map(|(args, options, bs)| (args.with_content_disposition(v), options, bs)) + pub fn content_disposition(mut self, v: &str) -> Self { + self.args.0.content_disposition = Some(v.to_string()); + self } /// Sets Content-Encoding header for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_with_content_encoding`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, sets Content-Encoding as system metadata on the target file - /// - The value should follow HTTP Content-Encoding header format - /// - Common values include: - /// - `gzip` - Content encoded using gzip compression - /// - `deflate` - Content encoded using deflate compression - /// - `br` - Content encoded using Brotli compression - /// - `identity` - No encoding applied (default value) - /// - If not supported, the value will be ignored - /// - /// This operation allows specifying the encoding applied to the content being written. + /// Refer to [`options::WriteOptions::content_encoding`] for more details. /// /// ### Example /// @@ -860,27 +779,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn content_encoding(self, v: &str) -> Self { - self.map(|(args, options, bs)| (args.with_content_encoding(v), options, bs)) + pub fn content_encoding(mut self, v: &str) -> Self { + self.args.0.content_encoding = Some(v.to_string()); + self } /// Sets If-Match header for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_with_if_match`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, the write operation will only succeed if the target's ETag matches the specified value - /// - The value should be a valid ETag string - /// - Common values include: - /// - A specific ETag value like `"686897696a7c876b7e"` - /// - `*` - Matches any existing resource - /// - If not supported, the value will be ignored - /// - /// This operation provides conditional write functionality based on ETag matching, - /// helping prevent unintended overwrites in concurrent scenarios. + /// Refer to [`options::WriteOptions::if_match`] for more details. /// /// ### Example /// @@ -899,30 +805,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn if_match(self, s: &str) -> Self { - self.map(|(args, options, bs)| (args.with_if_match(s), options, bs)) + pub fn if_match(mut self, s: &str) -> Self { + self.args.0.if_match = Some(s.to_string()); + self } /// Sets If-None-Match header for this write request. /// - /// Note: Certain services, like `s3`, support `if_not_exists` but not `if_none_match`. - /// Use `if_not_exists` if you only want to check whether a file exists. - /// - /// ### Capability - /// - /// Check [`Capability::write_with_if_none_match`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, the write operation will only succeed if the target's ETag does not match the specified value - /// - The value should be a valid ETag string - /// - Common values include: - /// - A specific ETag value like `"686897696a7c876b7e"` - /// - `*` - Matches if the resource does not exist - /// - If not supported, the value will be ignored - /// - /// This operation provides conditional write functionality based on ETag non-matching, - /// useful for preventing overwriting existing resources or ensuring unique writes. + /// Refer to [`options::WriteOptions::if_none_match`] for more details. /// /// ### Example /// @@ -941,24 +831,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn if_none_match(self, s: &str) -> Self { - self.map(|(args, options, bs)| (args.with_if_none_match(s), options, bs)) + pub fn if_none_match(mut self, s: &str) -> Self { + self.args.0.if_none_match = Some(s.to_string()); + self } /// Sets the condition that write operation will succeed only if target does not exist. /// - /// ### Capability - /// - /// Check [`Capability::write_with_if_not_exists`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, the write operation will only succeed if the target path does not exist - /// - Will return error if target already exists - /// - If not supported, the value will be ignored - /// - /// This operation provides a way to ensure write operations only create new resources - /// without overwriting existing ones, useful for implementing "create if not exists" logic. + /// Refer to [`options::WriteOptions::if_not_exists`] for more details. /// /// ### Example /// @@ -977,30 +857,14 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn if_not_exists(self, b: bool) -> Self { - self.map(|(args, options, bs)| (args.with_if_not_exists(b), options, bs)) + pub fn if_not_exists(mut self, b: bool) -> Self { + self.args.0.if_not_exists = b; + self } /// Sets user metadata for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_with_user_metadata`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, the user metadata will be attached to the object during write - /// - Accepts key-value pairs where both key and value are strings - /// - Keys are case-insensitive in most services - /// - Services may have limitations for user metadata, for example: - /// - Key length is typically limited (e.g., 1024 bytes) - /// - Value length is typically limited (e.g., 4096 bytes) - /// - Total metadata size might be limited - /// - Some characters might be forbidden in keys - /// - If not supported, the metadata will be ignored - /// - /// User metadata provides a way to attach custom metadata to objects during write operations. - /// This metadata can be retrieved later when reading the object. + /// Refer to [`options::WriteOptions::user_metadata`] for more details. /// /// ### Example /// @@ -1022,38 +886,21 @@ impl>> FutureWrite { /// # Ok(()) /// # } /// ``` - pub fn user_metadata(self, data: impl IntoIterator) -> Self { - self.map(|(args, options, bs)| { - ( - args.with_user_metadata(HashMap::from_iter(data)), - options, - bs, - ) - }) + pub fn user_metadata(mut self, data: impl IntoIterator) -> Self { + self.args.0.user_metadata = Some(HashMap::from_iter(data)); + self } } /// Future that generated by [`Operator::writer_with`]. /// /// Users can add more options by public functions provided by this struct. -pub type FutureWriter = OperatorFuture<(OpWrite, OpWriter), Writer, F>; +pub type FutureWriter = OperatorFuture; impl>> FutureWriter { /// Sets append mode for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_can_append`] before using this feature. - /// - /// ### Behavior - /// - /// - By default, write operations overwrite existing files - /// - When append is set to true: - /// - New data will be appended to the end of existing file - /// - If file doesn't exist, it will be created - /// - If not supported, will return an error - /// - /// This operation allows adding data to existing files instead of overwriting them. + /// Refer to [`options::WriteOptions::append`] for more details. /// /// ### Example /// @@ -1072,35 +919,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn append(self, v: bool) -> Self { - self.map(|(args, options)| (args.with_append(v), options)) + pub fn append(mut self, v: bool) -> Self { + self.args.append = v; + self } /// Sets chunk size for buffered writes. /// - /// ### Capability - /// - /// Check [`Capability::write_multi_min_size`] and [`Capability::write_multi_max_size`] for size limits. - /// - /// ### Behavior - /// - /// - By default, OpenDAL sets optimal chunk size based on service capabilities - /// - When chunk size is set: - /// - Data will be buffered until reaching chunk size - /// - One API call will be made per chunk - /// - Last chunk may be smaller than chunk size - /// - Important considerations: - /// - Some services require minimum chunk sizes (e.g. S3's EntityTooSmall error) - /// - Smaller chunks increase API calls and costs - /// - Larger chunks increase memory usage, but improve performance and reduce costs - /// - /// ### Performance Impact - /// - /// Setting appropriate chunk size can: - /// - Reduce number of API calls - /// - Improve overall throughput - /// - Lower operation costs - /// - Better utilize network bandwidth + /// Refer to [`options::WriteOptions::chunk`] for more details. /// /// ### Example /// @@ -1123,34 +949,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn chunk(self, v: usize) -> Self { - self.map(|(args, options)| (args, options.with_chunk(v))) + pub fn chunk(mut self, v: usize) -> Self { + self.args.chunk = Some(v); + self } /// Sets concurrent write operations for this writer. /// - /// ## Behavior - /// - /// - By default, OpenDAL writes files sequentially - /// - When concurrent is set: - /// - Multiple write operations can execute in parallel - /// - Write operations return immediately without waiting if tasks space are available - /// - Close operation ensures all writes complete in order - /// - Memory usage increases with concurrency level - /// - If not supported, falls back to sequential writes - /// - /// This feature significantly improves performance when: - /// - Writing large files - /// - Network latency is high - /// - Storage service supports concurrent uploads like multipart uploads - /// - /// ## Performance Impact - /// - /// Setting appropriate concurrency can: - /// - Increase write throughput - /// - Reduce total write time - /// - Better utilize available bandwidth - /// - Trade memory for performance + /// Refer to [`options::WriteOptions::concurrent`] for more details. /// /// ## Example /// @@ -1176,30 +982,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn concurrent(self, v: usize) -> Self { - self.map(|(args, options)| (args.with_concurrent(v), options)) + pub fn concurrent(mut self, v: usize) -> Self { + self.args.concurrent = v.max(1); + self } /// Sets Cache-Control header for this write operation. /// - /// ### Capability - /// - /// Check [`Capability::write_with_cache_control`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, sets Cache-Control as system metadata on the target file - /// - The value should follow HTTP Cache-Control header format - /// - If not supported, the value will be ignored - /// - /// This operation allows controlling caching behavior for the written content. - /// - /// ### Use Cases - /// - /// - Setting browser cache duration - /// - Configuring CDN behavior - /// - Optimizing content delivery - /// - Managing cache invalidation + /// Refer to [`options::WriteOptions::cache_control`] for more details. /// /// ### Example /// @@ -1222,28 +1012,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - /// - /// ### References - /// - /// - [MDN Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) - /// - [RFC 7234 Section 5.2](https://tools.ietf.org/html/rfc7234#section-5.2) - pub fn cache_control(self, v: &str) -> Self { - self.map(|(args, options)| (args.with_cache_control(v), options)) + pub fn cache_control(mut self, v: &str) -> Self { + self.args.cache_control = Some(v.to_string()); + self } /// Sets `Content-Type` header for this write operation. /// - /// ## Capability - /// - /// Check [`Capability::write_with_content_type`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, sets Content-Type as system metadata on the target file - /// - The value should follow MIME type format (e.g. "text/plain", "image/jpeg") - /// - If not supported, the value will be ignored - /// - /// This operation allows specifying the media type of the content being written. + /// Refer to [`options::WriteOptions::content_type`] for more details. /// /// ## Example /// @@ -1264,29 +1040,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn content_type(self, v: &str) -> Self { - self.map(|(args, options)| (args.with_content_type(v), options)) + pub fn content_type(mut self, v: &str) -> Self { + self.args.content_type = Some(v.to_string()); + self } - /// ## `content_disposition` - /// /// Sets Content-Disposition header for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_with_content_disposition`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, sets Content-Disposition as system metadata on the target file - /// - The value should follow HTTP Content-Disposition header format - /// - Common values include: - /// - `inline` - Content displayed within browser - /// - `attachment` - Content downloaded as file - /// - `attachment; filename="example.jpg"` - Downloaded with specified filename - /// - If not supported, the value will be ignored - /// - /// This operation allows controlling how the content should be displayed or downloaded. + /// Refer to [`options::WriteOptions::content_disposition`] for more details. /// /// ### Example /// @@ -1308,28 +1069,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn content_disposition(self, v: &str) -> Self { - self.map(|(args, options)| (args.with_content_disposition(v), options)) + pub fn content_disposition(mut self, v: &str) -> Self { + self.args.content_disposition = Some(v.to_string()); + self } /// Sets Content-Encoding header for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_with_content_encoding`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, sets Content-Encoding as system metadata on the target file - /// - The value should follow HTTP Content-Encoding header format - /// - Common values include: - /// - `gzip` - Content encoded using gzip compression - /// - `deflate` - Content encoded using deflate compression - /// - `br` - Content encoded using Brotli compression - /// - `identity` - No encoding applied (default value) - /// - If not supported, the value will be ignored - /// - /// This operation allows specifying the encoding applied to the content being written. + /// Refer to [`options::WriteOptions::content_encoding`] for more details. /// /// ### Example /// @@ -1351,15 +1098,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn content_encoding(self, v: &str) -> Self { - self.map(|(args, options)| (args.with_content_encoding(v), options)) + pub fn content_encoding(mut self, v: &str) -> Self { + self.args.content_encoding = Some(v.to_string()); + self } /// Sets If-Match header for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_with_if_match`] before using this feature. + /// Refer to [`options::WriteOptions::if_match`] for more details. /// /// ### Behavior /// @@ -1393,30 +1139,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn if_match(self, s: &str) -> Self { - self.map(|(args, options)| (args.with_if_match(s), options)) + pub fn if_match(mut self, s: &str) -> Self { + self.args.if_match = Some(s.to_string()); + self } /// Sets If-None-Match header for this write request. /// - /// Note: Certain services, like `s3`, support `if_not_exists` but not `if_none_match`. - /// Use `if_not_exists` if you only want to check whether a file exists. - /// - /// ### Capability - /// - /// Check [`Capability::write_with_if_none_match`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, the write operation will only succeed if the target's ETag does not match the specified value - /// - The value should be a valid ETag string - /// - Common values include: - /// - A specific ETag value like `"686897696a7c876b7e"` - /// - `*` - Matches if the resource does not exist - /// - If not supported, the value will be ignored - /// - /// This operation provides conditional write functionality based on ETag non-matching, - /// useful for preventing overwriting existing resources or ensuring unique writes. + /// Refer to [`options::WriteOptions::if_none_match`] for more details. /// /// ### Example /// @@ -1438,24 +1168,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn if_none_match(self, s: &str) -> Self { - self.map(|(args, options)| (args.with_if_none_match(s), options)) + pub fn if_none_match(mut self, s: &str) -> Self { + self.args.if_none_match = Some(s.to_string()); + self } /// Sets the condition that write operation will succeed only if target does not exist. /// - /// ### Capability - /// - /// Check [`Capability::write_with_if_not_exists`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, the write operation will only succeed if the target path does not exist - /// - Will return error if target already exists - /// - If not supported, the value will be ignored - /// - /// This operation provides a way to ensure write operations only create new resources - /// without overwriting existing ones, useful for implementing "create if not exists" logic. + /// Refer to [`options::WriteOptions::if_not_exists`] for more details. /// /// ### Example /// @@ -1477,30 +1197,14 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn if_not_exists(self, b: bool) -> Self { - self.map(|(args, options)| (args.with_if_not_exists(b), options)) + pub fn if_not_exists(mut self, b: bool) -> Self { + self.args.if_not_exists = b; + self } /// Sets user metadata for this write request. /// - /// ### Capability - /// - /// Check [`Capability::write_with_user_metadata`] before using this feature. - /// - /// ### Behavior - /// - /// - If supported, the user metadata will be attached to the object during write - /// - Accepts key-value pairs where both key and value are strings - /// - Keys are case-insensitive in most services - /// - Services may have limitations for user metadata, for example: - /// - Key length is typically limited (e.g., 1024 bytes) - /// - Value length is typically limited (e.g., 4096 bytes) - /// - Total metadata size might be limited - /// - Some characters might be forbidden in keys - /// - If not supported, the metadata will be ignored - /// - /// User metadata provides a way to attach custom metadata to objects during write operations. - /// This metadata can be retrieved later when reading the object. + /// Refer to [`options::WriteOptions::user_metadata`] for more details. /// /// ### Example /// @@ -1524,20 +1228,22 @@ impl>> FutureWriter { /// # Ok(()) /// # } /// ``` - pub fn user_metadata(self, data: impl IntoIterator) -> Self { - self.map(|(args, options)| (args.with_user_metadata(HashMap::from_iter(data)), options)) + pub fn user_metadata(mut self, data: impl IntoIterator) -> Self { + self.args.user_metadata = Some(HashMap::from_iter(data)); + self } } /// Future that generated by [`Operator::delete_with`]. /// /// Users can add more options by public functions provided by this struct. -pub type FutureDelete = OperatorFuture; +pub type FutureDelete = OperatorFuture; impl>> FutureDelete { /// Change the version of this delete operation. - pub fn version(self, v: &str) -> Self { - self.map(|args| args.with_version(v)) + pub fn version(mut self, v: &str) -> Self { + self.args.version = Some(v.to_string()); + self } } diff --git a/core/src/types/options.rs b/core/src/types/options.rs new file mode 100644 index 000000000000..a7b9d019a27f --- /dev/null +++ b/core/src/types/options.rs @@ -0,0 +1,518 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Options module provides options definitions for operations. + +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; + +use crate::raw::BytesRange; + +/// Options for delete operations. +#[non_exhaustive] +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct DeleteOptions { + /// The version of the file to delete. + pub version: Option, +} + +/// Options for list operations. +#[non_exhaustive] +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct ListOptions { + /// The limit passed to underlying service to specify the max results + /// that could return per-request. + /// + /// Users could use this to control the memory usage of list operation. + pub limit: Option, + /// The start_after passes to underlying service to specify the specified key + /// to start listing from. + pub start_after: Option, + /// The recursive is used to control whether the list operation is recursive. + /// + /// - If `false`, list operation will only list the entries under the given path. + /// - If `true`, list operation will list all entries that starts with given path. + /// + /// Default to `false`. + pub recursive: bool, + /// The version is used to control whether the object versions should be returned. + /// + /// - If `false`, list operation will not return with object versions + /// - If `true`, list operation will return with object versions if object versioning is supported + /// by the underlying service + /// + /// Default to `false` + pub versions: bool, + /// The deleted is used to control whether the deleted objects should be returned. + /// + /// - If `false`, list operation will not return with deleted objects + /// - If `true`, list operation will return with deleted objects if object versioning is supported + /// by the underlying service + /// + /// Default to `false` + pub deleted: bool, +} + +/// Options for read operations. +#[non_exhaustive] +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct ReadOptions { + /// Set `range` for this operation. + /// + /// If we have a file with size `n`. + /// + /// - `..` means read bytes in range `[0, n)` of file. + /// - `0..1024` and `..1024` means read bytes in range `[0, 1024)` of file + /// - `1024..` means read bytes in range `[1024, n)` of file + /// + /// The type implements `From>`, so users can use `(1024..).into()` instead. + pub range: BytesRange, + /// Set `version` for this operation. + /// + /// This option can be used to retrieve the data of a specified version of the given path. + /// + /// If the version doesn't exist, an error with kind [`ErrorKind::NotFound`] will be returned. + pub version: Option, + + /// Set `if_match` for this operation. + /// + /// This option can be used to check if the file's `ETag` matches the given `ETag`. + /// + /// If file exists and it's etag doesn't match, an error with kind [`ErrorKind::ConditionNotMatch`] + /// will be returned. + pub if_match: Option, + /// Set `if_none_match` for this operation. + /// + /// This option can be used to check if the file's `ETag` doesn't match the given `ETag`. + /// + /// If file exists and it's etag match, an error with kind [`ErrorKind::ConditionNotMatch`] + /// will be returned. + pub if_none_match: Option, + /// Set `if_modified_since` for this operation. + /// + /// This option can be used to check if the file has been modified since the given timestamp. + /// + /// If file exists and it hasn't been modified since the specified time, an error with kind + /// [`ErrorKind::ConditionNotMatch`] will be returned. + pub if_modified_since: Option>, + /// Set `if_unmodified_since` for this operation. + /// + /// This feature can be used to check if the file hasn't been modified since the given timestamp. + /// + /// If file exists and it has been modified since the specified time, an error with kind + /// [`ErrorKind::ConditionNotMatch`] will be returned. + pub if_unmodified_since: Option>, + + /// Set `concurrent` for the operation. + /// + /// OpenDAL by default to read file without concurrent. This is not efficient for cases when users + /// read large chunks of data. By setting `concurrent`, opendal will reading files concurrently + /// on support storage services. + /// + /// By setting `concurrent`, opendal will fetch chunks concurrently with + /// the give chunk size. + /// + /// Refer to [`crate::docs::performance`] for more details. + pub concurrent: usize, + /// Set `chunk` for the operation. + /// + /// OpenDAL will use services' preferred chunk size by default. Users can set chunk based on their own needs. + /// + /// Refer to [`crate::docs::performance`] for more details. + pub chunk: Option, + /// Controls the optimization strategy for range reads in [`Reader::fetch`]. + /// + /// When performing range reads, if the gap between two requested ranges is smaller than + /// the configured `gap` size, OpenDAL will merge these ranges into a single read request + /// and discard the unrequested data in between. This helps reduce the number of API calls + /// to remote storage services. + /// + /// This optimization is particularly useful when performing multiple small range reads + /// that are close to each other, as it reduces the overhead of multiple network requests + /// at the cost of transferring some additional data. + /// + /// Refer to [`crate::docs::performance`] for more details. + pub gap: Option, + + /// Specify the content-type header that should be sent back by the operation. + /// + /// This option is only meaningful when used along with presign. + pub override_content_type: Option, + /// Specify the `cache-control` header that should be sent back by the operation. + /// + /// This option is only meaningful when used along with presign. + pub override_cache_control: Option, + /// Specify the `content-disposition` header that should be sent back by the operation. + /// + /// This option is only meaningful when used along with presign. + pub override_content_disposition: Option, +} + +/// Options for reader operations. +#[non_exhaustive] +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct ReaderOptions { + /// Set `version` for this operation. + /// + /// This option can be used to retrieve the data of a specified version of the given path. + /// + /// If the version doesn't exist, an error with kind [`ErrorKind::NotFound`] will be returned. + pub version: Option, + + /// Set `if_match` for this operation. + /// + /// This option can be used to check if the file's `ETag` matches the given `ETag`. + /// + /// If file exists and it's etag doesn't match, an error with kind [`ErrorKind::ConditionNotMatch`] + /// will be returned. + pub if_match: Option, + /// Set `if_none_match` for this operation. + /// + /// This option can be used to check if the file's `ETag` doesn't match the given `ETag`. + /// + /// If file exists and it's etag match, an error with kind [`ErrorKind::ConditionNotMatch`] + /// will be returned. + pub if_none_match: Option, + /// Set `if_modified_since` for this operation. + /// + /// This option can be used to check if the file has been modified since the given timestamp. + /// + /// If file exists and it hasn't been modified since the specified time, an error with kind + /// [`ErrorKind::ConditionNotMatch`] will be returned. + pub if_modified_since: Option>, + /// Set `if_unmodified_since` for this operation. + /// + /// This feature can be used to check if the file hasn't been modified since the given timestamp. + /// + /// If file exists and it has been modified since the specified time, an error with kind + /// [`ErrorKind::ConditionNotMatch`] will be returned. + pub if_unmodified_since: Option>, + + /// Set `concurrent` for the operation. + /// + /// OpenDAL by default to read file without concurrent. This is not efficient for cases when users + /// read large chunks of data. By setting `concurrent`, opendal will reading files concurrently + /// on support storage services. + /// + /// By setting `concurrent`, opendal will fetch chunks concurrently with + /// the give chunk size. + /// + /// Refer to [`crate::docs::performance`] for more details. + pub concurrent: usize, + /// Set `chunk` for the operation. + /// + /// OpenDAL will use services' preferred chunk size by default. Users can set chunk based on their own needs. + /// + /// Refer to [`crate::docs::performance`] for more details. + pub chunk: Option, + /// Controls the optimization strategy for range reads in [`Reader::fetch`]. + /// + /// When performing range reads, if the gap between two requested ranges is smaller than + /// the configured `gap` size, OpenDAL will merge these ranges into a single read request + /// and discard the unrequested data in between. This helps reduce the number of API calls + /// to remote storage services. + /// + /// This optimization is particularly useful when performing multiple small range reads + /// that are close to each other, as it reduces the overhead of multiple network requests + /// at the cost of transferring some additional data. + /// + /// Refer to [`crate::docs::performance`] for more details. + pub gap: Option, +} + +/// Options for stat operations. +#[non_exhaustive] +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct StatOptions { + /// Set `version` for this operation. + /// + /// This options can be used to retrieve the data of a specified version of the given path. + /// + /// If the version doesn't exist, an error with kind [`ErrorKind::NotFound`] will be returned. + pub version: Option, + + /// Set `if_match` for this operation. + /// + /// This option can be used to check if the file's `ETag` matches the given `ETag`. + /// + /// If file exists and it's etag doesn't match, an error with kind [`ErrorKind::ConditionNotMatch`] + /// will be returned. + pub if_match: Option, + /// Set `if_none_match` for this operation. + /// + /// This option can be used to check if the file's `ETag` doesn't match the given `ETag`. + /// + /// If file exists and it's etag match, an error with kind [`ErrorKind::ConditionNotMatch`] + /// will be returned. + pub if_none_match: Option, + /// Set `if_modified_since` for this operation. + /// + /// This option can be used to check if the file has been modified since the given timestamp. + /// + /// If file exists and it hasn't been modified since the specified time, an error with kind + /// [`ErrorKind::ConditionNotMatch`] will be returned. + pub if_modified_since: Option>, + /// Set `if_unmodified_since` for this operation. + /// + /// This feature can be used to check if the file hasn't been modified since the given timestamp. + /// + /// If file exists and it has been modified since the specified time, an error with kind + /// [`ErrorKind::ConditionNotMatch`] will be returned. + pub if_unmodified_since: Option>, + + /// Specify the content-type header that should be sent back by the operation. + /// + /// This option is only meaningful when used along with presign. + pub override_content_type: Option, + /// Specify the `cache-control` header that should be sent back by the operation. + /// + /// This option is only meaningful when used along with presign. + pub override_cache_control: Option, + /// Specify the `content-disposition` header that should be sent back by the operation. + /// + /// This option is only meaningful when used along with presign. + pub override_content_disposition: Option, +} + +/// Options for write operations. +#[non_exhaustive] +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct WriteOptions { + /// Sets append mode for this operation. + /// + /// ### Capability + /// + /// Check [`Capability::write_can_append`] before using this option. + /// + /// ### Behavior + /// + /// - By default, write operations overwrite existing files + /// - When append is set to true: + /// - New data will be appended to the end of existing file + /// - If file doesn't exist, it will be created + /// - If not supported, will return an error + /// + /// This operation allows adding data to existing files instead of overwriting them. + pub append: bool, + + /// Sets Cache-Control header for this write operation. + /// + /// ### Capability + /// + /// Check [`Capability::write_with_cache_control`] before using this feature. + /// + /// ### Behavior + /// + /// - If supported, sets Cache-Control as system metadata on the target file + /// - The value should follow HTTP Cache-Control header format + /// - If not supported, the value will be ignored + /// + /// This operation allows controlling caching behavior for the written content. + /// + /// ### Use Cases + /// + /// - Setting browser cache duration + /// - Configuring CDN behavior + /// - Optimizing content delivery + /// - Managing cache invalidation + /// + /// ### References + /// + /// - [MDN Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) + /// - [RFC 7234 Section 5.2](https://tools.ietf.org/html/rfc7234#section-5.2) + pub cache_control: Option, + /// Sets `Content-Type` header for this write operation. + /// + /// ## Capability + /// + /// Check [`Capability::write_with_content_type`] before using this feature. + /// + /// ### Behavior + /// + /// - If supported, sets Content-Type as system metadata on the target file + /// - The value should follow MIME type format (e.g. "text/plain", "image/jpeg") + /// - If not supported, the value will be ignored + /// + /// This operation allows specifying the media type of the content being written. + pub content_type: Option, + /// Sets Content-Disposition header for this write request. + /// + /// ### Capability + /// + /// Check [`Capability::write_with_content_disposition`] before using this feature. + /// + /// ### Behavior + /// + /// - If supported, sets Content-Disposition as system metadata on the target file + /// - The value should follow HTTP Content-Disposition header format + /// - Common values include: + /// - `inline` - Content displayed within browser + /// - `attachment` - Content downloaded as file + /// - `attachment; filename="example.jpg"` - Downloaded with specified filename + /// - If not supported, the value will be ignored + /// + /// This operation allows controlling how the content should be displayed or downloaded. + pub content_disposition: Option, + /// Sets Content-Encoding header for this write request. + /// + /// ### Capability + /// + /// Check [`Capability::write_with_content_encoding`] before using this feature. + /// + /// ### Behavior + /// + /// - If supported, sets Content-Encoding as system metadata on the target file + /// - The value should follow HTTP Content-Encoding header format + /// - Common values include: + /// - `gzip` - Content encoded using gzip compression + /// - `deflate` - Content encoded using deflate compression + /// - `br` - Content encoded using Brotli compression + /// - `identity` - No encoding applied (default value) + /// - If not supported, the value will be ignored + /// + /// This operation allows specifying the encoding applied to the content being written. + pub content_encoding: Option, + /// Sets user metadata for this write request. + /// + /// ### Capability + /// + /// Check [`Capability::write_with_user_metadata`] before using this feature. + /// + /// ### Behavior + /// + /// - If supported, the user metadata will be attached to the object during write + /// - Accepts key-value pairs where both key and value are strings + /// - Keys are case-insensitive in most services + /// - Services may have limitations for user metadata, for example: + /// - Key length is typically limited (e.g., 1024 bytes) + /// - Value length is typically limited (e.g., 4096 bytes) + /// - Total metadata size might be limited + /// - Some characters might be forbidden in keys + /// - If not supported, the metadata will be ignored + /// + /// User metadata provides a way to attach custom metadata to objects during write operations. + /// This metadata can be retrieved later when reading the object. + pub user_metadata: Option>, + + /// Sets If-Match header for this write request. + /// + /// ### Capability + /// + /// Check [`Capability::write_with_if_match`] before using this feature. + /// + /// ### Behavior + /// + /// - If supported, the write operation will only succeed if the target's ETag matches the specified value + /// - The value should be a valid ETag string + /// - Common values include: + /// - A specific ETag value like `"686897696a7c876b7e"` + /// - `*` - Matches any existing resource + /// - If not supported, the value will be ignored + /// + /// This operation provides conditional write functionality based on ETag matching, + /// helping prevent unintended overwrites in concurrent scenarios. + pub if_match: Option, + /// Sets If-None-Match header for this write request. + /// + /// Note: Certain services, like `s3`, support `if_not_exists` but not `if_none_match`. + /// Use `if_not_exists` if you only want to check whether a file exists. + /// + /// ### Capability + /// + /// Check [`Capability::write_with_if_none_match`] before using this feature. + /// + /// ### Behavior + /// + /// - If supported, the write operation will only succeed if the target's ETag does not match the specified value + /// - The value should be a valid ETag string + /// - Common values include: + /// - A specific ETag value like `"686897696a7c876b7e"` + /// - `*` - Matches if the resource does not exist + /// - If not supported, the value will be ignored + /// + /// This operation provides conditional write functionality based on ETag non-matching, + /// useful for preventing overwriting existing resources or ensuring unique writes. + pub if_none_match: Option, + /// Sets the condition that write operation will succeed only if target does not exist. + /// + /// ### Capability + /// + /// Check [`Capability::write_with_if_not_exists`] before using this feature. + /// + /// ### Behavior + /// + /// - If supported, the write operation will only succeed if the target path does not exist + /// - Will return error if target already exists + /// - If not supported, the value will be ignored + /// + /// This operation provides a way to ensure write operations only create new resources + /// without overwriting existing ones, useful for implementing "create if not exists" logic. + pub if_not_exists: bool, + + /// Sets concurrent write operations for this writer. + /// + /// ## Behavior + /// + /// - By default, OpenDAL writes files sequentially + /// - When concurrent is set: + /// - Multiple write operations can execute in parallel + /// - Write operations return immediately without waiting if tasks space are available + /// - Close operation ensures all writes complete in order + /// - Memory usage increases with concurrency level + /// - If not supported, falls back to sequential writes + /// + /// This feature significantly improves performance when: + /// - Writing large files + /// - Network latency is high + /// - Storage service supports concurrent uploads like multipart uploads + /// + /// ## Performance Impact + /// + /// Setting appropriate concurrency can: + /// - Increase write throughput + /// - Reduce total write time + /// - Better utilize available bandwidth + /// - Trade memory for performance + pub concurrent: usize, + /// Sets chunk size for buffered writes. + /// + /// ### Capability + /// + /// Check [`Capability::write_multi_min_size`] and [`Capability::write_multi_max_size`] for size limits. + /// + /// ### Behavior + /// + /// - By default, OpenDAL sets optimal chunk size based on service capabilities + /// - When chunk size is set: + /// - Data will be buffered until reaching chunk size + /// - One API call will be made per chunk + /// - Last chunk may be smaller than chunk size + /// - Important considerations: + /// - Some services require minimum chunk sizes (e.g. S3's EntityTooSmall error) + /// - Smaller chunks increase API calls and costs + /// - Larger chunks increase memory usage, but improve performance and reduce costs + /// + /// ### Performance Impact + /// + /// Setting appropriate chunk size can: + /// - Reduce number of API calls + /// - Improve overall throughput + /// - Lower operation costs + /// - Better utilize network bandwidth + pub chunk: Option, +}