From 909461087c608855aa252bf2b3343aac53210c75 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Tue, 13 Sep 2022 01:19:25 +0100
Subject: [PATCH] add `ContentDisposition::attachment` constructor (#2867)

---
 actix-web/CHANGES.md                          |  4 ++++
 .../src/http/header/content_disposition.rs    | 24 +++++++++++++++++--
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md
index d11108c2..9ded67e3 100644
--- a/actix-web/CHANGES.md
+++ b/actix-web/CHANGES.md
@@ -1,6 +1,10 @@
 # Changelog
 
 ## Unreleased - 2022-xx-xx
+### Added
+- Add `ContentDisposition::attachment` constructor. [#2867]
+
+[#2867]: https://github.com/actix/actix-web/pull/2867
 
 
 ## 4.2.1 - 2022-09-12
diff --git a/actix-web/src/http/header/content_disposition.rs b/actix-web/src/http/header/content_disposition.rs
index 0bb45919..f743302a 100644
--- a/actix-web/src/http/header/content_disposition.rs
+++ b/actix-web/src/http/header/content_disposition.rs
@@ -79,7 +79,7 @@ impl<'a> From<&'a str> for DispositionType {
 /// assert!(param.is_filename());
 /// assert_eq!(param.as_filename().unwrap(), "sample.txt");
 /// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 #[allow(clippy::large_enum_variant)]
 pub enum DispositionParam {
     /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from
@@ -302,7 +302,7 @@ impl DispositionParam {
 /// change to match local file system conventions if applicable, and do not use directory path
 /// information that may be present.
 /// See [RFC 2183 ยง2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3).
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ContentDisposition {
     /// The disposition type
     pub disposition: DispositionType,
@@ -312,16 +312,36 @@ pub struct ContentDisposition {
 }
 
 impl ContentDisposition {
+    /// Constructs a Content-Disposition header suitable for downloads.
+    ///
+    /// # Examples
+    /// ```
+    /// use actix_web::http::header::{ContentDisposition, TryIntoHeaderValue as _};
+    ///
+    /// let cd = ContentDisposition::attachment("files.zip");
+    ///
+    /// let cd_val = cd.try_into_value().unwrap();
+    /// assert_eq!(cd_val, "attachment; filename=\"files.zip\"");
+    /// ```
+    pub fn attachment(filename: impl Into<String>) -> Self {
+        Self {
+            disposition: DispositionType::Attachment,
+            parameters: vec![DispositionParam::Filename(filename.into())],
+        }
+    }
+
     /// Parse a raw Content-Disposition header value.
     pub fn from_raw(hv: &header::HeaderValue) -> Result<Self, crate::error::ParseError> {
         // `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible
         //  ASCII characters. So `hv.as_bytes` is necessary here.
         let hv = String::from_utf8(hv.as_bytes().to_vec())
             .map_err(|_| crate::error::ParseError::Header)?;
+
         let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';');
         if disp_type.is_empty() {
             return Err(crate::error::ParseError::Header);
         }
+
         let mut cd = ContentDisposition {
             disposition: disp_type.into(),
             parameters: Vec::new(),