← Commit list

f89eca9cd6

Daniel Stenberg
Daniel Stenberg
2026-09-04
SUCCESS Duration: 389s

Subject

tool_cb_wrt: inject the no-clobber number before the extension

Binary

No binary (build failed)

Architecture & Compiler Flags

Architecture: x86_64-linux-gnu Compiler: gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0

—

Build Log

(log yok)

Commit Diff GitHub →

docs/cmdline-opts/no-clobber.md+5 -0
@@ -21,6 +21,11 @@ that already exist. Instead, a dot and a number gets appended to the name of
the file that would be created, up to filename.9999 after which it does not
create any file.
+Starting in curl 8.23.0, if the target filename uses an extension, curl
+instead creates the new filename by inserting `-[num]` before the extension.
+Downloading to `image.jpg` with --no-clobber, curl first tries to save it as
+`image-1.jpg` then `image-2.jpg` etc.
+
Note that this is the negated option name documented. You can thus use
--clobber to enforce the clobbering, even if --remote-header-name is
specified.
src/tool_cb_wrt.c+35 -2
@@ -36,6 +36,40 @@
#define MAX_CLOBBER_ATTEMPTS 9999
+/* 'fname' contains [path + ] file name */
+static bool clobber_name(struct dynbuf *fbuf, const char *fname,
+ int next_num)
+{
+ /* skip the directory, if present */
+ const char *slash = strrchr(fname, '/');
+ const char *base = fname;
+ const char *dot;
+#ifdef _WIN32
+ const char *bslash = strrchr(fname, '\\');
+ /* use the one furthest away */
+ if(bslash && (!slash || (bslash > slash)))
+ slash = bslash;
+#endif
+ if(slash)
+ base = slash + 1;
+
+ dot = strrchr(base, '.');
+ curlx_dyn_reset(fbuf);
+ if(dot && (dot != base) && dot[1]) {
+ /* there is an extension after the dot and the dot is not the first file
+ name character */
+ int prefix = (int)(dot - fname);
+ if(curlx_dyn_addf(fbuf, "%.*s-%d.%s", prefix, fname, next_num, dot + 1))
+ return FALSE;
+ }
+ else {
+ /* without any extension */
+ if(curlx_dyn_addf(fbuf, "%s.%d", fname, next_num))
+ return FALSE;
+ }
+ return TRUE;
+}
+
/* create/open a local file for writing, return TRUE on success */
bool tool_create_output_file(struct OutStruct *outs,
struct OperationConfig *config)
@@ -82,8 +116,7 @@ bool tool_create_output_file(struct OutStruct *outs,
(errno == EEXIST || errno == EISDIR) &&
/* and we have not reached the retry limit */
(next_num < max_num)) {
- curlx_dyn_reset(&fbuffer);
- if(curlx_dyn_addf(&fbuffer, "%s.%d", fname, next_num))
+ if(!clobber_name(&fbuffer, fname, next_num))
return FALSE;
next_num++;
do {
tests/data/Makefile.am+1 -1
@@ -2096,7 +2096,7 @@ TESTCASES = \
\
test4000 \
test4001 \
- \
+ test4644 \
test5000 \
test5001 \
test5002 \
tests/data/test4644+56 -0
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+<testcase>
+<info>
+<keywords>
+--no-clobber
+</keywords>
+</info>
+
+<reply>
+<data nocheck="yes">
+HTTP/1.1 200 OK swsclose
+Content-Length: 12
+Connection: close
+Content-Type: text/html
+
+new content
+</data>
+</reply>
+
+<client>
+<file name="%LOGDIR/save.txt">
+exists before command runs
+</file>
+<file1 name="%LOGDIR/save-1.txt">
+exists before command runs
+</file1>
+<server>
+http
+</server>
+<name>
+--no-clobber with extension and an added number
+</name>
+<command option="no-output,no-include">
+http://%HOSTIP:%HTTPPORT/%TESTNUMBER -o %LOGDIR/save.txt --no-clobber
+</command>
+</client>
+
+# Verify data after the test has been "shot".
+<verify>
+<protocol crlf="headers">
+GET /%TESTNUMBER HTTP/1.1
+Host: %HOSTIP:%HTTPPORT
+User-Agent: curl/%VERSION
+Accept: */*
+
+</protocol>
+
+# this file should be untouched
+<file name="%LOGDIR/save.txt">
+exists before command runs
+</file>
+<file2 name="%LOGDIR/save-2.txt">
+new content
+</file2>
+</verify>
+</testcase>