From 12d7a197f726735c0e10f7c64ada007ba4e753ae Mon Sep 17 00:00:00 2001
From: Gaurav Atreya <allmanpride@gmail.com>
Date: Fri, 13 Sep 2024 22:33:55 -0400
Subject: [PATCH 1/6] new flag: Enable renaming during file upload if duplicate
 exists

---
 README.md      |  7 ++++++-
 src/args.rs    |  9 +++++++++
 src/config.rs  |  4 ++++
 src/errors.rs  |  4 +++-
 src/file_op.rs | 39 +++++++++++++++++++++++++++++++++++----
 5 files changed, 57 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index bff3de703..2bad20a37 100644
--- a/README.md
+++ b/README.md
@@ -289,6 +289,11 @@ Options:
 
           [env: OVERWRITE_FILES=]
 
+  -R, --rename-duplicate
+          Enable renaming files during file upload if duplicate exists
+
+          [env: RENAME_DUPLICATE_FILES=]
+
   -r, --enable-tar
           Enable uncompressed tar archive generation
 
@@ -497,7 +502,7 @@ You can provide `-i` multiple times to bind to multiple interfaces at the same t
 
 This is mostly a note for me on how to release this thing:
 
-- Make sure `CHANGELOG.md` is up to date.
+- Make sure [CHANGELOG.md](./CHANGELOG.md) is up to date.
 - `cargo release <version>`
 - `cargo release --execute <version>`
 - Releases will automatically be deployed by GitHub Actions.
diff --git a/src/args.rs b/src/args.rs
index dd09f65e4..14773f88d 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -198,6 +198,15 @@ pub struct CliArgs {
     #[arg(short = 'o', long = "overwrite-files", env = "OVERWRITE_FILES")]
     pub overwrite_files: bool,
 
+    /// Enable renaming files during file upload if duplicate exists
+    #[arg(
+        short = 'R',
+        long = "rename-duplicate",
+        env = "RENAME_DUPLICATE_FILES",
+        conflicts_with = "overwrite_files"
+    )]
+    pub rename_duplicate: bool,
+
     /// Enable uncompressed tar archive generation
     #[arg(short = 'r', long = "enable-tar", env = "MINISERVE_ENABLE_TAR")]
     pub enable_tar: bool,
diff --git a/src/config.rs b/src/config.rs
index f468365ad..0c0feccd2 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -110,6 +110,9 @@ pub struct MiniserveConfig {
     /// Enable upload to override existing files
     pub overwrite_files: bool,
 
+    /// Enable renaming files during file upload if duplicate exists
+    pub rename_duplicate: bool,
+
     /// If false, creation of uncompressed tar archives is disabled
     pub tar_enabled: bool,
 
@@ -289,6 +292,7 @@ impl MiniserveConfig {
             spa: args.spa,
             pretty_urls: args.pretty_urls,
             overwrite_files: args.overwrite_files,
+            rename_duplicate: args.rename_duplicate,
             show_qrcode: args.qrcode,
             mkdir_enabled: args.mkdir_enabled,
             file_upload: args.allowed_upload_dir.is_some(),
diff --git a/src/errors.rs b/src/errors.rs
index 21f8f12be..7e9599caa 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -37,7 +37,9 @@ pub enum RuntimeError {
     MultipartError(String),
 
     /// Might occur during file upload
-    #[error("File already exists, and the overwrite_files option has not been set")]
+    #[error(
+        "File already exists, and the overwrite_files or rename_duplicate option has not been set"
+    )]
     DuplicateFileError,
 
     /// Upload not allowed
diff --git a/src/file_op.rs b/src/file_op.rs
index 18bdcbe21..6f1b7b12c 100644
--- a/src/file_op.rs
+++ b/src/file_op.rs
@@ -21,11 +21,34 @@ use crate::{
 /// Returns total bytes written to file.
 async fn save_file(
     field: actix_multipart::Field,
-    file_path: PathBuf,
+    mut file_path: PathBuf,
     overwrite_files: bool,
+    rename_duplicate: bool,
 ) -> Result<u64, RuntimeError> {
-    if !overwrite_files && file_path.exists() {
-        return Err(RuntimeError::DuplicateFileError);
+    if file_path.exists() {
+        // clap makes sure both overwrite_files and rename_duplicate cannot be true
+        if rename_duplicate {
+            // optionally should we use Path::file_prefix, which
+            // extracts the portion of the file name before the
+            // first .?
+            let file_name = file_path.file_stem().unwrap_or_default().to_string_lossy();
+            let file_ext = file_path.extension().map(|s| s.to_string_lossy());
+            let filepaths = (1..).map(|i| {
+                if let Some(ext) = &file_ext {
+                    file_path.with_file_name(format!("{}-{}.{}", file_name, i, ext))
+                } else {
+                    file_path.with_file_name(format!("{}-{}", file_name, i))
+                }
+            });
+            for fp in filepaths {
+                if !fp.exists() {
+                    file_path = fp;
+                    break;
+                }
+            }
+        } else if !overwrite_files {
+            return Err(RuntimeError::DuplicateFileError);
+        }
     }
 
     let file = match File::create(&file_path).await {
@@ -57,6 +80,7 @@ async fn handle_multipart(
     mut field: actix_multipart::Field,
     path: PathBuf,
     overwrite_files: bool,
+    rename_duplicate: bool,
     allow_mkdir: bool,
     allow_hidden_paths: bool,
     allow_symlinks: bool,
@@ -168,7 +192,13 @@ async fn handle_multipart(
         }
     }
 
-    save_file(field, path.join(filename_path), overwrite_files).await
+    save_file(
+        field,
+        path.join(filename_path),
+        overwrite_files,
+        rename_duplicate,
+    )
+    .await
 }
 
 /// Query parameters used by upload and rm APIs
@@ -225,6 +255,7 @@ pub async fn upload_file(
                 field,
                 non_canonicalized_target_dir.clone(),
                 conf.overwrite_files,
+                conf.rename_duplicate,
                 conf.mkdir_enabled,
                 conf.show_hidden,
                 !conf.no_symlinks,

From f8e3098278c39bebe185a0a64bc0ba74da9833a8 Mon Sep 17 00:00:00 2001
From: Gaurav Atreya <allmanpride@gmail.com>
Date: Sat, 21 Sep 2024 20:46:55 -0400
Subject: [PATCH 2/6] refactor and commented the file rename logic for
 readability

---
 src/file_op.rs | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/src/file_op.rs b/src/file_op.rs
index 6f1b7b12c..9bf554c49 100644
--- a/src/file_op.rs
+++ b/src/file_op.rs
@@ -28,19 +28,18 @@ async fn save_file(
     if file_path.exists() {
         // clap makes sure both overwrite_files and rename_duplicate cannot be true
         if rename_duplicate {
-            // optionally should we use Path::file_prefix, which
-            // extracts the portion of the file name before the
-            // first .?
+            // extract extension of the file and the file stem without extension
+            // file.txt => (file, txt)
             let file_name = file_path.file_stem().unwrap_or_default().to_string_lossy();
             let file_ext = file_path.extension().map(|s| s.to_string_lossy());
-            let filepaths = (1..).map(|i| {
-                if let Some(ext) = &file_ext {
+            for i in 1.. {
+                // increment the number N in {file_name}-{N}.{file_ext}
+                // format until available name is found (e.g. file-1.txt, file-2.txt, etc)
+                let fp = if let Some(ext) = &file_ext {
                     file_path.with_file_name(format!("{}-{}.{}", file_name, i, ext))
                 } else {
                     file_path.with_file_name(format!("{}-{}", file_name, i))
-                }
-            });
-            for fp in filepaths {
+                };
                 if !fp.exists() {
                     file_path = fp;
                     break;

From 181988ca342d25c4d66167942f2449fb93152688 Mon Sep 17 00:00:00 2001
From: Gaurav Atreya <allmanpride@gmail.com>
Date: Sat, 21 Sep 2024 21:14:24 -0400
Subject: [PATCH 3/6] Add test cases for duplicate files (upload
 fail/overwrite/rename)

---
 tests/upload_files.rs | 174 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 174 insertions(+)

diff --git a/tests/upload_files.rs b/tests/upload_files.rs
index 77a9dc319..35c5edb57 100644
--- a/tests/upload_files.rs
+++ b/tests/upload_files.rs
@@ -7,7 +7,10 @@ use rstest::rstest;
 use select::document::Document;
 use select::predicate::{Attr, Text};
 use std::fs::create_dir_all;
+use std::fs::File;
+use std::io::Write;
 use std::path::Path;
+use std::path::PathBuf;
 
 #[rstest]
 fn uploading_files_works(#[with(&["-u"])] server: TestServer) -> Result<(), Error> {
@@ -275,3 +278,174 @@ fn set_media_type(
 
     Ok(())
 }
+
+#[rstest]
+fn uploading_duplicate_file_is_prevented(#[with(&["-u"])] server: TestServer) -> Result<(), Error> {
+    let test_file_name = "duplicate test file.txt";
+    let test_file_contents = "Test File Contents";
+
+    // Before uploading, check whether the uploaded file does not yet exist.
+    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
+    let parsed = Document::from_read(body)?;
+    assert!(parsed.find(Text).all(|x| x.text() != test_file_name));
+
+    // create the file
+    let test_file_path = write_file_contents(
+        server.path().to_path_buf(),
+        test_file_name,
+        test_file_contents,
+    );
+
+    // Perform the actual upload.
+    let upload_action = parsed
+        .find(Attr("id", "file_submit"))
+        .next()
+        .expect("Couldn't find element with id=file_submit")
+        .attr("action")
+        .expect("Upload form doesn't have action attribute");
+    // Then try to upload anyway
+    let form = multipart::Form::new();
+    let part = multipart::Part::text("this should not be uploaded")
+        .file_name(test_file_name)
+        .mime_str("text/plain")?;
+    let form = form.part("file_to_upload", part);
+
+    let client = Client::new();
+    // Ensure uploading fails and returns an error
+    assert!(client
+        .post(server.url().join(upload_action)?)
+        .multipart(form)
+        .send()?
+        .error_for_status()
+        .is_err());
+
+    // After uploading, uploaded file is still getting listed.
+    let body = reqwest::blocking::get(server.url())?;
+    let parsed = Document::from_read(body)?;
+    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));
+    // and assert the contents is the same as before
+    assert_file_contents(test_file_path, test_file_contents);
+
+    Ok(())
+}
+
+#[rstest]
+fn overwrite_duplicate_file(
+    #[with(&["--overwrite-files", "-u"])] server: TestServer,
+) -> Result<(), Error> {
+    let test_file_name = "duplicate test file.txt";
+    let test_file_contents = "Test File Contents";
+    let test_file_contents_new = "New Uploaded Test File Contents";
+
+    // Before uploading, check whether the uploaded file does not yet exist.
+    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
+    let parsed = Document::from_read(body)?;
+    assert!(parsed.find(Text).all(|x| x.text() != test_file_name));
+
+    // create the file
+    let test_file_path = write_file_contents(
+        server.path().to_path_buf(),
+        test_file_name,
+        test_file_contents,
+    );
+
+    // Perform the actual upload.
+    let upload_action = parsed
+        .find(Attr("id", "file_submit"))
+        .next()
+        .expect("Couldn't find element with id=file_submit")
+        .attr("action")
+        .expect("Upload form doesn't have action attribute");
+    // Then try to upload anyway
+    let form = multipart::Form::new();
+    let part = multipart::Part::text(test_file_contents_new)
+        .file_name(test_file_name)
+        .mime_str("text/plain")?;
+    let form = form.part("file_to_upload", part);
+
+    let client = Client::new();
+    client
+        .post(server.url().join(upload_action)?)
+        .multipart(form)
+        .send()?
+        .error_for_status()?;
+
+    // After uploading, uploaded file is still getting listed.
+    let body = reqwest::blocking::get(server.url())?;
+    let parsed = Document::from_read(body)?;
+    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));
+    // and assert the contents is different one from before
+    assert_file_contents(test_file_path, test_file_contents_new);
+
+    Ok(())
+}
+
+#[rstest]
+fn rename_duplicate_file(
+    #[with(&["--rename-duplicate", "-u"])] server: TestServer,
+) -> Result<(), Error> {
+    let test_file_name = "duplicate test file.txt";
+    let test_file_name_new = "duplicate test file-1.txt";
+    let test_file_contents = "Test File Contents";
+    let test_file_contents_new = "New Uploaded Test File Contents";
+
+    // Before uploading, check whether the uploaded file does not yet exist.
+    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
+    let parsed = Document::from_read(body)?;
+    assert!(parsed.find(Text).all(|x| x.text() != test_file_name));
+
+    // create the file
+    let test_file_path = write_file_contents(
+        server.path().to_path_buf(),
+        test_file_name,
+        test_file_contents,
+    );
+
+    // Perform the actual upload.
+    let upload_action = parsed
+        .find(Attr("id", "file_submit"))
+        .next()
+        .expect("Couldn't find element with id=file_submit")
+        .attr("action")
+        .expect("Upload form doesn't have action attribute");
+    // Then try to upload anyway
+    let form = multipart::Form::new();
+    let part = multipart::Part::text(test_file_contents_new)
+        .file_name(test_file_name)
+        .mime_str("text/plain")?;
+    let form = form.part("file_to_upload", part);
+
+    let client = Client::new();
+    client
+        .post(server.url().join(upload_action)?)
+        .multipart(form)
+        .send()?
+        .error_for_status()?;
+
+    // After uploading, make sure old and new files are both listed.
+    let body = reqwest::blocking::get(server.url())?;
+    let parsed = Document::from_read(body)?;
+    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));
+    assert!(parsed.find(Text).any(|x| x.text() == test_file_name_new));
+    // and assert the contents is same for old and new files
+    assert_file_contents(server.path().join(&test_file_path), test_file_contents);
+    assert_file_contents(
+        server.path().join(&test_file_name_new),
+        test_file_contents_new,
+    );
+
+    Ok(())
+}
+
+fn write_file_contents(path: PathBuf, filename: &str, contents: &str) -> PathBuf {
+    let file_path = path.join(filename);
+    let mut file = File::create(&file_path).unwrap();
+    file.write_all(contents.as_bytes())
+        .expect("Couldn't write file");
+    file_path
+}
+
+fn assert_file_contents(file_path: PathBuf, contents: &str) {
+    let file_contents = std::fs::read_to_string(&file_path).unwrap();
+    assert!(file_contents == contents)
+}

From 5edf363ae34b8f96ac9ecafa16f2efdb76d08ac6 Mon Sep 17 00:00:00 2001
From: Gaurav Atreya <allmanpride@gmail.com>
Date: Sat, 21 Sep 2024 21:17:01 -0400
Subject: [PATCH 4/6] More info on rename-duplicate arg

---
 src/args.rs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/args.rs b/src/args.rs
index 14773f88d..818bd62b0 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -199,6 +199,11 @@ pub struct CliArgs {
     pub overwrite_files: bool,
 
     /// Enable renaming files during file upload if duplicate exists
+    ///
+    /// The renaming will occur by adding numerical suffix to the
+    /// filename before the final extension. For example file.txt will
+    /// be uploaded as file-1.txt, the number will be increased untill
+    /// a available slot is found.
     #[arg(
         short = 'R',
         long = "rename-duplicate",

From a3956976dd6b84527d72c049f4bd967d727b2b89 Mon Sep 17 00:00:00 2001
From: Gaurav Atreya <allmanpride@gmail.com>
Date: Sat, 21 Sep 2024 23:11:02 -0400
Subject: [PATCH 5/6] Fix the duplicate file tests to check file exists before
 upload

---
 README.md             |  5 ++++-
 tests/upload_files.rs | 30 +++++++++++++++---------------
 2 files changed, 19 insertions(+), 16 deletions(-)

diff --git a/README.md b/README.md
index 2bad20a37..0aafaf0b4 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,7 @@ If a header is already set or previously inserted, it will not be overwritten.
     # Fullchain TLS and HTTP Strict Transport Security (HSTS)
     miniserve --tls-cert fullchain.pem --tls-key my.key --header "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload" /tmp/myshare
 
-If the parameter value has spaces, be sure to wrap it in quotes.  
+If the parameter value has spaces, be sure to wrap it in quotes.
 (To achieve an A+ rating at https://www.ssllabs.com/ssltest/, enabling both fullchain TLS and HSTS is necessary.)
 
 ### Upload a file using `curl`:
@@ -292,6 +292,9 @@ Options:
   -R, --rename-duplicate
           Enable renaming files during file upload if duplicate exists
 
+          The renaming will occur by adding numerical suffix to the filename before the final extension. For example file.txt will be uploaded as file-1.txt, the number will
+          be increased untill a available slot is found.
+
           [env: RENAME_DUPLICATE_FILES=]
 
   -r, --enable-tar
diff --git a/tests/upload_files.rs b/tests/upload_files.rs
index 35c5edb57..a8c4ac785 100644
--- a/tests/upload_files.rs
+++ b/tests/upload_files.rs
@@ -284,11 +284,6 @@ fn uploading_duplicate_file_is_prevented(#[with(&["-u"])] server: TestServer) ->
     let test_file_name = "duplicate test file.txt";
     let test_file_contents = "Test File Contents";
 
-    // Before uploading, check whether the uploaded file does not yet exist.
-    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
-    let parsed = Document::from_read(body)?;
-    assert!(parsed.find(Text).all(|x| x.text() != test_file_name));
-
     // create the file
     let test_file_path = write_file_contents(
         server.path().to_path_buf(),
@@ -296,6 +291,11 @@ fn uploading_duplicate_file_is_prevented(#[with(&["-u"])] server: TestServer) ->
         test_file_contents,
     );
 
+    // Before uploading, make sure the file is there.
+    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
+    let parsed = Document::from_read(body)?;
+    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));
+
     // Perform the actual upload.
     let upload_action = parsed
         .find(Attr("id", "file_submit"))
@@ -337,11 +337,6 @@ fn overwrite_duplicate_file(
     let test_file_contents = "Test File Contents";
     let test_file_contents_new = "New Uploaded Test File Contents";
 
-    // Before uploading, check whether the uploaded file does not yet exist.
-    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
-    let parsed = Document::from_read(body)?;
-    assert!(parsed.find(Text).all(|x| x.text() != test_file_name));
-
     // create the file
     let test_file_path = write_file_contents(
         server.path().to_path_buf(),
@@ -349,6 +344,11 @@ fn overwrite_duplicate_file(
         test_file_contents,
     );
 
+    // Before uploading, make sure the file is there.
+    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
+    let parsed = Document::from_read(body)?;
+    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));
+
     // Perform the actual upload.
     let upload_action = parsed
         .find(Attr("id", "file_submit"))
@@ -389,11 +389,6 @@ fn rename_duplicate_file(
     let test_file_contents = "Test File Contents";
     let test_file_contents_new = "New Uploaded Test File Contents";
 
-    // Before uploading, check whether the uploaded file does not yet exist.
-    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
-    let parsed = Document::from_read(body)?;
-    assert!(parsed.find(Text).all(|x| x.text() != test_file_name));
-
     // create the file
     let test_file_path = write_file_contents(
         server.path().to_path_buf(),
@@ -401,6 +396,11 @@ fn rename_duplicate_file(
         test_file_contents,
     );
 
+    // Before uploading, make sure the file is there.
+    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
+    let parsed = Document::from_read(body)?;
+    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));
+
     // Perform the actual upload.
     let upload_action = parsed
         .find(Attr("id", "file_submit"))

From 897acb2e5313fc4886b514395d9b75eb61182ed9 Mon Sep 17 00:00:00 2001
From: Gaurav Atreya <allmanpride@gmail.com>
Date: Mon, 23 Sep 2024 18:43:46 -0400
Subject: [PATCH 6/6] Combine overwrite/rename/error on file with duplicate
 name into enum

---
 src/args.rs           | 33 ++++++++++++++-----------
 src/config.rs         | 12 +++------
 src/errors.rs         |  4 +--
 src/file_op.rs        | 57 +++++++++++++++++++------------------------
 tests/upload_files.rs |  9 ++++---
 5 files changed, 55 insertions(+), 60 deletions(-)

diff --git a/src/args.rs b/src/args.rs
index 818bd62b0..1dbba6d83 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -15,6 +15,14 @@ pub enum MediaType {
     Video,
 }
 
+#[derive(ValueEnum, Clone, Default, Copy)]
+pub enum DuplicateFile {
+    #[default]
+    Error,
+    Overwrite,
+    Rename,
+}
+
 #[derive(Parser)]
 #[command(name = "miniserve", author, about, version)]
 pub struct CliArgs {
@@ -194,23 +202,20 @@ pub struct CliArgs {
     )]
     pub media_type_raw: Option<String>,
 
-    /// Enable overriding existing files during file upload
-    #[arg(short = 'o', long = "overwrite-files", env = "OVERWRITE_FILES")]
-    pub overwrite_files: bool,
-
-    /// Enable renaming files during file upload if duplicate exists
+    /// What to do if existing files with same name is present during file upload
     ///
-    /// The renaming will occur by adding numerical suffix to the
-    /// filename before the final extension. For example file.txt will
-    /// be uploaded as file-1.txt, the number will be increased untill
-    /// a available slot is found.
+    /// If you enable renaming files, the renaming will occur by
+    /// adding numerical suffix to the filename before the final
+    /// extension. For example file.txt will be uploaded as
+    /// file-1.txt, the number will be increased untill a available
+    /// slot is found.
     #[arg(
-        short = 'R',
-        long = "rename-duplicate",
-        env = "RENAME_DUPLICATE_FILES",
-        conflicts_with = "overwrite_files"
+        short = 'o',
+        long = "on-duplicate-files",
+        env = "ON_DUPLICATE_FILES",
+        default_value = "error"
     )]
-    pub rename_duplicate: bool,
+    pub on_duplicate_files: DuplicateFile,
 
     /// Enable uncompressed tar archive generation
     #[arg(short = 'r', long = "enable-tar", env = "MINISERVE_ENABLE_TAR")]
diff --git a/src/config.rs b/src/config.rs
index 0c0feccd2..6e2a10076 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -12,7 +12,7 @@ use anyhow::{anyhow, Context, Result};
 use rustls_pemfile as pemfile;
 
 use crate::{
-    args::{parse_auth, CliArgs, MediaType},
+    args::{parse_auth, CliArgs, DuplicateFile, MediaType},
     auth::RequiredAuth,
     file_utils::sanitize_path,
     listing::{SortingMethod, SortingOrder},
@@ -107,11 +107,8 @@ pub struct MiniserveConfig {
     /// HTML accept attribute value
     pub uploadable_media_type: Option<String>,
 
-    /// Enable upload to override existing files
-    pub overwrite_files: bool,
-
-    /// Enable renaming files during file upload if duplicate exists
-    pub rename_duplicate: bool,
+    /// What to do on upload if filename already exists
+    pub on_duplicate_files: DuplicateFile,
 
     /// If false, creation of uncompressed tar archives is disabled
     pub tar_enabled: bool,
@@ -291,8 +288,7 @@ impl MiniserveConfig {
             index: args.index,
             spa: args.spa,
             pretty_urls: args.pretty_urls,
-            overwrite_files: args.overwrite_files,
-            rename_duplicate: args.rename_duplicate,
+            on_duplicate_files: args.on_duplicate_files,
             show_qrcode: args.qrcode,
             mkdir_enabled: args.mkdir_enabled,
             file_upload: args.allowed_upload_dir.is_some(),
diff --git a/src/errors.rs b/src/errors.rs
index 7e9599caa..88ae0fa33 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -37,9 +37,7 @@ pub enum RuntimeError {
     MultipartError(String),
 
     /// Might occur during file upload
-    #[error(
-        "File already exists, and the overwrite_files or rename_duplicate option has not been set"
-    )]
+    #[error("File already exists, and the on_duplicate_files option is set to error out")]
     DuplicateFileError,
 
     /// Upload not allowed
diff --git a/src/file_op.rs b/src/file_op.rs
index 9bf554c49..bb7644e02 100644
--- a/src/file_op.rs
+++ b/src/file_op.rs
@@ -10,6 +10,7 @@ use serde::Deserialize;
 use tokio::fs::File;
 use tokio::io::AsyncWriteExt;
 
+use crate::args::DuplicateFile;
 use crate::{
     config::MiniserveConfig, errors::RuntimeError, file_utils::contains_symlink,
     file_utils::sanitize_path,
@@ -22,31 +23,31 @@ use crate::{
 async fn save_file(
     field: actix_multipart::Field,
     mut file_path: PathBuf,
-    overwrite_files: bool,
-    rename_duplicate: bool,
+    on_duplicate_files: DuplicateFile,
 ) -> Result<u64, RuntimeError> {
     if file_path.exists() {
-        // clap makes sure both overwrite_files and rename_duplicate cannot be true
-        if rename_duplicate {
-            // extract extension of the file and the file stem without extension
-            // file.txt => (file, txt)
-            let file_name = file_path.file_stem().unwrap_or_default().to_string_lossy();
-            let file_ext = file_path.extension().map(|s| s.to_string_lossy());
-            for i in 1.. {
-                // increment the number N in {file_name}-{N}.{file_ext}
-                // format until available name is found (e.g. file-1.txt, file-2.txt, etc)
-                let fp = if let Some(ext) = &file_ext {
-                    file_path.with_file_name(format!("{}-{}.{}", file_name, i, ext))
-                } else {
-                    file_path.with_file_name(format!("{}-{}", file_name, i))
-                };
-                if !fp.exists() {
-                    file_path = fp;
-                    break;
+        match on_duplicate_files {
+            DuplicateFile::Error => return Err(RuntimeError::DuplicateFileError),
+            DuplicateFile::Overwrite => (),
+            DuplicateFile::Rename => {
+                // extract extension of the file and the file stem without extension
+                // file.txt => (file, txt)
+                let file_name = file_path.file_stem().unwrap_or_default().to_string_lossy();
+                let file_ext = file_path.extension().map(|s| s.to_string_lossy());
+                for i in 1.. {
+                    // increment the number N in {file_name}-{N}.{file_ext}
+                    // format until available name is found (e.g. file-1.txt, file-2.txt, etc)
+                    let fp = if let Some(ext) = &file_ext {
+                        file_path.with_file_name(format!("{}-{}.{}", file_name, i, ext))
+                    } else {
+                        file_path.with_file_name(format!("{}-{}", file_name, i))
+                    };
+                    if !fp.exists() {
+                        file_path = fp;
+                        break;
+                    }
                 }
             }
-        } else if !overwrite_files {
-            return Err(RuntimeError::DuplicateFileError);
         }
     }
 
@@ -78,8 +79,7 @@ async fn save_file(
 async fn handle_multipart(
     mut field: actix_multipart::Field,
     path: PathBuf,
-    overwrite_files: bool,
-    rename_duplicate: bool,
+    on_duplicate_files: DuplicateFile,
     allow_mkdir: bool,
     allow_hidden_paths: bool,
     allow_symlinks: bool,
@@ -191,13 +191,7 @@ async fn handle_multipart(
         }
     }
 
-    save_file(
-        field,
-        path.join(filename_path),
-        overwrite_files,
-        rename_duplicate,
-    )
-    .await
+    save_file(field, path.join(filename_path), on_duplicate_files).await
 }
 
 /// Query parameters used by upload and rm APIs
@@ -253,8 +247,7 @@ pub async fn upload_file(
             handle_multipart(
                 field,
                 non_canonicalized_target_dir.clone(),
-                conf.overwrite_files,
-                conf.rename_duplicate,
+                conf.on_duplicate_files,
                 conf.mkdir_enabled,
                 conf.show_hidden,
                 !conf.no_symlinks,
diff --git a/tests/upload_files.rs b/tests/upload_files.rs
index a8c4ac785..65787f077 100644
--- a/tests/upload_files.rs
+++ b/tests/upload_files.rs
@@ -280,7 +280,10 @@ fn set_media_type(
 }
 
 #[rstest]
-fn uploading_duplicate_file_is_prevented(#[with(&["-u"])] server: TestServer) -> Result<(), Error> {
+#[case(server(&["-u"]))]
+#[case(server(&["-u", "-o", "error"]))]
+#[case(server(&["-u", "--on-duplicate-files", "error"]))]
+fn uploading_duplicate_file_is_prevented(#[case] server: TestServer) -> Result<(), Error> {
     let test_file_name = "duplicate test file.txt";
     let test_file_contents = "Test File Contents";
 
@@ -331,7 +334,7 @@ fn uploading_duplicate_file_is_prevented(#[with(&["-u"])] server: TestServer) ->
 
 #[rstest]
 fn overwrite_duplicate_file(
-    #[with(&["--overwrite-files", "-u"])] server: TestServer,
+    #[with(&["-u", "--on-duplicate-files", "overwrite"])] server: TestServer,
 ) -> Result<(), Error> {
     let test_file_name = "duplicate test file.txt";
     let test_file_contents = "Test File Contents";
@@ -382,7 +385,7 @@ fn overwrite_duplicate_file(
 
 #[rstest]
 fn rename_duplicate_file(
-    #[with(&["--rename-duplicate", "-u"])] server: TestServer,
+    #[with(&["-u", "--on-duplicate-files", "rename"])] server: TestServer,
 ) -> Result<(), Error> {
     let test_file_name = "duplicate test file.txt";
     let test_file_name_new = "duplicate test file-1.txt";