Repository: kestra-io/kestra (java)

Issue:
FileAlreadyExistsException if 2nd Python task is added to WorkingDirectory after Git Clone — namespaceFiles are added to the same wdir. Add a new `ifExists` Enum property
### Describe the issue

When you use `io.kestra.plugin.git.Clone` within `io.kestra.plugin.core.flow.WorkingDirectory` and try to process the files with `io.kestra.plugin.scripts.python.Commands`, the first task works, the second one gives a `FileAlreadyExistsException` error.

This is the flow:

```
id: test_workdir_issue
namespace: prod
description: Checks the entire notes repository for issues.

tasks:
  - id: git_wdir
    type: io.kestra.plugin.core.flow.WorkingDirectory
    disabled: false
    tasks:
      - id: clone
        type: io.kestra.plugin.git.Clone
        branch: main
        url: https://github.com/kestra-io/examples

      - id: python_command_1
        type: io.kestra.plugin.scripts.python.Commands
        namespaceFiles:
          enabled: true
        commands:
          - python scripts/sample_python.py

      - id: python_command_2
        disabled: false
        type: io.kestra.plugin.scripts.python.Commands
        namespaceFiles:
          enabled: true
        commands:
          - python scripts/sample_python.py
```

As you can see, the two tasks execute the same Python file.

This is the Python file:

```
import os
from kestra import Kestra


# Define the directory path
directory_path = '.'

# Walk through the directory
for root, dirs, files in os.walk(directory_path):
    for file in files:
      print(file)
```

This is the only file I have in the namespace:

![Image](https://github.com/user-attachments/assets/48a77e46-f1ef-4a47-ad5b-e6b6166285da)

When you execute this flow, task1 executes without issues but task2 fails. This is what you get:

```
2025-01-31 11:35:38.341Using task runner 'io.kestra.plugin.scripts.runner.docker.Docker'
2025-01-31 11:35:38.342java.nio.file.FileAlreadyExistsException: /tmp/kestra-wd/tmp/6X16rIjYcDw5bJhpOWjeEw/scripts/sample_python.py
 at java.base/sun.nio.fs.UnixException.translateToIOException(Unknown Source)
 at java.base/sun.nio.fs.UnixException.rethrowAsIOException(Unknown Source)
 at java.base/sun.nio.fs.UnixException.rethrowAsIOException(Unknown Source)
 at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(Unknown Source)
 at java.base/java.nio.file.Files.newByteChannel(Unknown Source)
 at java.base/java.nio.file.Files.createFile(Unknown Source)
 at io.kestra.core.runners.LocalWorkingDir.createFile(LocalWorkingDir.java:163)
 at io.kestra.plugin.scripts.exec.scripts.runners.CommandsWrapper.lambda$run$0(CommandsWrapper.java:143)
 at io.kestra.core.utils.Rethrow.lambda$throwConsumer$0(Rethrow.java:50)
 at java.base/java.lang.Iterable.forEach(Unknown Source)
 at io.kestra.plugin.scripts.exec.scripts.runners.CommandsWrapper.run(CommandsWrapper.java:141)
 at io.kestra.plugin.scripts.python.Commands.run(Commands.java:273)
 at io.kestra.plugin.scripts.python.Commands.run(Commands.java:19)
 at io.kestra.core.runners.WorkerTaskCallable.doCall(WorkerTaskCallable.java:78)
 at io.kestra.core.runners.AbstractWorkerCallable.call(AbstractWorkerCallable.java:56)
 at io.kestra.core.runners.WorkerSecurityService.callInSecurityContext(WorkerSecurityService.java:10)
 at io.kestra.core.runners.Worker.callJob(Worker.java:782)
 at io.kestra.core.runners.Worker.runAttempt(Worker.java:741)
 at io.kestra.core.runners.Worker.run(Worker.java:589)
 at io.kestra.core.runners.Worker.handleTask(Worker.java:328)
 at io.kestra.core.runners.Worker.lambda$run$8(Worker.java:250)
 at io.micrometer.core.instrument.internal.TimedRunnable.run(TimedRunnable.java:49)
 at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
 at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
 at java.base/java.lang.Thread.run(Unknown Source)
2025-01-31 11:35:38.342/tmp/kestra-wd/tmp/6X16rIjYcDw5bJhpOWjeEw/scripts/sample_python.py
```

The interesting part is that `FileAlreadyExistsException` always shows a file conflict with the file that shows up last in the namespace. For example, when we add a random filename like this:

![Image](https://github.com/user-attachments/assets/cd73d748-eb86-4aea-92ff-d9259536d280)

The second task now throws an error like this:

```
2025-01-31 11:39:46.774Using task runner 'io.kestra.plugin.scripts.runner.docker.Docker'
2025-01-31 11:39:46.775java.nio.file.FileAlreadyExistsException: /tmp/kestra-wd/tmp/7Oghb5vKw1gL3WYpIcka83/test/blabla-test.txt
 at java.base/sun.nio.fs.UnixException.translateToIOException(Unknown Source)
 at java.base/sun.nio.fs.UnixException.rethrowAsIOException(Unknown Source)
 at java.base/sun.nio.fs.UnixException.rethrowAsIOException(Unknown Source)
 at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(Unknown Source)
 at java.base/java.nio.file.Files.newByteChannel(Unknown Source)
 at java.base/java.nio.file.Files.createFile(Unknown Source)
 at io.kestra.core.runners.LocalWorkingDir.createFile(LocalWorkingDir.java:163)
 at io.kestra.plugin.scripts.exec.scripts.runners.CommandsWrapper.lambda$run$0(CommandsWrapper.java:143)
 at io.kestra.core.utils.Rethrow.lambda$throwConsumer$0(Rethrow.java:50)
 at java.base/java.lang.Iterable.forEach(Unknown Source)
 at io.kestra.plugin.scripts.exec.scripts.runners.CommandsWrapper.run(CommandsWrapper.java:141)
 at io.kestra.plugin.scripts.python.Commands.run(Commands.java:273)
 at io.kestra.plugin.scripts.python.Commands.run(Commands.java:19)
 at io.kestra.core.runners.WorkerTaskCallable.doCall(WorkerTaskCallable.java:78)
 at io.kestra.core.runners.AbstractWorkerCallable.call(AbstractWorkerCallable.java:56)
 at io.kestra.core.runners.WorkerSecurityService.callInSecurityContext(WorkerSecurityService.java:10)
 at io.kestra.core.runners.Worker.callJob(Worker.java:782)
 at io.kestra.core.runners.Worker.runAttempt(Worker.java:741)
 at io.kestra.core.runners.Worker.run(Worker.java:589)
 at io.kestra.core.runners.Worker.handleTask(Worker.java:328)
 at io.kestra.core.runners.Worker.lambda$run$8(Worker.java:250)
 at io.micrometer.core.instrument.internal.TimedRunnable.run(TimedRunnable.java:49)
 at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
 at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
 at java.base/java.lang.Thread.run(Unknown Source)
2025-01-31 11:39:46.775/tmp/kestra-wd/tmp/7Oghb5vKw1gL3WYpIcka83/test/blabla-test.txt
```

### Environment

- Kestra Version: 0.20.14 and develop both have the same issue.


Pull request description:
feat(core): #7932 add file exist comportment to NamespaceFiles
<!-- Thanks for submitting a Pull Request to Kestra. To help us review your contribution, please follow the guidelines below:

- Make sure that your commits follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) specification e.g. `feat(ui): add a new navigation menu item` or `fix(core): fix a bug in the core model` or `docs: update the README.md`. This will help us automatically generate the changelog.
- The title should briefly summarize the proposed changes.
- Provide a short overview of the change and the value it adds.
- Share a flow example to help the reviewer understand and QA the change.
- Use "closes" to automatically close an issue. For example, `closes #1234` will close issue #1234. -->
Fixes #7932
### What changes are being made and why?

<!-- Please include a brief summary of the changes included in this PR e.g. closes #1234. -->

---

### How the changes have been QAed?

<!-- Include example code that shows how this PR has been QAed. The code should present a complete yet easily reproducible flow.

```yaml
# Your example flow code here
```

Note that this is not a replacement for unit tests but rather a way to demonstrate how the changes work in a real-life scenario, as the end-user would experience them.

Remove this section if this change applies to all flows or to the documentation only. -->

---

### Setup Instructions

<!--If there are any setup requirements like API keys or trial accounts, kindly include brief bullet-points-description outlining the setup process below.

- [External System Documentation](URL)
- Steps to set up the necessary resources

If there are no setup requirements, you can remove this section.

Thank you for your contribution. ❤️  -->


Patch:
diff --git a/core/src/main/java/io/kestra/core/models/tasks/FileExistComportment.java b/core/src/main/java/io/kestra/core/models/tasks/FileExistComportment.java
new file mode 100644
index 000000000..a44507c7c
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/models/tasks/FileExistComportment.java
@@ -0,0 +1,5 @@
+package io.kestra.core.models.tasks;
+
+public enum FileExistComportment {
+    OVERWRITE, FAIL, WARN, IGNORE
+}
diff --git a/core/src/main/java/io/kestra/core/models/tasks/NamespaceFiles.java b/core/src/main/java/io/kestra/core/models/tasks/NamespaceFiles.java
index 9625fdd41..0b4aeeee0 100644
--- a/core/src/main/java/io/kestra/core/models/tasks/NamespaceFiles.java
+++ b/core/src/main/java/io/kestra/core/models/tasks/NamespaceFiles.java
@@ -42,4 +42,10 @@ public class NamespaceFiles {
     )
     @Builder.Default
     private Property<List<String>> namespaces = Property.of(List.of("{{flow.namespace}}"));
+
+    @Schema(
+        title = "Comportment of the task if a file already exist in the working directory."
+    )
+    @Builder.Default
+    private Property<FileExistComportment> ifExists = Property.of(FileExistComportment.OVERWRITE);
 }
diff --git a/core/src/main/java/io/kestra/core/runners/LocalWorkingDir.java b/core/src/main/java/io/kestra/core/runners/LocalWorkingDir.java
index 4c199efd9..6b610557c 100644
--- a/core/src/main/java/io/kestra/core/runners/LocalWorkingDir.java
+++ b/core/src/main/java/io/kestra/core/runners/LocalWorkingDir.java
@@ -1,7 +1,10 @@
 package io.kestra.core.runners;
 
+import io.kestra.core.models.tasks.FileExistComportment;
 import io.kestra.core.utils.IdUtils;
 import io.kestra.core.utils.PathMatcherPredicate;
+import java.nio.file.FileAlreadyExistsException;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.io.FileUtils;
 
 import java.io.ByteArrayInputStream;
@@ -23,6 +26,7 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 /**
  * Default implementation of the {@link WorkingDir}.
  */
+@Slf4j
 public class LocalWorkingDir implements WorkingDir {
 
     private final Path workingDirPath;
@@ -175,7 +179,7 @@ public class LocalWorkingDir implements WorkingDir {
      * {@inheritDoc}
      **/
     @Override
-    public Path putFile(Path path, InputStream inputStream) throws IOException {
+    public Path putFile(Path path, InputStream inputStream, FileExistComportment comportment) throws IOException {
         if (path == null) {
             throw new IllegalArgumentException("Cannot create a working directory file with a null path");
         }
@@ -185,17 +189,27 @@ public class LocalWorkingDir implements WorkingDir {
         Path newFilePath = this.resolve(path);
         Files.createDirectories(newFilePath.getParent());
 
-        if (Files.notExists(newFilePath)) {
+        if (Files.exists(newFilePath)) {
+            switch (comportment) {
+                case OVERWRITE -> copyFile(inputStream, newFilePath);
+                case FAIL -> throw new FileAlreadyExistsException("File " + newFilePath + " already exist");
+                case WARN -> log.warn("File {} already exist. It will be ignore", newFilePath);
+                case IGNORE -> {}
+            }
+        } else {
             Files.createFile(newFilePath);
-        }
-
-        try (inputStream) {
-            Files.copy(inputStream, newFilePath, REPLACE_EXISTING);
+            copyFile(inputStream, newFilePath);
         }
 
         return newFilePath;
     }
 
+    private static void copyFile(InputStream inputStream, Path path) throws IOException {
+        try(inputStream) {
+            Files.copy(inputStream, path, REPLACE_EXISTING);
+        }
+    }
+
     /**
      * {@inheritDoc}
      **/
diff --git a/core/src/main/java/io/kestra/core/runners/WorkingDir.java b/core/src/main/java/io/kestra/core/runners/WorkingDir.java
index 7a993ce5e..ff6e143ff 100644
--- a/core/src/main/java/io/kestra/core/runners/WorkingDir.java
+++ b/core/src/main/java/io/kestra/core/runners/WorkingDir.java
@@ -1,5 +1,6 @@
 package io.kestra.core.runners;
 
+import io.kestra.core.models.tasks.FileExistComportment;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.FileAlreadyExistsException;
@@ -132,12 +133,13 @@ public interface WorkingDir {
     /**
      * Creates a new file or replaces an existing one with the given content.
      *
-     * @param path    The path of the file.
-     * @param content The file content - may be {@code null}.
+     * @param path        The path of the file.
+     * @param content     The file content - may be {@code null}.
+     * @param comportment How to react if the file already exist in the working directory
      * @throws IOException              if an error happens while creating the file.
      * @throws IllegalArgumentException if the given path is {@code null}.
      */
-    Path putFile(Path path, InputStream content) throws IOException;
+    Path putFile(Path path, InputStream content, FileExistComportment comportment) throws IOException;
 
     /**
      * Finds all files  in the working directory that matches one of the given patterns.
diff --git a/core/src/main/java/io/kestra/core/utils/NamespaceFilesUtils.java b/core/src/main/java/io/kestra/core/utils/NamespaceFilesUtils.java
index 98bf6b2eb..533c356f4 100644
--- a/core/src/main/java/io/kestra/core/utils/NamespaceFilesUtils.java
+++ b/core/src/main/java/io/kestra/core/utils/NamespaceFilesUtils.java
@@ -2,6 +2,7 @@ package io.kestra.core.utils;
 
 import io.kestra.core.models.executions.metrics.Counter;
 import io.kestra.core.models.executions.metrics.Timer;
+import io.kestra.core.models.tasks.FileExistComportment;
 import io.kestra.core.models.tasks.NamespaceFiles;
 import io.kestra.core.runners.RunContext;
 import io.kestra.core.storages.NamespaceFile;
@@ -21,9 +22,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
 
 import static io.kestra.core.utils.Rethrow.throwConsumer;
 
@@ -49,6 +48,8 @@ public class NamespaceFilesUtils {
 
         List<String> include = runContext.render(namespaceFiles.getInclude()).asList(String.class);
         List<String> exclude = runContext.render(namespaceFiles.getExclude()).asList(String.class);
+        FileExistComportment fileExistComportment = runContext.render(namespaceFiles.getIfExists())
+            .as(FileExistComportment.class).orElse(FileExistComportment.OVERWRITE);
         List<String> namespaces = runContext.render(namespaceFiles.getNamespaces()).asList(String.class);
 
         Map<String, NamespaceFile> namespaceFileMap = new HashMap<>();
@@ -67,7 +68,7 @@ public class NamespaceFilesUtils {
         Flux.fromIterable(matchedNamespaceFiles)
             .doOnNext(throwConsumer(namespaceFile -> {
                 InputStream content = runContext.storage().getFile(namespaceFile.uri());
-                runContext.workingDir().putFile(Path.of(namespaceFile.path()), content);
+                runContext.workingDir().putFile(Path.of(namespaceFile.path()), content, fileExistComportment);
             }))
             .publishOn(Schedulers.fromExecutorService(executorService))
             .blockLast();


Test patch:
diff --git a/core/src/test/java/io/kestra/core/runners/LocalWorkingDirTest.java b/core/src/test/java/io/kestra/core/runners/LocalWorkingDirTest.java
index f56988f42..97a910642 100644
--- a/core/src/test/java/io/kestra/core/runners/LocalWorkingDirTest.java
+++ b/core/src/test/java/io/kestra/core/runners/LocalWorkingDirTest.java
@@ -1,6 +1,8 @@
 package io.kestra.core.runners;
 
+import io.kestra.core.models.tasks.FileExistComportment;
 import io.kestra.core.utils.IdUtils;
+import java.io.ByteArrayInputStream;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -120,4 +122,23 @@ class LocalWorkingDirTest {
         assertThat(secondPath.toFile().exists(), is(true));
         assertThat(firtPath, equalTo(secondPath));
     }
+
+    @Test
+    void should_put_file_into_local_dir() throws IOException {
+        LocalWorkingDir workingDir = new LocalWorkingDir(Path.of("/tmp/"), IdUtils.create());
+        workingDir.path(true);
+        Path file = workingDir.createFile("test.txt", new ByteArrayInputStream("First file".getBytes(StandardCharsets.UTF_8)));
+
+        assertThrows(FileAlreadyExistsException.class, () -> workingDir.putFile(file, new ByteArrayInputStream("Hello world".getBytes(StandardCharsets.UTF_8)), FileExistComportment.FAIL));
+        assertThat(Files.readAllLines(file, StandardCharsets.UTF_8), is(List.of("First file")));
+
+        workingDir.putFile(file, new ByteArrayInputStream("Hello world".getBytes(StandardCharsets.UTF_8)), FileExistComportment.IGNORE);
+        assertThat(Files.readAllLines(file, StandardCharsets.UTF_8), is(List.of("First file")));
+
+        workingDir.putFile(file, new ByteArrayInputStream("Hello world".getBytes(StandardCharsets.UTF_8)), FileExistComportment.WARN);
+        assertThat(Files.readAllLines(file, StandardCharsets.UTF_8), is(List.of("First file")));
+
+        workingDir.putFile(file, new ByteArrayInputStream("New file".getBytes(StandardCharsets.UTF_8)), FileExistComportment.OVERWRITE);
+        assertThat(Files.readAllLines(file, StandardCharsets.UTF_8), is(List.of("New file")));
+    }
 }
\ No newline at end of file
diff --git a/core/src/test/java/io/kestra/core/runners/TestWorkingDir.java b/core/src/test/java/io/kestra/core/runners/TestWorkingDir.java
index acb7f1f64..36e146e08 100644
--- a/core/src/test/java/io/kestra/core/runners/TestWorkingDir.java
+++ b/core/src/test/java/io/kestra/core/runners/TestWorkingDir.java
@@ -1,5 +1,6 @@
 package io.kestra.core.runners;
 
+import io.kestra.core.models.tasks.FileExistComportment;
 import io.kestra.core.utils.IdUtils;
 
 import java.io.IOException;
@@ -91,8 +92,8 @@ public final class TestWorkingDir implements WorkingDir {
     }
 
     @Override
-    public Path putFile(Path path, InputStream content) throws IOException {
-        return captureCreateFileAndGet(delegate.putFile(path, content));
+    public Path putFile(Path path, InputStream content, FileExistComportment comportment) throws IOException {
+        return captureCreateFileAndGet(delegate.putFile(path, content, comportment));
     }
 
     @Override