This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new c45768cba6 Cleanup XML of action Copy Files, fixes #1958 (#6930)
c45768cba6 is described below

commit c45768cba6d6fa409a14611aacbc4cad83fc859f
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Apr 2 14:22:21 2026 +0200

    Cleanup XML of action Copy Files, fixes #1958 (#6930)
    
    * Cleanup XML of action Copy Files, fixes #1958
    
    * track byte count
---
 .../actions/copyfiles/ActionCopyFiles.java         | 1171 +++++++-------------
 .../actions/copyfiles/ActionCopyFilesDialog.java   |  186 +---
 .../workflow/actions/copyfiles/CopyFilesItem.java  |   65 ++
 .../copyfiles/messages/messages_de_DE.properties   |    7 -
 .../copyfiles/messages/messages_en_US.properties   |   23 +-
 .../copyfiles/messages/messages_es_AR.properties   |    7 -
 .../copyfiles/messages/messages_fr_FR.properties   |    7 -
 .../copyfiles/messages/messages_it_IT.properties   |    7 -
 .../copyfiles/messages/messages_ja_JP.properties   |    7 -
 .../copyfiles/messages/messages_ko_KR.properties   |    9 +-
 .../copyfiles/messages/messages_nl_NL.properties   |    7 -
 .../copyfiles/messages/messages_pt_BR.properties   |    7 -
 .../copyfiles/messages/messages_zh_CN.properties   |    7 -
 .../src/main/samples/actions/copy-files.hwf        |    6 +-
 .../WorkflowActionCopyFilesLoadSaveTest.java       |  163 ++-
 .../WorkflowActionCopyFilesLocalTest.java          |  281 +++++
 .../copyfiles/WorkflowActionCopyFilesTest.java     |   55 +-
 .../src/test/resources/copy-files-action.xml       |   48 +
 18 files changed, 1018 insertions(+), 1045 deletions(-)

diff --git 
a/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/ActionCopyFiles.java
 
b/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/ActionCopyFiles.java
index db54c9c2b7..e1c11f4762 100644
--- 
a/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/ActionCopyFiles.java
+++ 
b/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/ActionCopyFiles.java
@@ -18,24 +18,23 @@
 package org.apache.hop.workflow.actions.copyfiles;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.apache.commons.vfs2.FileFilterSelector;
-import org.apache.commons.vfs2.FileName;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.commons.vfs2.FileSelectInfo;
 import org.apache.commons.vfs2.FileSelector;
 import org.apache.commons.vfs2.FileSystemException;
 import org.apache.commons.vfs2.FileType;
 import org.apache.commons.vfs2.NameScope;
-import org.apache.commons.vfs2.filter.NameFileFilter;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.ICheckResult;
 import org.apache.hop.core.Result;
@@ -43,16 +42,17 @@ import org.apache.hop.core.ResultFile;
 import org.apache.hop.core.RowMetaAndData;
 import org.apache.hop.core.annotations.Action;
 import org.apache.hop.core.exception.HopException;
-import org.apache.hop.core.exception.HopXmlException;
+import org.apache.hop.core.io.CountingInputStream;
+import org.apache.hop.core.io.CountingOutputStream;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.vfs.HopVfs;
-import org.apache.hop.core.xml.XmlHandler;
 import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataProperty;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.serializer.xml.ILegacyXml;
 import org.apache.hop.workflow.WorkflowMeta;
 import org.apache.hop.workflow.action.ActionBase;
-import org.apache.hop.workflow.action.IAction;
 import org.apache.hop.workflow.action.validator.AbstractFileValidator;
 import org.apache.hop.workflow.action.validator.ActionValidatorUtils;
 import org.apache.hop.workflow.action.validator.AndValidator;
@@ -61,7 +61,8 @@ import org.apache.hop.workflow.engine.IWorkflowEngine;
 import org.w3c.dom.Node;
 
 /** This defines a 'copy files' action. */
-@SuppressWarnings("java:S1104")
+@Getter
+@Setter
 @Action(
     id = "COPY_FILES",
     name = "i18n::ActionCopyFiles.Name",
@@ -70,204 +71,154 @@ import org.w3c.dom.Node;
     categoryDescription = 
"i18n:org.apache.hop.workflow:ActionCategory.Category.FileManagement",
     keywords = "i18n::ActionCopyFiles.keyword",
     documentationUrl = "/workflow/actions/copyfiles.html")
-public class ActionCopyFiles extends ActionBase implements Cloneable, IAction {
+public class ActionCopyFiles extends ActionBase implements ILegacyXml {
   private static final Class<?> PKG = ActionCopyFiles.class;
 
-  public static final String SOURCE_CONFIGURATION_NAME = 
"source_configuration_name";
-  public static final String SOURCE_FILE_FOLDER = "source_filefolder";
-
-  public static final String DESTINATION_CONFIGURATION_NAME = 
"destination_configuration_name";
-  public static final String DESTINATION_FILE_FOLDER = 
"destination_filefolder";
-
-  public static final String LOCAL_SOURCE_FILE = "LOCAL-SOURCE-FILE-";
-  public static final String LOCAL_DEST_FILE = "LOCAL-DEST-FILE-";
-
-  public static final String STATIC_SOURCE_FILE = "STATIC-SOURCE-FILE-";
-  public static final String STATIC_DEST_FILE = "STATIC-DEST-FILE-";
-
+  /** Legacy wizard prefix on source paths (stripped on load, no longer 
written). */
   public static final String DEST_URL = "EMPTY_DEST_URL-";
+
   public static final String SOURCE_URL = "EMPTY_SOURCE_URL-";
 
-  private static final String CONST_SPACE = "          ";
   private static final String CONST_SPACE_SHORT = "      ";
   private static final String CONST_FILE_COPIED = 
"ActionCopyFiles.Log.FileCopied";
   private static final String CONST_COPY_PROCESS = 
"ActionCopyFiles.Error.Exception.CopyProcess";
   private static final String CONST_FILE_EXISTS = 
"ActionCopyFiles.Log.FileExists";
+  private static final String CONST_FILE_EXISTS_SKIP_COPY =
+      "ActionCopyFiles.Log.FileExistsSkippingCopy";
+  private static final String CONST_FOLDER_EXISTS = 
"ActionCopyFiles.Log.FolderExists";
+  private static final String CONST_FOLDER_EXISTS_SKIP_COPY =
+      "ActionCopyFiles.Log.FolderExistsSkippingCopy";
+  private static final String CONST_FILE_OVERWRITE = 
"ActionCopyFiles.Log.FileOverwrite";
+
+  /** Destination parent folder exists or was created successfully. */
+  private static final int DESTINATION_FOLDER_READY = 1;
+
+  /** Destination parent folder is missing and "Create destination folder" is 
off. */
+  private static final int DESTINATION_FOLDER_NOT_AVAILABLE = 0;
 
-  public boolean copyEmptyFolders;
-  public boolean argFromPrevious;
-  public boolean overwriteFiles;
-  public boolean includeSubFolders;
-  public boolean addResultFilenames;
-  public boolean removeSourceFiles;
-  public boolean destinationIsAFile;
-  public boolean createDestinationFolder;
-  public String[] sourceFileFolder;
-  public String[] destinationFileFolder;
-  public String[] wildcard;
+  /** Creating the destination folder failed (already logged). */
+  private static final int DESTINATION_FOLDER_FAILED = -1;
 
+  @Getter(AccessLevel.NONE)
+  @Setter(AccessLevel.NONE)
   private HashSet<String> listFilesRemove = new HashSet<>();
+
+  @Getter(AccessLevel.NONE)
+  @Setter(AccessLevel.NONE)
   private HashSet<String> listAddResult = new HashSet<>();
-  private int nbrFail = 0;
 
-  private Map<String, String> configurationMappings = new HashMap<>();
+  @HopMetadataProperty(key = "copy_empty_folders")
+  private boolean copyEmptyFolders = true;
+
+  @HopMetadataProperty(key = "arg_from_previous")
+  private boolean argFromPrevious;
+
+  @HopMetadataProperty(key = "overwrite_files")
+  private boolean overwriteFiles;
+
+  @HopMetadataProperty(key = "include_subfolders")
+  private boolean includeSubFolders;
+
+  @HopMetadataProperty(key = "remove_source_files")
+  private boolean removeSourceFiles;
+
+  @HopMetadataProperty(key = "add_result_filesname")
+  private boolean addResultFilenames;
+
+  @HopMetadataProperty(key = "destination_is_a_file")
+  private boolean destinationIsAFile;
+
+  @HopMetadataProperty(key = "create_destination_folder")
+  private boolean createDestinationFolder;
+
+  @HopMetadataProperty(groupKey = "fields", key = "field")
+  private List<CopyFilesItem> fileRows = new ArrayList<>();
 
   public ActionCopyFiles(String n) {
     super(n, "");
-    copyEmptyFolders = true;
-    argFromPrevious = false;
-    sourceFileFolder = null;
-    removeSourceFiles = false;
-    destinationFileFolder = null;
-    wildcard = null;
-    overwriteFiles = false;
-    includeSubFolders = false;
-    addResultFilenames = false;
-    destinationIsAFile = false;
-    createDestinationFolder = false;
   }
 
   public ActionCopyFiles() {
     this("");
   }
 
-  public void allocate(int nrFields) {
-    sourceFileFolder = new String[nrFields];
-    destinationFileFolder = new String[nrFields];
-    wildcard = new String[nrFields];
-  }
-
-  @Override
-  public Object clone() {
-    ActionCopyFiles je = (ActionCopyFiles) super.clone();
-    if (sourceFileFolder != null) {
-      int nrFields = sourceFileFolder.length;
-      je.allocate(nrFields);
-      System.arraycopy(sourceFileFolder, 0, je.sourceFileFolder, 0, nrFields);
-      System.arraycopy(destinationFileFolder, 0, je.destinationFileFolder, 0, 
nrFields);
-      System.arraycopy(wildcard, 0, je.wildcard, 0, nrFields);
-    }
-    return je;
-  }
-
-  @Override
-  public String getXml() {
-    StringBuilder xml = new StringBuilder(300);
-
-    xml.append(super.getXml());
-    xml.append(CONST_SPACE_SHORT)
-        .append(XmlHandler.addTagValue("copy_empty_folders", 
copyEmptyFolders));
-    xml.append(CONST_SPACE_SHORT)
-        .append(XmlHandler.addTagValue("arg_from_previous", argFromPrevious));
-    
xml.append(CONST_SPACE_SHORT).append(XmlHandler.addTagValue("overwrite_files", 
overwriteFiles));
-    xml.append(CONST_SPACE_SHORT)
-        .append(XmlHandler.addTagValue("include_subfolders", 
includeSubFolders));
-    xml.append(CONST_SPACE_SHORT)
-        .append(XmlHandler.addTagValue("remove_source_files", 
removeSourceFiles));
-    xml.append(CONST_SPACE_SHORT)
-        .append(XmlHandler.addTagValue("add_result_filesname", 
addResultFilenames));
-    xml.append(CONST_SPACE_SHORT)
-        .append(XmlHandler.addTagValue("destination_is_a_file", 
destinationIsAFile));
-    xml.append(CONST_SPACE_SHORT)
-        .append(XmlHandler.addTagValue("create_destination_folder", 
createDestinationFolder));
-
-    xml.append("      <fields>").append(Const.CR);
-
-    // Get source and destination files, also wildcard
-    if (sourceFileFolder != null) {
-      for (int i = 0; i < sourceFileFolder.length; i++) {
-        xml.append("        <field>").append(Const.CR);
-        saveSource(xml, sourceFileFolder[i]);
-        saveDestination(xml, destinationFileFolder[i]);
-        xml.append(CONST_SPACE).append(XmlHandler.addTagValue("wildcard", 
wildcard[i]));
-        xml.append("        </field>").append(Const.CR);
+  public ActionCopyFiles(ActionCopyFiles other) {
+    super(other.getName(), other.getDescription(), other.getPluginId());
+    this.copyEmptyFolders = other.copyEmptyFolders;
+    this.argFromPrevious = other.argFromPrevious;
+    this.overwriteFiles = other.overwriteFiles;
+    this.includeSubFolders = other.includeSubFolders;
+    this.removeSourceFiles = other.removeSourceFiles;
+    this.addResultFilenames = other.addResultFilenames;
+    this.destinationIsAFile = other.destinationIsAFile;
+    this.createDestinationFolder = other.createDestinationFolder;
+    this.fileRows = new ArrayList<>();
+    if (other.fileRows != null) {
+      for (CopyFilesItem row : other.fileRows) {
+        this.fileRows.add(
+            new CopyFilesItem(
+                row.getSourceFileFolder(), row.getDestinationFileFolder(), 
row.getWildcard()));
       }
     }
-    xml.append("      </fields>").append(Const.CR);
-
-    return xml.toString();
   }
 
   @Override
-  public void loadXml(Node entrynode, IHopMetadataProvider metadataProvider, 
IVariables variables)
-      throws HopXmlException {
-    try {
-      super.loadXml(entrynode);
-      copyEmptyFolders =
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, 
"copy_empty_folders"));
-      argFromPrevious =
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, 
"arg_from_previous"));
-      overwriteFiles = "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, 
"overwrite_files"));
-      includeSubFolders =
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, 
"include_subfolders"));
-      removeSourceFiles =
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, 
"remove_source_files"));
-      addResultFilenames =
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, 
"add_result_filesname"));
-      destinationIsAFile =
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, 
"destination_is_a_file"));
-      createDestinationFolder =
-          "Y".equalsIgnoreCase(XmlHandler.getTagValue(entrynode, 
"create_destination_folder"));
-
-      Node fields = XmlHandler.getSubNode(entrynode, "fields");
-
-      // How many field arguments?
-      int nrFields = XmlHandler.countNodes(fields, "field");
-      allocate(nrFields);
-
-      // Read them all...
-      for (int i = 0; i < nrFields; i++) {
-        Node fnode = XmlHandler.getSubNodeByNr(fields, "field", i);
-        sourceFileFolder[i] = loadSource(fnode);
-        destinationFileFolder[i] = loadDestination(fnode);
-        wildcard[i] = XmlHandler.getTagValue(fnode, "wildcard");
+  public Object clone() {
+    ActionCopyFiles copy = (ActionCopyFiles) super.clone();
+    copy.listFilesRemove = new HashSet<>();
+    copy.listAddResult = new HashSet<>();
+    copy.fileRows = new ArrayList<>();
+    if (fileRows != null) {
+      for (CopyFilesItem row : fileRows) {
+        copy.fileRows.add(
+            new CopyFilesItem(
+                row.getSourceFileFolder(), row.getDestinationFileFolder(), 
row.getWildcard()));
       }
-    } catch (HopXmlException xe) {
-
-      throw new HopXmlException(
-          BaseMessages.getString(PKG, 
"ActionCopyFiles.Error.Exception.UnableLoadXML"), xe);
     }
+    return copy;
   }
 
-  protected String loadSource(Node fnode) {
-    String sourceFileFolder = XmlHandler.getTagValue(fnode, 
SOURCE_FILE_FOLDER);
-    String ncName = XmlHandler.getTagValue(fnode, SOURCE_CONFIGURATION_NAME);
-    return loadURL(sourceFileFolder, ncName, getMetadataProvider(), 
configurationMappings);
+  @Override
+  public boolean equals(Object obj) {
+    return super.equals(obj);
   }
 
-  protected String loadDestination(Node fnode) {
-    String destinationFileFolder = XmlHandler.getTagValue(fnode, 
DESTINATION_FILE_FOLDER);
-    String ncName = XmlHandler.getTagValue(fnode, 
DESTINATION_CONFIGURATION_NAME);
-    return loadURL(destinationFileFolder, ncName, getMetadataProvider(), 
configurationMappings);
+  @Override
+  public int hashCode() {
+    return super.hashCode();
   }
 
-  protected void saveSource(StringBuilder retval, String source) {
-    String namedCluster = configurationMappings.get(source);
-    
retval.append(CONST_SPACE).append(XmlHandler.addTagValue(SOURCE_FILE_FOLDER, 
source));
-    retval
-        .append(CONST_SPACE)
-        .append(XmlHandler.addTagValue(SOURCE_CONFIGURATION_NAME, 
namedCluster));
+  @Override
+  public void convertLegacyXml(Node node) throws HopException {
+    normalizeLegacyVfsPaths();
   }
 
-  protected void saveDestination(StringBuilder retval, String destination) {
-    String namedCluster = configurationMappings.get(destination);
-    
retval.append(CONST_SPACE).append(XmlHandler.addTagValue(DESTINATION_FILE_FOLDER,
 destination));
-    retval
-        .append(CONST_SPACE)
-        .append(XmlHandler.addTagValue(DESTINATION_CONFIGURATION_NAME, 
namedCluster));
+  /**
+   * Strips legacy per-row URL prefixes ({@link #SOURCE_URL}{i}-, {@link 
#DEST_URL}{i}-) left over
+   * from older Hop GUI storage. New saves write plain VFS paths only.
+   */
+  void normalizeLegacyVfsPaths() {
+    if (fileRows == null) {
+      fileRows = new ArrayList<>();
+      return;
+    }
+    List<CopyFilesItem> normalized = new ArrayList<>(fileRows.size());
+    for (int i = 0; i < fileRows.size(); i++) {
+      CopyFilesItem row = fileRows.get(i);
+      normalized.add(
+          new CopyFilesItem(
+              stripLegacyRowPrefix(row.getSourceFileFolder(), SOURCE_URL, i),
+              stripLegacyRowPrefix(row.getDestinationFileFolder(), DEST_URL, 
i),
+              row.getWildcard()));
+    }
+    fileRows = normalized;
   }
 
-  String[] preprocessfilefilder(String[] folders) {
-    List<String> nfolders = new ArrayList<>();
-    if (folders != null) {
-      for (int i = 0; i < folders.length; i++) {
-        nfolders.add(
-            folders[i]
-                .replace(ActionCopyFiles.SOURCE_URL + i + "-", "")
-                .replace(ActionCopyFiles.DEST_URL + i + "-", ""));
-      }
+  private static String stripLegacyRowPrefix(String path, String prefix, int 
index) {
+    if (path == null) {
+      return null;
     }
-    return nfolders.toArray(new String[nfolders.size()]);
+    return path.replace(prefix + index + "-", "");
   }
 
   @Override
@@ -275,123 +226,110 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
     Result result = previousResult;
 
     List<RowMetaAndData> rows = result.getRows();
-    RowMetaAndData resultRow = null;
 
     int nbrFail = 0;
 
-    nbrFail = 0;
-
     if (isBasic()) {
       logBasic(BaseMessages.getString(PKG, "ActionCopyFiles.Log.Starting"));
     }
 
-    try {
-      // Get source and destination files, also wildcard
-      String[] vSourceFileFolder = preprocessfilefilder(sourceFileFolder);
-      String[] vDestinationFileFolder = 
preprocessfilefilder(destinationFileFolder);
-      String[] vwildcard = wildcard;
+    result.setResult(false);
+    result.setNrErrors(1);
 
-      result.setResult(false);
-      result.setNrErrors(1);
+    if (argFromPrevious && isDetailed()) {
+      logDetailed(
+          BaseMessages.getString(
+              PKG,
+              "ActionCopyFiles.Log.ArgFromPrevious.Found",
+              (rows != null ? rows.size() : 0) + ""));
+    }
 
-      if (argFromPrevious && isDetailed()) {
-        logDetailed(
-            BaseMessages.getString(
-                PKG,
-                "ActionCopyFiles.Log.ArgFromPrevious.Found",
-                (rows != null ? rows.size() : 0) + ""));
-      }
+    if (argFromPrevious && rows != null) {
+      for (int iteration = 0; iteration < rows.size() && 
!parentWorkflow.isStopped(); iteration++) {
+        RowMetaAndData resultRow = rows.get(iteration);
 
-      if (argFromPrevious && rows != null) { // Copy the input row to the 
(command line) arguments
-        for (int iteration = 0;
-            iteration < rows.size() && !parentWorkflow.isStopped();
-            iteration++) {
-          resultRow = rows.get(iteration);
-
-          // Get source and destination file names, also wildcard
-          String vSourceFileFolderPrevious = resultRow.getString(0, null);
-          String vDestinationFileFolderPrevious = resultRow.getString(1, null);
-          String vWildcardPrevious = resultRow.getString(2, null);
-
-          if (!Utils.isEmpty(vSourceFileFolderPrevious)
-              && !Utils.isEmpty(vDestinationFileFolderPrevious)) {
-            if (isDetailed()) {
-              logDetailed(
-                  BaseMessages.getString(
-                      PKG,
-                      "ActionCopyFiles.Log.ProcessingRow",
-                      
HopVfs.getFriendlyURI(resolve(vSourceFileFolderPrevious), getVariables()),
-                      HopVfs.getFriendlyURI(
-                          resolve(vDestinationFileFolderPrevious), 
getVariables()),
-                      resolve(vWildcardPrevious)));
-            }
+        String vSourceFileFolderPrevious = resultRow.getString(0, null);
+        String vDestinationFileFolderPrevious = resultRow.getString(1, null);
+        String vWildcardPrevious = resultRow.getString(2, null);
 
-            if (!processFileFolder(
-                vSourceFileFolderPrevious,
-                vDestinationFileFolderPrevious,
-                vWildcardPrevious,
-                parentWorkflow,
-                result)) {
-              // The copy process fail
-              nbrFail++;
-            }
-          } else {
-            if (isDetailed()) {
-              logDetailed(
-                  BaseMessages.getString(
-                      PKG,
-                      "ActionCopyFiles.Log.IgnoringRow",
-                      
HopVfs.getFriendlyURI(resolve(vSourceFileFolder[iteration]), getVariables()),
-                      HopVfs.getFriendlyURI(
-                          resolve(vDestinationFileFolder[iteration]), 
getVariables()),
-                      vwildcard[iteration]));
-            }
+        if (!Utils.isEmpty(vSourceFileFolderPrevious)
+            && !Utils.isEmpty(vDestinationFileFolderPrevious)) {
+          if (isDetailed()) {
+            logDetailed(
+                BaseMessages.getString(
+                    PKG,
+                    "ActionCopyFiles.Log.ProcessingRow",
+                    HopVfs.getFriendlyURI(resolve(vSourceFileFolderPrevious), 
getVariables()),
+                    
HopVfs.getFriendlyURI(resolve(vDestinationFileFolderPrevious), getVariables()),
+                    resolve(vWildcardPrevious)));
           }
-        }
-      } else if (vSourceFileFolder != null && vDestinationFileFolder != null) {
-        for (int i = 0; i < vSourceFileFolder.length && 
!parentWorkflow.isStopped(); i++) {
-          if (!Utils.isEmpty(vSourceFileFolder[i]) && 
!Utils.isEmpty(vDestinationFileFolder[i])) {
-
-            // ok we can process this file/folder
 
-            if (isBasic()) {
-              logBasic(
-                  BaseMessages.getString(
-                      PKG,
-                      "ActionCopyFiles.Log.ProcessingRow",
-                      HopVfs.getFriendlyURI(resolve(vSourceFileFolder[i]), 
getVariables()),
-                      
HopVfs.getFriendlyURI(resolve(vDestinationFileFolder[i]), getVariables()),
-                      resolve(vwildcard[i])));
+          if (!processFileFolder(
+              vSourceFileFolderPrevious,
+              vDestinationFileFolderPrevious,
+              vWildcardPrevious,
+              parentWorkflow,
+              result)) {
+            nbrFail++;
+          }
+        } else {
+          if (isDetailed()) {
+            String srcHint = "";
+            String destHint = "";
+            String wildHint = "";
+            if (fileRows != null && iteration < fileRows.size()) {
+              CopyFilesItem item = fileRows.get(iteration);
+              srcHint =
+                  item.getSourceFileFolder() != null
+                      ? 
HopVfs.getFriendlyURI(resolve(item.getSourceFileFolder()), getVariables())
+                      : "";
+              destHint =
+                  item.getDestinationFileFolder() != null
+                      ? HopVfs.getFriendlyURI(
+                          resolve(item.getDestinationFileFolder()), 
getVariables())
+                      : "";
+              wildHint = item.getWildcard() != null ? item.getWildcard() : "";
             }
+            logDetailed(
+                BaseMessages.getString(
+                    PKG, "ActionCopyFiles.Log.IgnoringRow", srcHint, destHint, 
wildHint));
+          }
+        }
+      }
+    } else if (fileRows != null) {
+      for (int i = 0; i < fileRows.size() && !parentWorkflow.isStopped(); i++) 
{
+        CopyFilesItem row = fileRows.get(i);
+        String src = row.getSourceFileFolder();
+        String dst = row.getDestinationFileFolder();
+        String wild = row.getWildcard();
+        if (!Utils.isEmpty(src) && !Utils.isEmpty(dst)) {
+          if (isBasic()) {
+            logBasic(
+                BaseMessages.getString(
+                    PKG,
+                    "ActionCopyFiles.Log.ProcessingRow",
+                    HopVfs.getFriendlyURI(resolve(src), getVariables()),
+                    HopVfs.getFriendlyURI(resolve(dst), getVariables()),
+                    resolve(wild)));
+          }
 
-            if (!processFileFolder(
-                vSourceFileFolder[i],
-                vDestinationFileFolder[i],
-                vwildcard[i],
-                parentWorkflow,
-                result)) {
-              // The copy process fail
-              nbrFail++;
-            }
-          } else {
-            if (isDetailed()) {
-              logDetailed(
-                  BaseMessages.getString(
-                      PKG,
-                      "ActionCopyFiles.Log.IgnoringRow",
-                      HopVfs.getFriendlyURI(resolve(vSourceFileFolder[i]), 
getVariables()),
-                      
HopVfs.getFriendlyURI(resolve(vDestinationFileFolder[i]), getVariables()),
-                      vwildcard[i]));
-            }
+          if (!processFileFolder(src, dst, wild, parentWorkflow, result)) {
+            nbrFail++;
+          }
+        } else {
+          if (isDetailed()) {
+            logDetailed(
+                BaseMessages.getString(
+                    PKG,
+                    "ActionCopyFiles.Log.IgnoringRow",
+                    HopVfs.getFriendlyURI(resolve(Const.NVL(src, "")), 
getVariables()),
+                    HopVfs.getFriendlyURI(resolve(Const.NVL(dst, "")), 
getVariables()),
+                    Const.NVL(wild, "")));
           }
         }
       }
-    } finally {
-      listAddResult = null;
-      listFilesRemove = null;
     }
 
-    // Check if all files was process with success
     if (nbrFail == 0) {
       result.setResult(true);
       result.setNrErrors(0);
@@ -399,6 +337,16 @@ public class ActionCopyFiles extends ActionBase implements 
Cloneable, IAction {
       result.setNrErrors(nbrFail);
     }
 
+    if (isBasic()) {
+      if (nbrFail == 0) {
+        logBasic(BaseMessages.getString(PKG, 
"ActionCopyFiles.Log.FinishedSuccess"));
+      } else {
+        logBasic(
+            BaseMessages.getString(
+                PKG, "ActionCopyFiles.Log.FinishedWithErrors", 
Integer.toString(nbrFail)));
+      }
+    }
+
     return result;
   }
 
@@ -426,12 +374,23 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
       sourceFileFolder = HopVfs.getFileObject(realSourceFileFolderName, 
getVariables());
       destinationFileFolder = 
HopVfs.getFileObject(realDestinationFileFolderName, getVariables());
 
+      if (isDebug()) {
+        logDebug(
+            BaseMessages.getString(
+                PKG,
+                "ActionCopyFiles.Log.Debug.ResolvedPaths",
+                HopVfs.getFriendlyURI(realSourceFileFolderName, 
getVariables()),
+                HopVfs.getFriendlyURI(realDestinationFileFolderName, 
getVariables()),
+                Const.NVL(realWildcard, "")));
+      }
+
       if (sourceFileFolder.exists()) {
 
         // Check if destination folder/parent folder exists !
         // If user wanted and if destination folder does not exist
         // Apache Hop will create it
-        if (createDestinationFolder(destinationFileFolder)) {
+        int destinationStatus = ensureDestinationFolder(destinationFileFolder);
+        if (destinationStatus == DESTINATION_FOLDER_READY) {
 
           // Basic Tests
           if (sourceFileFolder.getType().equals(FileType.FOLDER) && 
destinationIsAFile) {
@@ -445,8 +404,6 @@ public class ActionCopyFiles extends ActionBase implements 
Cloneable, IAction {
                     HopVfs.getFriendlyURI(realSourceFileFolderName, 
getVariables()),
                     HopVfs.getFriendlyURI(realDestinationFileFolderName, 
getVariables())));
 
-            nbrFail++;
-
           } else {
 
             if (destinationFileFolder.getType().equals(FileType.FOLDER)
@@ -454,13 +411,10 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
               // Source is a file, destination is a folder
               // Copy the file to the destination folder
               //
-              NameFileFilter nameFileFilter =
-                  new NameFileFilter(
-                      
Collections.singletonList(sourceFileFolder.getName().getBaseName()));
-              FileSelector fileSelector = new 
FileFilterSelector(nameFileFilter);
-              destinationFileFolder.copyFrom(sourceFileFolder.getParent(), 
fileSelector);
-
-              trackBytesCopied(sourceFileFolder, result);
+              FileObject destChild =
+                  destinationFileFolder.resolveFile(
+                      sourceFileFolder.getName().getBaseName(), 
NameScope.CHILD);
+              copyFileWithByteTracking(sourceFileFolder, destChild, result);
 
               if (isDetailed()) {
                 logDetailed(
@@ -472,26 +426,25 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
               }
 
             } else if (sourceFileFolder.getType().equals(FileType.FILE) && 
destinationIsAFile) {
-              // Source is a file, destination is a file
-
-              destinationFileFolder.copyFrom(
-                  sourceFileFolder, new 
TextOneToOneFileSelector(destinationFileFolder));
-
-              trackBytesCopied(sourceFileFolder, result);
+              // Source is a file, destination is a file (stream copy + byte 
metrics; no copyFrom)
+              copyOneFileToFile(sourceFileFolder, destinationFileFolder, 
result);
             } else {
               // Both source and destination are folders
               if (isDetailed()) {
-                logDetailed("  ");
                 logDetailed(
                     BaseMessages.getString(
                         PKG,
-                        "ActionCopyFiles.Log.FetchFolder",
+                        "ActionCopyFiles.Log.ScanningSourceFolder",
                         HopVfs.getFriendlyURI(sourceFileFolder)));
               }
 
               TextFileSelector textFileSelector =
                   new TextFileSelector(
-                      sourceFileFolder, destinationFileFolder, realWildcard, 
parentWorkflow);
+                      sourceFileFolder,
+                      destinationFileFolder,
+                      realWildcard,
+                      parentWorkflow,
+                      result);
               try {
                 destinationFileFolder.copyFrom(sourceFileFolder, 
textFileSelector);
               } finally {
@@ -499,20 +452,6 @@ public class ActionCopyFiles extends ActionBase implements 
Cloneable, IAction {
               }
             }
 
-            // Track bytes for folder-to-folder copies via listAddResult
-            for (String copiedFile : listAddResult) {
-              try {
-                FileObject fo = HopVfs.getFileObject(copiedFile, 
getVariables());
-                if (fo.getType() == FileType.FILE && 
fo.getType().hasContent()) {
-                  long size = fo.getContent().getSize();
-                  
result.setBytesReadThisAction(result.getBytesReadThisAction() + size);
-                  
result.setBytesWrittenThisAction(result.getBytesWrittenThisAction() + size);
-                }
-              } catch (Exception e) {
-                logDebug("Could not get size of copied file: " + copiedFile);
-              }
-            }
-
             // Remove Files if needed
             if (removeSourceFiles && !listFilesRemove.isEmpty()) {
               String sourceFilefoldername = sourceFileFolder.toString();
@@ -601,10 +540,10 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
                 }
               }
             }
+
+            entrystatus = true;
           }
-          entrystatus = true;
-        } else {
-          // Destination Folder or Parent folder is missing
+        } else if (destinationStatus == DESTINATION_FOLDER_NOT_AVAILABLE) {
           logError(
               BaseMessages.getString(
                   PKG,
@@ -656,128 +595,151 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
     return entrystatus;
   }
 
-  private void trackBytesCopied(FileObject sourceFile, Result result) {
-    try {
-      if (sourceFile.getType().hasContent()) {
-        long size = sourceFile.getContent().getSize();
-        result.setBytesReadThisAction(result.getBytesReadThisAction() + size);
-        result.setBytesWrittenThisAction(result.getBytesWrittenThisAction() + 
size);
-      }
-    } catch (Exception e) {
-      logDebug("Could not get size of source file: " + sourceFile);
-    }
-  }
-
-  private class TextOneToOneFileSelector implements FileSelector {
-    FileObject destfile = null;
+  private static final int COPY_BUFFER_SIZE = 8192;
 
-    public TextOneToOneFileSelector(FileObject destinationfile) {
-
-      if (destinationfile != null) {
-        destfile = destinationfile;
+  /**
+   * Copies file content through {@link CountingInputStream} / {@link 
CountingOutputStream} so
+   * {@link Result#getBytesReadThisAction()} and {@link 
Result#getBytesWrittenThisAction()} match
+   * bytes moved over the streams (when {@link Const#HOP_METRIC_DATA_VOLUME} 
is enabled).
+   */
+  private void copyFileWithByteTracking(FileObject source, FileObject 
destination, Result result)
+      throws IOException {
+    FileObject parent = destination.getParent();
+    if (parent != null && !parent.exists()) {
+      parent.createFolder();
+    }
+    long read;
+    long written;
+    try (InputStream rawIn = HopVfs.getInputStream(source);
+        CountingInputStream in = new CountingInputStream(rawIn);
+        OutputStream rawOut = HopVfs.getOutputStream(destination, false);
+        CountingOutputStream out = new CountingOutputStream(rawOut)) {
+      byte[] buffer = new byte[COPY_BUFFER_SIZE];
+      int len;
+      while ((len = in.read(buffer)) != -1) {
+        if (len > 0) {
+          out.write(buffer, 0, len);
+        }
       }
+      out.flush();
+      read = in.getCount();
+      written = out.getCount();
     }
+    result.setBytesReadThisAction(result.getBytesReadThisAction() + read);
+    result.setBytesWrittenThisAction(result.getBytesWrittenThisAction() + 
written);
+  }
 
-    @Override
-    public boolean includeFile(FileSelectInfo info) {
-      boolean resultat = false;
-      String filename = null;
-
-      try {
-        // check if the destination file exists
-
-        if (destfile.exists()) {
-          if (isDetailed()) {
-            logDetailed(
-                CONST_SPACE_SHORT
-                    + BaseMessages.getString(
-                        PKG, CONST_FILE_EXISTS, 
HopVfs.getFriendlyURI(destfile)));
-          }
-
-          if (overwriteFiles) {
-            if (isDetailed()) {
-              logDetailed(
-                  CONST_SPACE_SHORT
-                      + BaseMessages.getString(
-                          PKG,
-                          "ActionCopyFiles.Log.FileOverwrite",
-                          HopVfs.getFriendlyURI(destfile)));
-            }
-
-            resultat = true;
-          }
-        } else {
+  /**
+   * File-to-file copy: same rules as the former one-to-one {@link 
FileSelector} (exists / overwrite
+   * / logging), then stream copy for byte metrics. Does not use {@code 
copyFrom}.
+   *
+   * @return true if the file was copied, false if skipped or on error
+   */
+  private boolean copyOneFileToFile(FileObject sourceFile, FileObject 
destFile, Result result) {
+    boolean shouldCopy = false;
+    try {
+      if (destFile.exists()) {
+        if (isDetailed()) {
+          logDetailed(
+              CONST_SPACE_SHORT
+                  + BaseMessages.getString(
+                      PKG,
+                      overwriteFiles ? CONST_FILE_EXISTS : 
CONST_FILE_EXISTS_SKIP_COPY,
+                      HopVfs.getFriendlyURI(destFile)));
+        }
+        if (overwriteFiles) {
           if (isDetailed()) {
             logDetailed(
                 CONST_SPACE_SHORT
                     + BaseMessages.getString(
                         PKG,
-                        CONST_FILE_COPIED,
-                        HopVfs.getFriendlyURI(info.getFile()),
-                        HopVfs.getFriendlyURI(destfile)));
+                        CONST_FILE_OVERWRITE,
+                        HopVfs.getFriendlyURI(sourceFile),
+                        HopVfs.getFriendlyURI(destFile)));
           }
-
-          resultat = true;
+          shouldCopy = true;
         }
-
-        if (resultat && removeSourceFiles) {
-          // add this folder/file to remove files
-          // This list will be fetched and all entries files
-          // will be removed
-          listFilesRemove.add(info.getFile().toString());
+      } else {
+        if (isDetailed()) {
+          logDetailed(
+              CONST_SPACE_SHORT
+                  + BaseMessages.getString(
+                      PKG,
+                      CONST_FILE_COPIED,
+                      HopVfs.getFriendlyURI(sourceFile),
+                      HopVfs.getFriendlyURI(destFile)));
         }
+        shouldCopy = true;
+      }
 
-        if (resultat && addResultFilenames) {
-          // add this folder/file to result files name
-          listAddResult.add(destfile.toString());
-        }
+      if (!shouldCopy) {
+        return false;
+      }
 
-      } catch (Exception e) {
+      copyFileWithByteTracking(sourceFile, destFile, result);
 
-        logError(
-            BaseMessages.getString(
-                PKG,
-                CONST_COPY_PROCESS,
-                HopVfs.getFriendlyURI(info.getFile()),
-                filename,
-                e.getMessage()));
+      if (removeSourceFiles) {
+        listFilesRemove.add(sourceFile.toString());
       }
-
-      return resultat;
+      if (addResultFilenames) {
+        listAddResult.add(destFile.toString());
+      }
+      return true;
+    } catch (Exception e) {
+      logError(
+          BaseMessages.getString(
+              PKG,
+              CONST_COPY_PROCESS,
+              HopVfs.getFriendlyURI(sourceFile),
+              HopVfs.getFriendlyURI(destFile),
+              e.getMessage()));
+      return false;
     }
+  }
 
-    @Override
-    public boolean traverseDescendents(FileSelectInfo info) {
-      return false;
+  /**
+   * Ensures the destination folder (or the parent when destination is a file) 
exists or can be
+   * created.
+   *
+   * @return {@link #DESTINATION_FOLDER_READY} if ready, {@link 
#DESTINATION_FOLDER_NOT_AVAILABLE}
+   *     if missing and create is disabled, or {@link 
#DESTINATION_FOLDER_FAILED} on error (already
+   *     logged)
+   */
+  private void createMissingDestinationParent(FileObject folder) throws 
FileSystemException {
+    if (isDetailed()) {
+      logDetailed(
+          BaseMessages.getString(
+              PKG, "ActionCopyFiles.Log.FolderParentWillCreate", 
HopVfs.getFriendlyURI(folder)));
+    }
+    folder.createFolder();
+    if (isDetailed()) {
+      logDetailed(
+          BaseMessages.getString(
+              PKG, "ActionCopyFiles.Log.FolderParentCreated", 
HopVfs.getFriendlyURI(folder)));
     }
   }
 
-  private boolean createDestinationFolder(FileObject filefolder) {
+  private int ensureDestinationFolder(FileObject filefolder) {
     FileObject folder = null;
     try {
-      if (destinationIsAFile) {
-        folder = filefolder.getParent();
-      } else {
-        folder = filefolder;
-      }
+      folder = destinationIsAFile ? filefolder.getParent() : filefolder;
 
       if (!folder.exists()) {
         if (createDestinationFolder) {
-          if (isDetailed()) {
-            logDetailed("Folder  " + HopVfs.getFriendlyURI(folder) + " does 
not exist !");
-          }
-          folder.createFolder();
-          if (isDetailed()) {
-            logDetailed("Folder parent was created.");
-          }
+          createMissingDestinationParent(folder);
         } else {
-          logError("Folder  " + HopVfs.getFriendlyURI(folder) + " does not 
exist !");
-          return false;
+          return DESTINATION_FOLDER_NOT_AVAILABLE;
         }
       }
-      return true;
+      return DESTINATION_FOLDER_READY;
     } catch (Exception e) {
-      logError("Couldn't created parent folder " + 
HopVfs.getFriendlyURI(folder), e);
+      logError(
+          BaseMessages.getString(
+              PKG,
+              "ActionCopyFiles.Error.FolderParentCreateFailed",
+              folder != null ? HopVfs.getFriendlyURI(folder) : 
HopVfs.getFriendlyURI(filefolder)),
+          e);
+      return DESTINATION_FOLDER_FAILED;
     } finally {
       if (folder != null) {
         try {
@@ -788,7 +750,6 @@ public class ActionCopyFiles extends ActionBase implements 
Cloneable, IAction {
         }
       }
     }
-    return false;
   }
 
   private class TextFileSelector implements FileSelector {
@@ -802,6 +763,8 @@ public class ActionCopyFiles extends ActionBase implements 
Cloneable, IAction {
     // Store connection to destination source for improved performance to 
remote hosts
     FileObject destinationFolderObject = null;
 
+    private final Result copyResult;
+
     /**
      * @param selectedfile
      * @return True if the selectedfile matches the wildcard
@@ -820,8 +783,10 @@ public class ActionCopyFiles extends ActionBase implements 
Cloneable, IAction {
         FileObject sourcefolderin,
         FileObject destinationfolderin,
         String filewildcard,
-        IWorkflowEngine<WorkflowMeta> parentWorkflow) {
+        IWorkflowEngine<WorkflowMeta> parentWorkflow,
+        Result result) {
 
+      this.copyResult = result;
       if (sourcefolderin != null) {
         sourceFolder = sourcefolderin.toString();
       }
@@ -839,6 +804,7 @@ public class ActionCopyFiles extends ActionBase implements 
Cloneable, IAction {
     @Override
     public boolean includeFile(FileSelectInfo info) {
       boolean returncode = false;
+      boolean streamCopied = false;
       FileObject filename = null;
       String addFileNameString = null;
       try {
@@ -882,7 +848,9 @@ public class ActionCopyFiles extends ActionBase implements 
Cloneable, IAction {
                           CONST_SPACE_SHORT
                               + BaseMessages.getString(
                                   PKG,
-                                  "ActionCopyFiles.Log.FolderExists",
+                                  overwriteFiles
+                                      ? CONST_FOLDER_EXISTS
+                                      : CONST_FOLDER_EXISTS_SKIP_COPY,
                                   HopVfs.getFriendlyURI(filename)));
                     }
                     if (overwriteFiles) {
@@ -919,15 +887,17 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
                       logDetailed(
                           CONST_SPACE_SHORT
                               + BaseMessages.getString(
-                                  PKG, CONST_FILE_EXISTS, 
HopVfs.getFriendlyURI(filename)));
+                                  PKG,
+                                  overwriteFiles ? CONST_FILE_EXISTS : 
CONST_FILE_EXISTS_SKIP_COPY,
+                                  HopVfs.getFriendlyURI(filename)));
                     }
                     if (overwriteFiles) {
                       if (isDetailed()) {
                         logDetailed(
-                            "       "
+                            CONST_SPACE_SHORT
                                 + BaseMessages.getString(
                                     PKG,
-                                    CONST_FILE_EXISTS,
+                                    CONST_FILE_OVERWRITE,
                                     HopVfs.getFriendlyURI(info.getFile()),
                                     HopVfs.getFriendlyURI(filename)));
                       }
@@ -961,7 +931,9 @@ public class ActionCopyFiles extends ActionBase implements 
Cloneable, IAction {
                         CONST_SPACE_SHORT
                             + BaseMessages.getString(
                                 PKG,
-                                "ActionCopyFiles.Log.FolderExists",
+                                overwriteFiles
+                                    ? CONST_FOLDER_EXISTS
+                                    : CONST_FOLDER_EXISTS_SKIP_COPY,
                                 HopVfs.getFriendlyURI(filename)));
                   }
                   if (overwriteFiles) {
@@ -1003,19 +975,20 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
                     logDetailed(
                         CONST_SPACE_SHORT
                             + BaseMessages.getString(
-                                PKG, CONST_FILE_EXISTS, 
HopVfs.getFriendlyURI(filename)));
+                                PKG,
+                                overwriteFiles ? CONST_FILE_EXISTS : 
CONST_FILE_EXISTS_SKIP_COPY,
+                                HopVfs.getFriendlyURI(filename)));
                   }
 
                   if (overwriteFiles) {
                     if (isDetailed()) {
                       logDetailed(
                           CONST_SPACE_SHORT
-                              + BaseMessages.getString(PKG, 
"ActionCopyFiles.Log.FileExistsInfos"),
-                          BaseMessages.getString(
-                              PKG,
-                              CONST_FILE_EXISTS,
-                              HopVfs.getFriendlyURI(info.getFile()),
-                              HopVfs.getFriendlyURI(filename)));
+                              + BaseMessages.getString(
+                                  PKG,
+                                  CONST_FILE_OVERWRITE,
+                                  HopVfs.getFriendlyURI(info.getFile()),
+                                  HopVfs.getFriendlyURI(filename)));
                     }
 
                     returncode = true;
@@ -1025,6 +998,11 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
             }
           }
         }
+
+        if (returncode && filename != null && info.getFile().getType() == 
FileType.FILE) {
+          copyFileWithByteTracking(info.getFile(), filename, copyResult);
+          streamCopied = true;
+        }
       } catch (Exception e) {
 
         logError(
@@ -1062,7 +1040,9 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
             addFileNameString); // was a NPE before with the file_name=null 
above in the finally
       }
 
-      return returncode;
+      // File rows: we already copied bytes above when streamCopied; true 
would make copyFrom copy
+      // again. Folder rows: unchanged — still return returncode so VFS 
creates empty dirs.
+      return streamCopied ? false : returncode;
     }
 
     @Override
@@ -1082,109 +1062,6 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
     }
   }
 
-  private class TextOneFileSelector implements FileSelector {
-    String filename = null;
-    String folderName = null;
-    String destfolder = null;
-    private int traverseCount;
-
-    public TextOneFileSelector(
-        String sourcefolderin, String sourcefilenamein, String destfolderin) {
-      if (!Utils.isEmpty(sourcefilenamein)) {
-        filename = sourcefilenamein;
-      }
-
-      if (!Utils.isEmpty(sourcefolderin)) {
-        folderName = sourcefolderin;
-      }
-      if (!Utils.isEmpty(destfolderin)) {
-        destfolder = destfolderin;
-      }
-    }
-
-    @Override
-    public boolean includeFile(FileSelectInfo info) {
-      boolean resultat = false;
-      String filename = null;
-
-      try {
-        if (info.getFile().getType() == FileType.FILE) {
-          if (info.getFile().getName().getBaseName().equals(filename)
-              && (info.getFile().getParent().toString().equals(folderName))) {
-            // check if the file exists
-            filename = destfolder + Const.FILE_SEPARATOR + filename;
-
-            if (HopVfs.getFileObject(filename, getVariables()).exists()) {
-              if (isDetailed()) {
-                logDetailed(
-                    CONST_SPACE_SHORT
-                        + BaseMessages.getString(
-                            PKG,
-                            CONST_FILE_EXISTS,
-                            HopVfs.getFriendlyURI(filename, getVariables())));
-              }
-
-              if (overwriteFiles) {
-                if (isDetailed()) {
-                  logDetailed(
-                      CONST_SPACE_SHORT
-                          + BaseMessages.getString(
-                              PKG,
-                              "ActionCopyFiles.Log.FileOverwrite",
-                              HopVfs.getFriendlyURI(info.getFile()),
-                              HopVfs.getFriendlyURI(filename, 
getVariables())));
-                }
-
-                resultat = true;
-              }
-            } else {
-              if (isDetailed()) {
-                logDetailed(
-                    CONST_SPACE_SHORT
-                        + BaseMessages.getString(
-                            PKG,
-                            CONST_FILE_COPIED,
-                            HopVfs.getFriendlyURI(info.getFile()),
-                            HopVfs.getFriendlyURI(filename, getVariables())));
-              }
-
-              resultat = true;
-            }
-          }
-
-          if (resultat && removeSourceFiles) {
-            // add this folder/file to remove files
-            // This list will be fetched and all entries files
-            // will be removed
-            listFilesRemove.add(info.getFile().toString());
-          }
-
-          if (resultat && addResultFilenames) {
-            // add this folder/file to result files name
-            listAddResult.add(HopVfs.getFileObject(filename, 
getVariables()).toString());
-          }
-        }
-      } catch (Exception e) {
-        logError(
-            BaseMessages.getString(
-                PKG,
-                CONST_COPY_PROCESS,
-                HopVfs.getFriendlyURI(info.getFile()),
-                HopVfs.getFriendlyURI(filename, getVariables()),
-                e.getMessage()));
-
-        resultat = false;
-      }
-
-      return resultat;
-    }
-
-    @Override
-    public boolean traverseDescendents(FileSelectInfo info) {
-      return (traverseCount++ == 0 || includeSubFolders);
-    }
-  }
-
   @Override
   public void check(
       List<ICheckResult> remarks,
@@ -1195,7 +1072,7 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
         ActionValidatorUtils.andValidator()
             .validate(
                 this,
-                "arguments",
+                "fileRows",
                 remarks,
                 
AndValidator.putValidators(ActionValidatorUtils.notNullValidator()));
 
@@ -1208,8 +1085,11 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
     AndValidator.putValidators(
         ctx, ActionValidatorUtils.notNullValidator(), 
ActionValidatorUtils.fileExistsValidator());
 
-    for (int i = 0; i < sourceFileFolder.length; i++) {
-      ActionValidatorUtils.andValidator().validate(this, "arguments[" + i + 
"]", remarks, ctx);
+    if (fileRows != null) {
+      for (int i = 0; i < fileRows.size(); i++) {
+        ActionValidatorUtils.andValidator()
+            .validate(this, "fileRows[" + i + "].sourceFileFolder", remarks, 
ctx);
+      }
     }
   }
 
@@ -1217,269 +1097,4 @@ public class ActionCopyFiles extends ActionBase 
implements Cloneable, IAction {
   public boolean isEvaluation() {
     return true;
   }
-
-  public String loadURL(
-      String url,
-      String ncName,
-      IHopMetadataProvider metadataProvider,
-      Map<String, String> mappings) {
-    if (!Utils.isEmpty(ncName) && !Utils.isEmpty(url)) {
-      mappings.put(url, ncName);
-    }
-    return url;
-  }
-
-  public void setConfigurationMappings(Map<String, String> mappings) {
-    this.configurationMappings = mappings;
-  }
-
-  public String getConfigurationBy(String url) {
-    return this.configurationMappings.get(url);
-  }
-
-  public String getUrlPath(String incomingURL) {
-    String path = null;
-    try {
-      String noVariablesURL = incomingURL.replaceAll("[${}]", "/");
-      FileName fileName = 
HopVfs.getFileSystemManager(getVariables()).resolveURI(noVariablesURL);
-      String root = fileName.getRootURI();
-      path = incomingURL.substring(root.length() - 1);
-    } catch (FileSystemException e) {
-      path = null;
-    }
-    return path;
-  }
-
-  /**
-   * Gets copyEmptyFolders
-   *
-   * @return value of copyEmptyFolders
-   */
-  public boolean isCopyEmptyFolders() {
-    return copyEmptyFolders;
-  }
-
-  /**
-   * @param copyEmptyFolders The copyEmptyFolders to set
-   */
-  public void setCopyEmptyFolders(boolean copyEmptyFolders) {
-    this.copyEmptyFolders = copyEmptyFolders;
-  }
-
-  /**
-   * Gets argFromPrevious
-   *
-   * @return value of argFromPrevious
-   */
-  public boolean isArgFromPrevious() {
-    return argFromPrevious;
-  }
-
-  /**
-   * @param argFromPrevious The argFromPrevious to set
-   */
-  public void setArgFromPrevious(boolean argFromPrevious) {
-    this.argFromPrevious = argFromPrevious;
-  }
-
-  /**
-   * Gets overwriteFiles
-   *
-   * @return value of overwriteFiles
-   */
-  public boolean isOverwriteFiles() {
-    return overwriteFiles;
-  }
-
-  /**
-   * @param overwriteFiles The overwriteFiles to set
-   */
-  public void setOverwriteFiles(boolean overwriteFiles) {
-    this.overwriteFiles = overwriteFiles;
-  }
-
-  /**
-   * Gets includeSubFolders
-   *
-   * @return value of includeSubFolders
-   */
-  public boolean isIncludeSubFolders() {
-    return includeSubFolders;
-  }
-
-  /**
-   * @param includeSubFolders The includeSubFolders to set
-   */
-  public void setIncludeSubFolders(boolean includeSubFolders) {
-    this.includeSubFolders = includeSubFolders;
-  }
-
-  /**
-   * Gets addResultFilenames
-   *
-   * @return value of addResultFilenames
-   */
-  public boolean isAddResultFilenames() {
-    return addResultFilenames;
-  }
-
-  /**
-   * @param addResultFilenames The addResultFilenames to set
-   */
-  public void setAddResultFilenames(boolean addResultFilenames) {
-    this.addResultFilenames = addResultFilenames;
-  }
-
-  /**
-   * Gets removeSourceFiles
-   *
-   * @return value of removeSourceFiles
-   */
-  public boolean isRemoveSourceFiles() {
-    return removeSourceFiles;
-  }
-
-  /**
-   * @param removeSourceFiles The removeSourceFiles to set
-   */
-  public void setRemoveSourceFiles(boolean removeSourceFiles) {
-    this.removeSourceFiles = removeSourceFiles;
-  }
-
-  /**
-   * Gets destinationIsAFile
-   *
-   * @return value of destinationIsAFile
-   */
-  public boolean isDestinationIsAFile() {
-    return destinationIsAFile;
-  }
-
-  /**
-   * @param destinationIsAFile The destinationIsAFile to set
-   */
-  public void setDestinationIsAFile(boolean destinationIsAFile) {
-    this.destinationIsAFile = destinationIsAFile;
-  }
-
-  /**
-   * Gets createDestinationFolder
-   *
-   * @return value of createDestinationFolder
-   */
-  public boolean isCreateDestinationFolder() {
-    return createDestinationFolder;
-  }
-
-  /**
-   * @param createDestinationFolder The createDestinationFolder to set
-   */
-  public void setCreateDestinationFolder(boolean createDestinationFolder) {
-    this.createDestinationFolder = createDestinationFolder;
-  }
-
-  /**
-   * Gets sourceFileFolder
-   *
-   * @return value of sourceFileFolder
-   */
-  public String[] getSourceFileFolder() {
-    return sourceFileFolder;
-  }
-
-  /**
-   * @param sourceFileFolder The sourceFileFolder to set
-   */
-  public void setSourceFileFolder(String[] sourceFileFolder) {
-    this.sourceFileFolder = sourceFileFolder;
-  }
-
-  /**
-   * Gets destinationFileFolder
-   *
-   * @return value of destinationFileFolder
-   */
-  public String[] getDestinationFileFolder() {
-    return destinationFileFolder;
-  }
-
-  /**
-   * @param destinationFileFolder The destinationFileFolder to set
-   */
-  public void setDestinationFileFolder(String[] destinationFileFolder) {
-    this.destinationFileFolder = destinationFileFolder;
-  }
-
-  /**
-   * Gets wildcard
-   *
-   * @return value of wildcard
-   */
-  public String[] getWildcard() {
-    return wildcard;
-  }
-
-  /**
-   * @param wildcard The wildcard to set
-   */
-  public void setWildcard(String[] wildcard) {
-    this.wildcard = wildcard;
-  }
-
-  /**
-   * Gets listFilesRemove
-   *
-   * @return value of listFilesRemove
-   */
-  public HashSet<String> getListFilesRemove() {
-    return listFilesRemove;
-  }
-
-  /**
-   * @param listFilesRemove The listFilesRemove to set
-   */
-  public void setListFilesRemove(HashSet<String> listFilesRemove) {
-    this.listFilesRemove = listFilesRemove;
-  }
-
-  /**
-   * Gets listAddResult
-   *
-   * @return value of listAddResult
-   */
-  public HashSet<String> getListAddResult() {
-    return listAddResult;
-  }
-
-  /**
-   * @param listAddResult The listAddResult to set
-   */
-  public void setListAddResult(HashSet<String> listAddResult) {
-    this.listAddResult = listAddResult;
-  }
-
-  /**
-   * Gets nbrFail
-   *
-   * @return value of nbrFail
-   */
-  public int getNbrFail() {
-    return nbrFail;
-  }
-
-  /**
-   * @param nbrFail The nbrFail to set
-   */
-  public void setNbrFail(int nbrFail) {
-    this.nbrFail = nbrFail;
-  }
-
-  /**
-   * Gets configurationMappings
-   *
-   * @return value of configurationMappings
-   */
-  public Map<String, String> getConfigurationMappings() {
-    return configurationMappings;
-  }
 }
diff --git 
a/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/ActionCopyFilesDialog.java
 
b/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/ActionCopyFilesDialog.java
index c735d03975..9d60bfe1a6 100644
--- 
a/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/ActionCopyFilesDialog.java
+++ 
b/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/ActionCopyFilesDialog.java
@@ -17,8 +17,6 @@
 
 package org.apache.hop.workflow.actions.copyfiles;
 
-import java.util.HashMap;
-import java.util.Map;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.Props;
 import org.apache.hop.core.util.Utils;
@@ -29,7 +27,6 @@ import org.apache.hop.ui.core.dialog.BaseDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.widget.ColumnInfo;
-import org.apache.hop.ui.core.widget.ITextVarButtonRenderCallback;
 import org.apache.hop.ui.core.widget.TableView;
 import org.apache.hop.ui.workflow.action.ActionDialog;
 import org.apache.hop.workflow.WorkflowMeta;
@@ -57,9 +54,6 @@ public class ActionCopyFilesDialog extends ActionDialog {
   protected static final String[] FILETYPES =
       new String[] {BaseMessages.getString(PKG, 
"ActionCopyFiles.Filetype.All")};
 
-  public static final String LOCAL_ENVIRONMENT = "Local";
-  public static final String STATIC_ENVIRONMENT = "<Static>";
-
   protected Button wPrevious;
   protected Button wCopyEmptyFolders;
   protected Button wOverwriteFiles;
@@ -96,9 +90,6 @@ public class ActionCopyFilesDialog extends ActionDialog {
     ModifyListener lsMod = e -> action.setChanged();
     changed = action.hasChanged();
 
-    int middle = this.middle;
-    int margin = this.margin;
-
     wTabFolder = new CTabFolder(shell, SWT.BORDER);
     PropsUi.setLook(wTabFolder, Props.WIDGET_STYLE_TAB);
 
@@ -109,10 +100,6 @@ public class ActionCopyFilesDialog extends ActionDialog {
     fdTabFolder.bottom = new FormAttachment(wCancel, -margin);
     wTabFolder.setLayoutData(fdTabFolder);
 
-    // ///////////////////////////////////////////////////////////
-    // / START OF FILES TAB
-    // ///////////////////////////////////////////////////////////
-
     CTabItem wFilesTab = new CTabItem(wTabFolder, SWT.NONE);
     wFilesTab.setFont(GuiResource.getInstance().getFontDefault());
     wFilesTab.setText(BaseMessages.getString(PKG, 
"ActionCopyFiles.Tab.Files.Label"));
@@ -135,14 +122,6 @@ public class ActionCopyFilesDialog extends ActionDialog {
     wFilesComp.layout();
     wFilesTab.setControl(wFilesComp);
 
-    // ///////////////////////////////////////////////////////////
-    // / END OF FILES TAB
-    // ///////////////////////////////////////////////////////////
-
-    // ////////////////////////
-    // START OF SETTINGS TAB ///
-    // ////////////////////////
-
     CTabItem wSettingsTab = new CTabItem(wTabFolder, SWT.NONE);
     wSettingsTab.setFont(GuiResource.getInstance().getFontDefault());
     wSettingsTab.setText(BaseMessages.getString(PKG, 
"ActionCopyFiles.Settings.Label"));
@@ -238,10 +217,6 @@ public class ActionCopyFilesDialog extends ActionDialog {
     wSettingsTab.setControl(wSettingsComp);
     PropsUi.setLook(wSettingsComp);
 
-    // ///////////////////////////////////////////////////////////
-    // / END OF SETTINGS TAB
-    // ///////////////////////////////////////////////////////////
-
     wlFields = new Label(wFilesComp, SWT.NONE);
     wlFields.setText(BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.Label"));
     PropsUi.setLook(wlFields);
@@ -251,18 +226,13 @@ public class ActionCopyFilesDialog extends ActionDialog {
     wlFields.setLayoutData(fdlFields);
 
     int rows =
-        action.sourceFileFolder == null
+        action.getFileRows() == null
             ? 1
-            : (action.sourceFileFolder.length == 0 ? 0 : 
action.sourceFileFolder.length);
-    final int FieldsRows = rows;
+            : (action.getFileRows().isEmpty() ? 0 : 
action.getFileRows().size());
+    final int fieldsRows = rows;
 
     ColumnInfo[] colinf =
         new ColumnInfo[] {
-          new ColumnInfo(
-              BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.SourceEnvironment.Label"),
-              ColumnInfo.COLUMN_TYPE_CCOMBO,
-              false,
-              true),
           new ColumnInfo(
               BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.SourceFileFolder.Label"),
               ColumnInfo.COLUMN_TYPE_TEXT_BUTTON,
@@ -271,40 +241,24 @@ public class ActionCopyFilesDialog extends ActionDialog {
               BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.Wildcard.Label"),
               ColumnInfo.COLUMN_TYPE_TEXT,
               false),
-          new ColumnInfo(
-              BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.DestinationEnvironment.Label"),
-              ColumnInfo.COLUMN_TYPE_CCOMBO,
-              false,
-              true),
           new ColumnInfo(
               BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.DestinationFileFolder.Label"),
               ColumnInfo.COLUMN_TYPE_TEXT_BUTTON,
               false)
         };
 
-    setComboValues(colinf[0]);
-
-    ITextVarButtonRenderCallback callback =
-        () -> {
-          String envType = 
wFields.getActiveTableItem().getText(wFields.getActiveTableColumn() - 1);
-          return !STATIC_ENVIRONMENT.equalsIgnoreCase(envType);
-        };
+    colinf[0].setUsingVariables(true);
+    colinf[0].setToolTip(
+        BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.SourceFileFolder.Tooltip"));
+    colinf[0].setTextVarButtonSelectionListener(getFileSelectionAdapter());
 
     colinf[1].setUsingVariables(true);
-    colinf[1].setToolTip(
-        BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.SourceFileFolder.Tooltip"));
-    colinf[1].setTextVarButtonSelectionListener(getFileSelectionAdapter());
-    colinf[1].setRenderTextVarButtonCallback(callback);
+    colinf[1].setToolTip(BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.Wildcard.Tooltip"));
 
     colinf[2].setUsingVariables(true);
-    colinf[2].setToolTip(BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.Wildcard.Tooltip"));
-
-    setComboValues(colinf[3]);
-
-    colinf[4].setUsingVariables(true);
-    colinf[4].setToolTip(
+    colinf[2].setToolTip(
         BaseMessages.getString(PKG, 
"ActionCopyFiles.Fields.DestinationFileFolder.Tooltip"));
-    colinf[4].setTextVarButtonSelectionListener(getFileSelectionAdapter());
+    colinf[2].setTextVarButtonSelectionListener(getFileSelectionAdapter());
 
     wFields =
         new TableView(
@@ -312,7 +266,7 @@ public class ActionCopyFilesDialog extends ActionDialog {
             wFilesComp,
             SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI,
             colinf,
-            FieldsRows,
+            fieldsRows,
             lsMod,
             props);
 
@@ -360,7 +314,7 @@ public class ActionCopyFilesDialog extends ActionDialog {
     return new SelectionAdapter() {
       @Override
       public void widgetSelected(SelectionEvent e) {
-        String filename = BaseDialog.presentFileDialog(shell, null, null, 
true); // all files
+        String filename = BaseDialog.presentFileDialog(shell, null, null, 
true);
         if (filename != null) {
           wFields.getActiveTableItem().setText(wFields.getActiveTableColumn(), 
filename);
         }
@@ -376,77 +330,31 @@ public class ActionCopyFilesDialog extends ActionDialog {
   /** Copy information from the meta-data input to the dialog fields. */
   public void getData() {
     wName.setText(Const.NVL(action.getName(), ""));
-    wCopyEmptyFolders.setSelection(action.copyEmptyFolders);
+    wCopyEmptyFolders.setSelection(action.isCopyEmptyFolders());
 
-    if (action.sourceFileFolder != null) {
-      for (int i = 0; i < action.sourceFileFolder.length; i++) {
+    if (action.getFileRows() != null) {
+      for (int i = 0; i < action.getFileRows().size(); i++) {
         TableItem ti = wFields.table.getItem(i);
-        if (action.sourceFileFolder[i] != null) {
-          String sourceUrl = action.sourceFileFolder[i];
-          String clusterName = action.getConfigurationBy(sourceUrl);
-          if (clusterName != null) {
-            clusterName =
-                clusterName.startsWith(ActionCopyFiles.LOCAL_SOURCE_FILE)
-                    ? LOCAL_ENVIRONMENT
-                    : clusterName;
-            clusterName =
-                clusterName.startsWith(ActionCopyFiles.STATIC_SOURCE_FILE)
-                    ? STATIC_ENVIRONMENT
-                    : clusterName;
-
-            ti.setText(1, clusterName);
-            sourceUrl =
-                clusterName.equals(LOCAL_ENVIRONMENT) || 
clusterName.equals(STATIC_ENVIRONMENT)
-                    ? sourceUrl
-                    : action.getUrlPath(sourceUrl);
-          }
-          if (sourceUrl != null) {
-            sourceUrl = sourceUrl.replace(ActionCopyFiles.SOURCE_URL + i + 
"-", "");
-          } else {
-            sourceUrl = "";
-          }
-          ti.setText(2, sourceUrl);
+        CopyFilesItem row = action.getFileRows().get(i);
+        if (row.getSourceFileFolder() != null) {
+          ti.setText(1, row.getSourceFileFolder());
         }
-        if (action.wildcard[i] != null) {
-          ti.setText(3, action.wildcard[i]);
+        if (row.getWildcard() != null) {
+          ti.setText(2, row.getWildcard());
         }
-        if (action.destinationFileFolder[i] != null) {
-          String destinationURL = action.destinationFileFolder[i];
-          String clusterName = action.getConfigurationBy(destinationURL);
-          if (clusterName != null) {
-            clusterName =
-                clusterName.startsWith(ActionCopyFiles.LOCAL_DEST_FILE)
-                    ? LOCAL_ENVIRONMENT
-                    : clusterName;
-            clusterName =
-                clusterName.startsWith(ActionCopyFiles.STATIC_DEST_FILE)
-                    ? STATIC_ENVIRONMENT
-                    : clusterName;
-            ti.setText(4, clusterName);
-            destinationURL =
-                clusterName.equals(LOCAL_ENVIRONMENT) || 
clusterName.equals(STATIC_ENVIRONMENT)
-                    ? destinationURL
-                    : action.getUrlPath(destinationURL);
-          }
-          if (destinationURL != null) {
-            destinationURL = destinationURL.replace(ActionCopyFiles.DEST_URL + 
i + "-", "");
-          } else {
-            destinationURL = "";
-          }
-          ti.setText(5, destinationURL);
+        if (row.getDestinationFileFolder() != null) {
+          ti.setText(3, row.getDestinationFileFolder());
         }
       }
-
       wFields.optimizeTableView();
     }
-    wPrevious.setSelection(action.argFromPrevious);
-    wOverwriteFiles.setSelection(action.overwriteFiles);
-    wIncludeSubfolders.setSelection(action.includeSubFolders);
-    wRemoveSourceFiles.setSelection(action.removeSourceFiles);
-    wDestinationIsAFile.setSelection(action.destinationIsAFile);
-    wCreateDestinationFolder.setSelection(action.createDestinationFolder);
-
-    wAddFileToResult.setSelection(action.addResultFilenames);
+    wPrevious.setSelection(action.isArgFromPrevious());
+    wOverwriteFiles.setSelection(action.isOverwriteFiles());
+    wIncludeSubfolders.setSelection(action.isIncludeSubFolders());
+    wRemoveSourceFiles.setSelection(action.isRemoveSourceFiles());
+    wDestinationIsAFile.setSelection(action.isDestinationIsAFile());
+    wCreateDestinationFolder.setSelection(action.isCreateDestinationFolder());
+    wAddFileToResult.setSelection(action.isAddResultFilenames());
   }
 
   @Override
@@ -480,33 +388,14 @@ public class ActionCopyFilesDialog extends ActionDialog {
     action.setCreateDestinationFolder(wCreateDestinationFolder.getSelection());
 
     int nrItems = wFields.nrNonEmpty();
-
-    Map<String, String> sourceDestinationMappings = new HashMap<>();
-    action.sourceFileFolder = new String[nrItems];
-    action.destinationFileFolder = new String[nrItems];
-    action.wildcard = new String[nrItems];
-
+    java.util.List<CopyFilesItem> rows = new java.util.ArrayList<>(nrItems);
     for (int i = 0; i < nrItems; i++) {
-      String sourceNc = wFields.getNonEmpty(i).getText(1);
-      sourceNc =
-          sourceNc.equals(LOCAL_ENVIRONMENT) ? 
ActionCopyFiles.LOCAL_SOURCE_FILE + i : sourceNc;
-      sourceNc =
-          sourceNc.equals(STATIC_ENVIRONMENT) ? 
ActionCopyFiles.STATIC_SOURCE_FILE + i : sourceNc;
-      String source = wFields.getNonEmpty(i).getText(2);
-      String wild = wFields.getNonEmpty(i).getText(3);
-      String destNc = wFields.getNonEmpty(i).getText(4);
-      destNc = destNc.equals(LOCAL_ENVIRONMENT) ? 
ActionCopyFiles.LOCAL_DEST_FILE + i : destNc;
-      destNc = destNc.equals(STATIC_ENVIRONMENT) ? 
ActionCopyFiles.STATIC_DEST_FILE + i : destNc;
-      String dest = wFields.getNonEmpty(i).getText(5);
-      source = ActionCopyFiles.SOURCE_URL + i + "-" + source;
-      dest = ActionCopyFiles.DEST_URL + i + "-" + dest;
-      action.sourceFileFolder[i] =
-          action.loadURL(source, sourceNc, getMetadataProvider(), 
sourceDestinationMappings);
-      action.destinationFileFolder[i] =
-          action.loadURL(dest, destNc, getMetadataProvider(), 
sourceDestinationMappings);
-      action.wildcard[i] = wild;
+      String source = wFields.getNonEmpty(i).getText(1);
+      String wild = wFields.getNonEmpty(i).getText(2);
+      String dest = wFields.getNonEmpty(i).getText(3);
+      rows.add(new CopyFilesItem(source, dest, wild));
     }
-    action.setConfigurationMappings(sourceDestinationMappings);
+    action.setFileRows(rows);
 
     dispose();
   }
@@ -514,9 +403,4 @@ public class ActionCopyFilesDialog extends ActionDialog {
   public boolean showFileButtons() {
     return true;
   }
-
-  protected void setComboValues(ColumnInfo colInfo) {
-    String[] values = {LOCAL_ENVIRONMENT, STATIC_ENVIRONMENT};
-    colInfo.setComboValues(values);
-  }
 }
diff --git 
a/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/CopyFilesItem.java
 
b/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/CopyFilesItem.java
new file mode 100644
index 0000000000..4972178afd
--- /dev/null
+++ 
b/plugins/actions/copyfiles/src/main/java/org/apache/hop/workflow/actions/copyfiles/CopyFilesItem.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.workflow.actions.copyfiles;
+
+import java.util.Objects;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+
+/** One source / destination / wildcard row for the Copy files action. */
+@Getter
+@Setter
+public class CopyFilesItem {
+
+  @HopMetadataProperty(key = "source_filefolder")
+  private String sourceFileFolder;
+
+  @HopMetadataProperty(key = "destination_filefolder")
+  private String destinationFileFolder;
+
+  @HopMetadataProperty(key = "wildcard")
+  private String wildcard;
+
+  public CopyFilesItem() {}
+
+  public CopyFilesItem(String sourceFileFolder, String destinationFileFolder, 
String wildcard) {
+    this.sourceFileFolder = sourceFileFolder;
+    this.destinationFileFolder = destinationFileFolder;
+    this.wildcard = wildcard;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    CopyFilesItem that = (CopyFilesItem) o;
+    return Objects.equals(sourceFileFolder, that.sourceFileFolder)
+        && Objects.equals(destinationFileFolder, that.destinationFileFolder)
+        && Objects.equals(wildcard, that.wildcard);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(sourceFileFolder, destinationFileFolder, wildcard);
+  }
+}
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_de_DE.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_de_DE.properties
index ea65670c5d..87b8331e81 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_de_DE.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_de_DE.properties
@@ -27,7 +27,6 @@ ActionCopyFiles.CreateDestinationFolder.Tooltip=Erstelle das 
Ziel-Verzeichnis, f
 ActionCopyFiles.Description=Dateien kopieren
 ActionCopyFiles.DestinationIsAFile.Label=Ziel ist eine Datei
 ActionCopyFiles.DestinationIsAFile.Tooltip=Apache Hop wird 
ber\u00FCcksichtigen, dass das Ziel eine Datei ist.
-ActionCopyFiles.Error.DestinationFolderNotFound=Ziel-Verzeichnis nicht 
vorhanden\!
 ActionCopyFiles.Error.Exception.CanRemoveFileFolder=Kann Datei/Verzeichnis 
[{0}] nicht l\u00F6schen
 ActionCopyFiles.Error.Exception.CopyProcess=Fehler beim Versuch 
Datei/Verzeichnis [{0}] nach [{1}] zu kopieren. Apache Hop erhh\u00E4lt die 
folgende Ausnahme-Meldung : [{2}]
 
ActionCopyFiles.Error.Exception.CopyProcessFileSystemException=Dateisystem-Ausnahme:
 {0}
@@ -47,20 +46,14 @@ ActionCopyFiles.Filetype.All=Alle Dateien
 ActionCopyFiles.IncludeSubfolders.Label=Einschlie\u00DFlich Unterverzeichnisse
 ActionCopyFiles.IncludeSubfolders.Tooltip=Einschalten, wenn Sie auch 
Unterverzeichnisse holen m\u00F6chten.\r\nFunktioniert nur, wenn die Quelle ein 
Verzeichnis ist.
 ActionCopyFiles.keyword=kopieren,Dateien
-ActionCopyFiles.Log.ArgFromPrevious.Found=Finden von [{0}] Argumenten aus dem 
vorherigen Ergebnis (result).
 ActionCopyFiles.Log.CanNotCopyFolderToFile=Kann Verzeichnis [{0}] nicht in 
Datei [{1}] kopieren.
 ActionCopyFiles.Log.FetchFolder=Abholen \: [{0}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=Datei [{0}] wurde zum Ergebnis 
(result) f\u00FCr Dateinamen hinzugef\u00FCgt.
 ActionCopyFiles.Log.FileCopied=Datei [{0}] wurde nach Verzeichnis [{1}] 
kopiert.
 ActionCopyFiles.Log.FileExists=Datei [{0}] vorhanden\!
-ActionCopyFiles.Log.FileExistsInfos=Datei vorhanden\!
 ActionCopyFiles.Log.FileFolderRemoved=Datei/Verzeichnis [{0}] wurde 
gel\u00F6scht
-ActionCopyFiles.Log.FileOverwrite=Datei [{0}] wurde \u00FCberschrieben durch 
[{1}]
 ActionCopyFiles.Log.FolderCopied=Verzeichnis [{0}] wurde nach [{1}] kopiert.
 ActionCopyFiles.Log.FolderExists=Verzeichnis [{0}] vorhanden\!
-ActionCopyFiles.Log.FolderOverwrite=Verzeichnis [{0}] wurde \u00FCberschrieben
-ActionCopyFiles.Log.IgnoringRow=Zeilen mit Quell- oder Ziel-Wert \= leer 
(NULL) werden ignoriert. Quell-Datei/Verzeichnis \: [{0}], 
Ziel-Datei/Verzeichnis \: [{1}], Platzhalter (wildcard) \: [{2}]
-ActionCopyFiles.Log.ProcessingRow=Zeile in Arbeit\: Quell-Datei/Verzeichnis \: 
[{0}]... Ziel-Datei/Verzeichnis \: [{1}]... Platzhalter (wildcard) \: [{2}]...
 ActionCopyFiles.Log.Starting=Starten...
 ActionCopyFiles.Name=Dateien kopieren
 ActionCopyFiles.Name.Default=Dateien kopieren
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_en_US.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_en_US.properties
index 2fc7a4fd74..6e2597c412 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_en_US.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_en_US.properties
@@ -26,7 +26,8 @@ ActionCopyFiles.CreateDestinationFolder.Tooltip=Create the 
specified destination
 ActionCopyFiles.Description=Copy files
 ActionCopyFiles.DestinationIsAFile.Label=Destination is a file
 ActionCopyFiles.DestinationIsAFile.Tooltip=Select this if the destination is a 
file.
-ActionCopyFiles.Error.DestinationFolderNotFound=Destination folder does not 
exist\!
+ActionCopyFiles.Error.DestinationFolderNotFound=Destination folder [{0}] does 
not exist. Enable ''Create destination folder'' or create the folder before 
running.
+ActionCopyFiles.Error.FolderParentCreateFailed=Could not create destination 
folder [{0}]
 ActionCopyFiles.Error.Exception.CanRemoveFileFolder=Can not delete file/folder 
[{0}]
 ActionCopyFiles.Error.Exception.CopyProcess=Can not copy file/folder [{0}] to 
[{1}]. Exception \: [{2}]
 ActionCopyFiles.Error.Exception.CopyProcessFileSystemException=File System 
Exception: {0}
@@ -46,20 +47,28 @@ ActionCopyFiles.Filetype.All=All files
 ActionCopyFiles.IncludeSubfolders.Label=Include subfolders
 ActionCopyFiles.IncludeSubfolders.Tooltip=Check this if you want to fetch also 
sub folders\nThis option will work only when the source is a folder.
 ActionCopyFiles.keyword=copy,files
-ActionCopyFiles.Log.ArgFromPrevious.Found=found [{0}] argument(s) from 
previous result
+ActionCopyFiles.Log.ArgFromPrevious.Found=Found [{0}] row(s) from the previous 
action result to use as copy arguments.
 ActionCopyFiles.Log.CanNotCopyFolderToFile=Can not copy folder [{0}] to file 
[{1}]
 ActionCopyFiles.Log.FetchFolder=Fetching \: [{0}]
+ActionCopyFiles.Log.Debug.CouldNotGetFileSize=Could not determine size of 
source file {0}\: {1}
+ActionCopyFiles.Log.Debug.ResolvedPaths=Resolved copy paths - source\: [{0}], 
destination\: [{1}], wildcard\: [{2}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=File [{0}] was added to result 
filesname
 ActionCopyFiles.Log.FileCopied=File [{0}] was copied to [{1}] 
 ActionCopyFiles.Log.FileExists=file [{0}] exists\!
-ActionCopyFiles.Log.FileExistsInfos=File exists\!
+ActionCopyFiles.Log.FileExistsSkippingCopy=Destination file [{0}] already 
exists; skipping copy because replacing existing files is disabled.
 ActionCopyFiles.Log.FileFolderRemoved=File/folder [{0}] was deleted
-ActionCopyFiles.Log.FileOverwrite=File [{0}] was overwritten
+ActionCopyFiles.Log.FileOverwrite=Source [{0}] will overwrite destination [{1}]
+ActionCopyFiles.Log.FinishedSuccess=Copy files action finished successfully.
+ActionCopyFiles.Log.FinishedWithErrors=Copy files action finished with {0} 
error(s).
 ActionCopyFiles.Log.FolderCopied=Folder [{0}] was copied to [{1}]
 ActionCopyFiles.Log.FolderExists=Folder [{0}] exists\!
-ActionCopyFiles.Log.FolderOverwrite=Folder [{0}] was overwritten
-ActionCopyFiles.Log.IgnoringRow=Ignoring row with source or destination is 
NULL. Source File/folder source \: [{0}], destination file/folder \: [{1}], 
wildcard \: [{2}]
-ActionCopyFiles.Log.ProcessingRow=Processing row source File/folder source \: 
[{0}] ... destination file/folder \: [{1}]... wildcard \: [{2}]
+ActionCopyFiles.Log.FolderExistsSkippingCopy=Destination folder [{0}] already 
exists; skipping copy because replacing existing files is disabled.
+ActionCopyFiles.Log.FolderOverwrite=Source folder [{0}] will overwrite 
destination folder [{1}]
+ActionCopyFiles.Log.FolderParentCreated=Created destination folder [{0}]
+ActionCopyFiles.Log.FolderParentWillCreate=Destination folder [{0}] does not 
exist; creating it now.
+ActionCopyFiles.Log.IgnoringRow=Ignoring row\: source or destination is empty. 
Source\: [{0}], destination\: [{1}], wildcard\: [{2}]
+ActionCopyFiles.Log.ProcessingRow=Processing copy row - source\: [{0}], 
destination\: [{1}], wildcard\: [{2}]
+ActionCopyFiles.Log.ScanningSourceFolder=Scanning source folder [{0}] for 
files to copy...
 ActionCopyFiles.Log.Starting=Starting ...
 ActionCopyFiles.Name=Copy files
 ActionCopyFiles.Name.Default=Copy files
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_es_AR.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_es_AR.properties
index 134760e058..69c8b23285 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_es_AR.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_es_AR.properties
@@ -26,7 +26,6 @@ ActionCopyFiles.CreateDestinationFolder.Label=Crear 
directorio destino
 ActionCopyFiles.CreateDestinationFolder.Tooltip=Crear directorio destino de 
ser necesario.\nSi el destino es un archivo, se crear\u00E1 el directorio padre 
de ser necesario.
 ActionCopyFiles.DestinationIsAFile.Label=El destino es un archivo
 ActionCopyFiles.DestinationIsAFile.Tooltip=Apache Hop considerar\u00E1 que el 
destino es un archivo.
-ActionCopyFiles.Error.DestinationFolderNotFound=\u00A1El directorio destino no 
existe\!
 ActionCopyFiles.Error.Exception.CanRemoveFileFolder=No se puede eliminar el 
archivo/directorio [{0}]
 ActionCopyFiles.Error.Exception.CopyProcess=No se puede copiar el 
archivo/directorio [{0}] a [{1}]. Excepci\u00F3n \: [{2}]
 ActionCopyFiles.Error.Exception.UnableLoadXML=Imposible cargar la entrada de 
trabajo del tipo 'Copiar archivos' del nodo XML
@@ -44,20 +43,14 @@ ActionCopyFiles.FilenameDelete.Tooltip=Quitar de la grilla 
a los archivos selecc
 ActionCopyFiles.Filetype.All=Todos los archivos
 ActionCopyFiles.IncludeSubfolders.Label=Incluir subdirectorios
 ActionCopyFiles.IncludeSubfolders.Tooltip=Seleccione esta opci\u00F3n si desea 
incluir subdirectorios\nEsta opci\u00F3n s\u00F3lo funcionar\u00E1 si el origen 
es un directorio.
-ActionCopyFiles.Log.ArgFromPrevious.Found=Se encontraron [{0}] argumentos del 
resultado anterior
 ActionCopyFiles.Log.CanNotCopyFolderToFile=Imposible copiar el directorio 
[{0}] al archivo [{1}]
 ActionCopyFiles.Log.FetchFolder=Leyendo \: [{0}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=El archivo [{0}] fue 
a\u00F1adido a los archivos de resultado
 ActionCopyFiles.Log.FileCopied=El archivo [{0}] fue copiado a [{1}]
 ActionCopyFiles.Log.FileExists=\u00A1El archivo [{0}] existe\!
-ActionCopyFiles.Log.FileExistsInfos=\u00A1El archivo existe\!
 ActionCopyFiles.Log.FileFolderRemoved=El archivo/directorio [{0}] fue eliminado
-ActionCopyFiles.Log.FileOverwrite=El archivo [{0}] fue sobreescrito
 ActionCopyFiles.Log.FolderCopied=El directorio [{0}] fue copiado a [{1}]
 ActionCopyFiles.Log.FolderExists=\u00A1El directorio [{0}] existe\!
-ActionCopyFiles.Log.FolderOverwrite=El directorio [{0}] fue sobreescrito
-ActionCopyFiles.Log.IgnoringRow=Ignorando fila donde el origen \u00F3 destino 
es NULO. Archivo/directorio origen\: [{0}], archivo/directorio destino\: [{1}], 
comod\u00EDn\: [{2}]
-ActionCopyFiles.Log.ProcessingRow=Procesando fila. Archivo/Directorio origen\: 
[{0}] ... Archivo/Directorio destino\: [{1}] ... Comod\u00EDn\: [{2}]
 ActionCopyFiles.Log.Starting=Comenzando...
 ActionCopyFiles.Name=Copiar archivos
 ActionCopyFiles.Name.Default=Copiar archivos
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_fr_FR.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_fr_FR.properties
index 1b564e1b28..74d61549ba 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_fr_FR.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_fr_FR.properties
@@ -26,7 +26,6 @@ ActionCopyFiles.CreateDestinationFolder.Tooltip=Cr\u00E9er le 
r\u00E9pertoire de
 ActionCopyFiles.Description=Copie de fichiers\n Possibilit\u00E9 de parcourir 
un dossier, de copier les fichier vers une destination\n et enlever les source.
 ActionCopyFiles.DestinationIsAFile.Label=La destination est un fichier
 ActionCopyFiles.DestinationIsAFile.Tooltip=La destination est un fichier.
-ActionCopyFiles.Error.DestinationFolderNotFound=Le dossier de destination 
n'existe pas\u00A0!
 ActionCopyFiles.Error.Exception.CanRemoveFileFolder=Impossible de supprimer le 
fichier/r\u00E9pertoire [{0}]
 ActionCopyFiles.Error.Exception.CopyProcess=Apache Hop a rencontr\u00E9 une 
erreur lors du processus de copie du fichier/r\u00E9pertoire [{0}] vers [{1}]. 
Exception \: [{2}]
 ActionCopyFiles.Error.Exception.CopyProcessFileSystemException=Erreur au 
niveau du syst\u00E8me de fichier\: {0}
@@ -43,20 +42,14 @@ ActionCopyFiles.FilenameDelete.Tooltip=Retirer les 
entr\u00E9es s\u00E9lectionn\
 ActionCopyFiles.Filetype.All=Tous les fichiers
 ActionCopyFiles.IncludeSubfolders.Label=Inclure sous r\u00E9pertoires
 ActionCopyFiles.IncludeSubfolders.Tooltip=S\u00E9lectionnez cette option pour 
parcourir les sous r\u00E9pertoires\nCette option ne sera prise en compte que 
si la source est un r\u00E9pertoire.
-ActionCopyFiles.Log.ArgFromPrevious.Found=[{0}] argument(s) trouv\u00E9(s) 
depuis l''action pr\u00E9c\u00E9dente
 ActionCopyFiles.Log.CanNotCopyFolderToFile=Impossible de copier le 
r\u00E9pertoire [{0}] vers le fichier [{1}]
 ActionCopyFiles.Log.FetchFolder=R\u00E9pertoire racine recherche \: [{0}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=Le fichier [{0}] a 
\u00E9t\u00E9 ajout\u00E9 aux nom de fichiers en r\u00E9sultat
 ActionCopyFiles.Log.FileCopied=Le fichier [{0}] a \u00E9t\u00E9 copi\u00E9 
vers [{1}] 
 ActionCopyFiles.Log.FileExists=Le fichier [{0}] existe\!
-ActionCopyFiles.Log.FileExistsInfos=Fichier existe\!
 ActionCopyFiles.Log.FileFolderRemoved=Le Fichier/r\u00E9pertoire [{0}] a 
\u00E9t\u00E9 supprim\u00E9
-ActionCopyFiles.Log.FileOverwrite=Le fichier [{0}] a \u00E9t\u00E9 
\u00E9cras\u00E9
 ActionCopyFiles.Log.FolderCopied=Le r\u00E9pertoire [{0}] a \u00E9t\u00E9 
copi\u00E9 vers [{1}]
 ActionCopyFiles.Log.FolderExists=Le r\u00E9pertoire [{0}] existe\!
-ActionCopyFiles.Log.FolderOverwrite=Le r\u00E9pertoire [{0}] a \u00E9t\u00E9 
\u00E9cras\u00E9
-ActionCopyFiles.Log.IgnoringRow=La ligne dont la source ou la destination est 
nulle a \u00E9t\u00E9 ignor\u00E9e. Fichier/r\u00E9pertoire source \: [{0}], 
fichier/r\u00E9pertoire destination \: [{1}], caract\u00E8res joker \: [{2}]
-ActionCopyFiles.Log.ProcessingRow=Traitement ligne Fichier/r\u00E9pertoire 
source \: [{0}] ... fichier/r\u00E9pertoire destination \: [{1}]... 
caract\u00E8res joker \: [{2}]
 ActionCopyFiles.Log.Starting=D\u00E9marrage ...
 ActionCopyFiles.Name=Copie de fichiers
 ActionCopyFiles.Name.Default=Copie de fichiers
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_it_IT.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_it_IT.properties
index d6391f30c2..afd69664b1 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_it_IT.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_it_IT.properties
@@ -26,7 +26,6 @@ ActionCopyFiles.CreateDestinationFolder.Label=Crea cartelle 
di destinazione
 ActionCopyFiles.CreateDestinationFolder.Tooltip=Crea cartella di destinazione 
se necessario.\r\nSe la destinazione \u00E8 un file, la cartella padre 
verr\u00E0 creata se necessario.
 ActionCopyFiles.DestinationIsAFile.Label=La destinazione \u00E8 un file
 ActionCopyFiles.DestinationIsAFile.Tooltip=Apache Hop considera la 
destinazione come file.
-ActionCopyFiles.Error.DestinationFolderNotFound=La cartella di destinazione 
non esiste\!
 ActionCopyFiles.Error.Exception.CanRemoveFileFolder=Impossibile cancellare il 
file/cartella [{0}]
 ActionCopyFiles.Error.Exception.CopyProcess=Impossibile copiare file/cartella 
[{0}] in [{1}]. Eccezione\: [{2}]
 ActionCopyFiles.Error.Exception.CopyProcessFileSystemException=Eccezione del 
file system\: {0}
@@ -43,20 +42,14 @@ ActionCopyFiles.FilenameDelete.Tooltip=Rimuovi i file 
selezionati dalla griglia
 ActionCopyFiles.Filetype.All=Tutti i file
 ActionCopyFiles.IncludeSubfolders.Label=Includi sottocartelle
 ActionCopyFiles.IncludeSubfolders.Tooltip=Seleziona questo se vuoi analizzare 
anche le sottocartelle\r\nQuesta opzione funzioner\u00E0 solo quando la 
sorgente \u00E8 una cartella.
-ActionCopyFiles.Log.ArgFromPrevious.Found=trovato/i [{0}] argomento/i dal 
precedente risultato
 ActionCopyFiles.Log.CanNotCopyFolderToFile=Impossibile copiare la cartella 
[{0}] nel file [{1}]
 ActionCopyFiles.Log.FetchFolder=Prelievo \: [{0}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=Il file [{0}] \u00E8 stato 
aggiunto al risultato
 ActionCopyFiles.Log.FileCopied=Il file [{0}] \u00E8 stato copiato in [{1}] 
 ActionCopyFiles.Log.FileExists=Il file [{0}] esiste\!
-ActionCopyFiles.Log.FileExistsInfos=File esistente\!
 ActionCopyFiles.Log.FileFolderRemoved=File/Cartella [{0}] cancellato
-ActionCopyFiles.Log.FileOverwrite=File [{0}] sovrascritto
 ActionCopyFiles.Log.FolderCopied=La cartella [{0}] \u00E8 stata copiata in 
[{1}]
 ActionCopyFiles.Log.FolderExists=La cartella [{0}] esiste\!
-ActionCopyFiles.Log.FolderOverwrite=La cartella [{0}] \u00E8 stata sovrascritta
-ActionCopyFiles.Log.IgnoringRow=Ignoro la riga con fonte o destinazione NULLI. 
File/cartella di partenza (fonte) \: [{0}], file/cartella di destinazione \: 
[{0}], wildcard \: [{2}]
-ActionCopyFiles.Log.ProcessingRow=Processo la riga con fonte File/cartella \: 
[{0}] ... file/folder di destinazione \: [{1}]... wildcard \: [{2}]
 ActionCopyFiles.Log.Starting=Avvio...
 ActionCopyFiles.Name=Copia i file
 ActionCopyFiles.Name.Default=Copia i file
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_ja_JP.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_ja_JP.properties
index 702f179d91..e42feb1c6f 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_ja_JP.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_ja_JP.properties
@@ -27,7 +27,6 @@ ActionCopyFiles.CreateDestinationFolder.Tooltip=Create 
destination folder if nec
 ActionCopyFiles.Description=Copy 
files\n\u30d5\u30a1\u30a4\u30eb\u30b3\u30d4\u30fc
 
ActionCopyFiles.DestinationIsAFile.Label=\u79fb\u52d5\u5148\u304c\u30d5\u30a1\u30a4\u30eb
 
ActionCopyFiles.DestinationIsAFile.Tooltip=\u79fb\u52d5\u5148\u304c\u30d5\u30a1\u30a4\u30eb\u3067\u3042\u308b\u5834\u5408\u306f\u30c1\u30a7\u30c3\u30af\u30fb\u30dc\u30c3\u30af\u30b9\u3092\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002
-ActionCopyFiles.Error.DestinationFolderNotFound=Destination folder does not 
exist\!
 ActionCopyFiles.Error.Exception.CanRemoveFileFolder=Can not delete file/folder 
[{0}]
 ActionCopyFiles.Error.Exception.CopyProcess=Can not copy file/folder [{0}] to 
[{1}]. Exception \: [{2}]
 ActionCopyFiles.Error.Exception.CopyProcessFileSystemException=File System 
Exception: {0}
@@ -44,20 +43,14 @@ 
ActionCopyFiles.FilenameDelete.Tooltip=\u9078\u629e\u3055\u308c\u305f\u30d5\u30a
 ActionCopyFiles.Filetype.All=\u5168\u3066\u306e\u30d5\u30a1\u30a4\u30eb
 
ActionCopyFiles.IncludeSubfolders.Label=\u30b5\u30d6\u30fb\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u542b\u3080
 
ActionCopyFiles.IncludeSubfolders.Tooltip=\u30b5\u30d6\u30fb\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u542b\u3080\u5834\u5408\u306f\u30c1\u30a7\u30c3\u30af\u30fb\u30dc\u30c3\u30af\u30b9\u3092\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002
-ActionCopyFiles.Log.ArgFromPrevious.Found=found [{0}] argument(s) from 
previous result
 ActionCopyFiles.Log.CanNotCopyFolderToFile=Can not copy folder [{0}] to file 
[{1}]
 ActionCopyFiles.Log.FetchFolder=Fetching \: [{0}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=File [{0}] was added to result 
filesname
 ActionCopyFiles.Log.FileCopied=File [{0}] was copied to [{1}] 
 ActionCopyFiles.Log.FileExists=file [{0}] exists\!
-ActionCopyFiles.Log.FileExistsInfos=File exists\!
 ActionCopyFiles.Log.FileFolderRemoved=File/folder [{0}] was deleted
-ActionCopyFiles.Log.FileOverwrite=File [{0}] was overwritten
 ActionCopyFiles.Log.FolderCopied=Folder [{0}] was copied to [{1}]
 ActionCopyFiles.Log.FolderExists=Folder [{0}] exists\!
-ActionCopyFiles.Log.FolderOverwrite=Folder [{0}] was overwritten
-ActionCopyFiles.Log.IgnoringRow=Ignoring row with source or destination is 
NULL. Source File/folder source \: [{0}], destination file/folder \: [{1}], 
wildcard \: [{2}]
-ActionCopyFiles.Log.ProcessingRow=Processing row source File/folder source \: 
[{0}] ... destination file/folder \: [{1}]... wildcard \: [{2}]
 ActionCopyFiles.Log.Starting=Starting ...
 ActionCopyFiles.Name=\u30d5\u30a1\u30a4\u30eb\u30b3\u30d4\u30fc
 ActionCopyFiles.Name.Default=\u30b3\u30d4\u30fc\u30d5\u30a1\u30a4\u30eb
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_ko_KR.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_ko_KR.properties
index 10330889db..6134de5351 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_ko_KR.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_ko_KR.properties
@@ -26,7 +26,6 @@ ActionCopyFiles.CreateDestinationFolder.Label=\uB300\uC0C1 
\uD3F4\uD130 \uC0DD\u
 ActionCopyFiles.CreateDestinationFolder.Tooltip=\uD544\uC694\uD558\uBA74 
\uB300\uC0C1 \uB514\uB809\uD1A0\uB9AC \uC0DD\uC131.\r\n\uB9CC\uC57D 
\uB300\uC0C1\uC774 \uD30C\uC77C\uBBF8\uBA74 \uBD80\uBAA8 \uD3F4\uB354\uAC00 
\uC0DD\uC131\uB420 \uAC83\uC785\uB2C8\uB2E4.
 ActionCopyFiles.DestinationIsAFile.Label=\uB300\uC0C1\uC740 \uD30C\uC77C
 ActionCopyFiles.DestinationIsAFile.Tooltip=Apache Hop\uB294 \uB300\uC0C1\uC744 
\uD30C\uC77C\uB85C \uC778\uC2DD\uD569\uB2C8\uB2E4.
-ActionCopyFiles.Error.DestinationFolderNotFound=\uB300\uC0C1 
\uD3F4\uB354\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4\!
 ActionCopyFiles.Error.Exception.CanRemoveFileFolder=\uD30C\uC77C/\uD3F4\uB354 
[{0}]\uB97C \uC0AD\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4
 ActionCopyFiles.Error.Exception.CopyProcess=\uD30C\uC77C/\uD3F4\uB354 
[{0}]\uC744 [{1}]\uB85C \uBCF5\uC0AC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 
\uC608\uC678 \: [{2}]
 ActionCopyFiles.Error.Exception.CopyProcessFileSystemException=\uD30C\uC77C 
\uC2DC\uC2A4\uD15C \uC608\uC678\: {0}
@@ -43,20 +42,14 @@ 
ActionCopyFiles.FilenameDelete.Tooltip=\uADF8\uB9AC\uB4DC\uC5D0\uC11C \uC120\uD0
 ActionCopyFiles.Filetype.All=\uBAA8\uB4E0 \uD30C\uC77C
 ActionCopyFiles.IncludeSubfolders.Label=\uD558\uC704\uD3F4\uB354 \uD3EC\uD568
 ActionCopyFiles.IncludeSubfolders.Tooltip=\uD558\uC704 \uD3F4\uB354\uB97C 
\uAC00\uC838\uC624\uB824\uBA74 \uC5EC\uAE30\uB97C 
\uCCB4\uD06C\uD558\uC2ED\uC2DC\uC624\r\n\uC774 \uC635\uC158\uC740 
\uC18C\uC2A4\uAC00 \uD3F4\uB354\uC778 \uACBD\uC6B0\uB9CC 
\uB3D9\uC791\uD569\uB2C8\uB2E4.
-ActionCopyFiles.Log.ArgFromPrevious.Found=\uC774\uC804 
\uACB0\uACFC\uB85C\uBD80\uD130 [{0}] \uC778\uC790 \uBC1C\uACAC
-ActionCopyFiles.Log.CanNotCopyFolderToFile=\uD3F4\uB354 [{0}]\uB97C 
\uD30C\uC77C [{1]}]\uB85C \uBCF5\uC0AC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4
+ActionCopyFiles.Log.CanNotCopyFolderToFile=\uD3F4\uB354 [{0}]\uB97C 
\uD30C\uC77C [{1}]\uB85C \uBCF5\uC0AC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4
 ActionCopyFiles.Log.FetchFolder=Fetching \: [{0}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=\uD30C\uC77C [{0}]\uC744 
\uACB0\uACFC \uD30C\uC77C\uC774\uB984\uC5D0 
\uCD94\uAC00\uD558\uC600\uC2B5\uB2C8\uB2E4
 ActionCopyFiles.Log.FileCopied=\uD30C\uC77C [{0}]\uB97C [{1}]\uB85C 
\uBCF5\uC0AC\uD558\uC600\uC2B5\uB2C8\uB2E4
 ActionCopyFiles.Log.FileExists=\uD30C\uC77C [{0}]\uC774 
\uC874\uC7AC\uD569\uB2C8\uB2E4\!
-ActionCopyFiles.Log.FileExistsInfos=\uD30C\uC77C\uC774 
\uC874\uC7AC\uD569\uB2C8\uB2E4\!
 ActionCopyFiles.Log.FileFolderRemoved=\uD30C\uC77C/\uD3F4\uB354 [{0}]\uB97C 
\uC0AD\uC81C\uD558\uC600\uC2B5\uB2C8\uB2E4
-ActionCopyFiles.Log.FileOverwrite=\uD30C\uC77C [{0}]\uB97C 
\uACB9\uCCD0\uC4F0\uAE30\uD558\uC600\uC2B5\uB2C8\uB2E4
 ActionCopyFiles.Log.FolderCopied=\uD3F4\uB354 [{0}]\uB97C [{1}]\uB85C 
\uBCF5\uC0AC\uD558\uC600\uC2B5\uB2C8\uB2E4
 ActionCopyFiles.Log.FolderExists=\uD3F4\uB354 [{0}]\uAC00 
\uC874\uC7AC\uD569\uB2C8\uB2E4
-ActionCopyFiles.Log.FolderOverwrite=\uD3F4\uB354 [{0}]\uB97C 
\uACB9\uCCD0\uC4F0\uAE30\uD558\uC600\uC2B5\uB2C8\uB2E4
-ActionCopyFiles.Log.IgnoringRow=\uC18C\uC2A4\uB098 \uB300\uC0C1\uC774 
NILL\uC774\uBA74 \uB85C\uC6B0\uB97C \uBB34\uC2DC. \uC18C\uC2A4 
\uD30C\uC77C/\uD3F4\uB354 \uC18C\uC2A4 \: [{0}], \uB300\uC0C1 
\uD30C\uC77C/\uD3F4\uB354 \: [{1}], \uC640\uC77C\uB4DC\uCE74\uB4DC \: [{2}]
-ActionCopyFiles.Log.ProcessingRow=\uB85C\uC6B0 \uCC98\uB9AC \uC18C\uC2A4 
\uD30C\uC77C/\uD3F4\uB354 \uC18C\uC2A4 \: [{0}] ... \uB300\uC0C1 
\uD30C\uC77C/\uD3F4\uB354 \: [{1}] ... \uC640\uC77C\uB4DC\uCE74\uB4DC \: [{2}]
 ActionCopyFiles.Log.Starting=\uC2DC\uC791 ...
 ActionCopyFiles.Name.Default=Copy files
 ActionCopyFiles.Name.Label=Action name\: 
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_nl_NL.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_nl_NL.properties
index 90f945e2ae..85741db0a7 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_nl_NL.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_nl_NL.properties
@@ -26,7 +26,6 @@ ActionCopyFiles.CreateDestinationFolder.Label=Maak de 
doel-folder aan
 ActionCopyFiles.CreateDestinationFolder.Tooltip=Maak indien nodig de 
doel-folder aan.  \nAls het doel een bestand is zal de folder indien nodig 
aangemaakt worden.
 ActionCopyFiles.DestinationIsAFile.Label=Doel is een bestand
 ActionCopyFiles.DestinationIsAFile.Tooltip=We nemen aan dat het doel van de 
copieer-opdracht een bestand is.
-ActionCopyFiles.Error.DestinationFolderNotFound=De doel-folder bestaat niet.
 ActionCopyFiles.Error.Exception.CanRemoveFileFolder=Het was niet mogelijk om 
het bestand of de folder [{0}] te verwijderen.
 ActionCopyFiles.Error.Exception.CopyProcess=Het is niet mogelijk om bestand or 
folder [{0}] naar bestemming [{1}] te kopi\u00EBren\: [{2}]
 ActionCopyFiles.Error.Exception.UnableLoadXML=Het was niet mogelijk om de 
action van het type "copyfiles" te laden van uit XML
@@ -42,20 +41,14 @@ ActionCopyFiles.FilenameDelete.Tooltip=Verwijder de 
geselecteerde bestanden uit
 ActionCopyFiles.Filetype.All=Alle bestanden
 ActionCopyFiles.IncludeSubfolders.Label=sub-folders meenemen
 ActionCopyFiles.IncludeSubfolders.Tooltip=Vink deze optie aan als u ook alle 
sub-folders wil mee kopi\u00EBren.\nDeze optie werkt enkel indien als bron een 
folder werd opgegeven.
-ActionCopyFiles.Log.ArgFromPrevious.Found=We vonden [{0}] argumentsen in het 
resultaat van een vorige action
 ActionCopyFiles.Log.CanNotCopyFolderToFile=Het was niet mogelijk om folder 
[{0}] naar bestand [{1}] te kopi\u00EBren.
 ActionCopyFiles.Log.FetchFolder=Ophalen van \: [{0}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=Bestandsnaam [{0}] werd 
toegevoegd aan het resultaat.
 ActionCopyFiles.Log.FileCopied=Bestand [{0}] is gekopieerd naar [{1}]
 ActionCopyFiles.Log.FileExists=Bestand [{0}] bestaat reeds\!
-ActionCopyFiles.Log.FileExistsInfos=Bestand bestaat\!
 ActionCopyFiles.Log.FileFolderRemoved=Bestand of folder [{0}] werd verwijderd
-ActionCopyFiles.Log.FileOverwrite=Bestand [{0}] werd overschreven
 ActionCopyFiles.Log.FolderCopied=Folder [{0}] werd gekopieerd naar [{1}]
 ActionCopyFiles.Log.FolderExists=Folder [{0}] bestaat\!
-ActionCopyFiles.Log.FolderOverwrite=Folder [{0}] werd overschreven
-ActionCopyFiles.Log.IgnoringRow=We negeren een rij waar de bron of het doel 
leeg is.  Bronbestand of -folder [{0}], doelbestand of -folder \: [{1}], 
wildcard \: [{2}].
-ActionCopyFiles.Log.ProcessingRow=Verwerken van rij met bronbestand of -folder 
[{0}] en doelbestand of -folder\: [{1}].  De wildcard is [{2}].
 ActionCopyFiles.Name.Default=Kopieer bestanden
 ActionCopyFiles.Name.Label=Action naam\:
 ActionCopyFiles.OverwriteFiles.Label=Vervang bestaande bestanden
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_pt_BR.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_pt_BR.properties
index ef2dea857d..41ba209009 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_pt_BR.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_pt_BR.properties
@@ -26,7 +26,6 @@ ActionCopyFiles.CreateDestinationFolder.Tooltip=Crie o 
diret\u00F3rio de destino
 ActionCopyFiles.Description=Copiar arquivos
 ActionCopyFiles.DestinationIsAFile.Label=O destino \u00E9 um arquivo
 ActionCopyFiles.DestinationIsAFile.Tooltip=Apache Hop considerar\u00E1 que o 
destino \u00E9 um arquivo.
-ActionCopyFiles.Error.DestinationFolderNotFound=A pasta destino n\u00E3o 
existe\!
 ActionCopyFiles.Error.Exception.CanRemoveFileFolder=N\u00E3o \u00E9 
poss\u00EDvel apagar a pasta/o arquivo [{0}]
 ActionCopyFiles.Error.Exception.CopyProcess=N\u00E3o \u00E9 poss\u00EDvel 
copiar a pasta/o arquivo [{0}] a [{1}]. Excep\u00E7\u00E3o\: [{2}]
 ActionCopyFiles.Error.Exception.CopyProcessFileSystemException=Erro de sistema 
de arquivos: {0}
@@ -45,19 +44,14 @@ ActionCopyFiles.Filetype.All=Todos os arquivos
 ActionCopyFiles.IncludeSubfolders.Label=Incluir Subpastas
 ActionCopyFiles.IncludeSubfolders.Tooltip=Selecionar esta op\u00E7\u00E3o para 
incluir pastas filhas.\r\nSomente quando a origem \u00E9 uma pasta.
 ActionCopyFiles.keyword=copiar,arquivos
-ActionCopyFiles.Log.ArgFromPrevious.Found=Encontrou-se [{0}] argumento(s) do 
resulto anterior
 ActionCopyFiles.Log.CanNotCopyFolderToFile=N\u00E3o \u00E9 poss\u00EDvel 
copiar pasta [{0}] a arquivo [{1}]
 ActionCopyFiles.Log.FetchFolder=Buscando : [{0}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=Arquivo [{0}] foi adicionado ao 
resultado
 ActionCopyFiles.Log.FileCopied=Arquivo [{0}] foi copiado a [{1}] 
 ActionCopyFiles.Log.FileExists=O arquivo [{0}] existe\!
-ActionCopyFiles.Log.FileExistsInfos=O arquivo existe\!
 ActionCopyFiles.Log.FileFolderRemoved=A pasta/o arquivo [{0}] foi eliminado
-ActionCopyFiles.Log.FileOverwrite=Arquivo [{0}] foi sobrescrito
 ActionCopyFiles.Log.FolderCopied=A pasta [{0}] foi copiada a [{1}]
 ActionCopyFiles.Log.FolderExists=A pasta [{0}] existe\!
-ActionCopyFiles.Log.FolderOverwrite=A pasta [{0}] foi substitu\u00EDda
-ActionCopyFiles.Log.ProcessingRow=Processando arquivo/pasta origem: [{0}] ... 
arquivo/pasta destino: [{1}]... coringa : [{2}]
 ActionCopyFiles.Log.Starting=Iniciando ...
 ActionCopyFiles.Name=Copiar arquivos
 ActionCopyFiles.Name.Default=Copiar arquivos
@@ -71,5 +65,4 @@ ActionCopyFiles.Settings.Label=Configura\u00E7\u00E3o
 ActionCopyFiles.Tab.Files.Label=Arquivos
 ActionCopyFiles.Title=Copiar arquivos
 ActionCopyFiles.Fields.SourceFileFolder.Tooltip=Adicione aqui o arquivo ou 
pasta para copiar\n se for uma pasta, o Apache Hop ler\u00E1 apenas se "incluir 
subpastas" estiver marcado!
-ActionCopyFiles.Log.IgnoringRow=Ignora linhas com origem ou destino nulo. 
Arquivo/pasta de origem : [{0}], arquivo/pasta de destino: [{1}], curinga: [{2}]
 ActionCopyFiles.Previous.Tooltip=Passa o resultado da entrada anterior como 
argumento dessa entrada.\nArgumentos devem estar na mesma ordem.\n ou seja: (1) 
arquivo/pasta de origem, (2) arquivo/pasta de destino, (3) curinga
diff --git 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_zh_CN.properties
 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_zh_CN.properties
index db8a5a0a51..a51bb19b49 100644
--- 
a/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_zh_CN.properties
+++ 
b/plugins/actions/copyfiles/src/main/resources/org/apache/hop/workflow/actions/copyfiles/messages/messages_zh_CN.properties
@@ -26,7 +26,6 @@ 
ActionCopyFiles.CreateDestinationFolder.Tooltip=\u5982\u679C\u5FC5\u8981\u521B\u
 ActionCopyFiles.Description=\u590D\u5236\u6587\u4EF6
 ActionCopyFiles.DestinationIsAFile.Label=\u76EE\u6807\u662F\u6587\u4EF6
 ActionCopyFiles.DestinationIsAFile.Tooltip=Apache Hop 
\u5C06\u628A\u8BE5\u76EE\u6807\u4F5C\u4E3A\u4E00\u4E2A\u6587\u4EF6.
-ActionCopyFiles.Error.DestinationFolderNotFound=\u76EE\u6807\u76EE\u5F55\u4E0D\u5B58\u5728\!
 
ActionCopyFiles.Error.Exception.CanRemoveFileFolder=\u4E0D\u80FD\u5220\u9664\u6587\u4EF6/\u76EE\u5F55
 [{0}]
 
ActionCopyFiles.Error.Exception.CopyProcess=\u4E0D\u80FD\u5C06\u6587\u4EF6/\u76EE\u5F55
 [{0}] \u590D\u5236\u5230 [{1}]. \u5F02\u5E38: [{2}]
 
ActionCopyFiles.Error.Exception.CopyProcessFileSystemException=\u6587\u4EF6\u7CFB\u7EDF\u5F02\u5E38\uFF1A{0}
@@ -46,20 +45,14 @@ ActionCopyFiles.Filetype.All=\u6240\u6709\u6587\u4EF6
 ActionCopyFiles.IncludeSubfolders.Label=\u5305\u62EC\u5B50\u76EE\u5F55
 
ActionCopyFiles.IncludeSubfolders.Tooltip=\u9009\u4E2D\u8BE5\u9009\u9879\u4EE5\u83B7\u53D6\u5B50\u76EE\u5F55,\n\u53EA\u6709\u5F53\u590D\u5236\u6E90\u662F\u76EE\u5F55\u65F6\u8BE5\u9009\u9879\u624D\u6709\u6548.
 ActionCopyFiles.keyword=\u590D\u5236\u3001\u6587\u4EF6
-ActionCopyFiles.Log.ArgFromPrevious.Found=\u4ECE\u4E0A\u4E00\u4E2A\u7ED3\u679C\u4E2D\u627E\u5230\u4E86
 [{0}] \u4E2A \u53C2\u6570
 
ActionCopyFiles.Log.CanNotCopyFolderToFile=\u4E0D\u80FD\u590D\u5236\u76EE\u5F55 
[{0}] \u5230\u6587\u4EF6 [{1}]
 ActionCopyFiles.Log.FetchFolder=\u6B63\u5728\u83B7\u53D6\: [{0}]
 ActionCopyFiles.Log.FileAddedToResultFilesName=\u6587\u4EF6 [{0}] 
\u5DF2\u7ECF\u88AB\u6DFB\u52A0\u5230\u7ED3\u679C\u6587\u4EF6\u4E2D
 ActionCopyFiles.Log.FileCopied=\u6587\u4EF6 [{0}] \u88AB\u590D\u5236\u5230 
[{1}] 
 ActionCopyFiles.Log.FileExists=\u6587\u4EF6 [{0}] \u5B58\u5728\!
-ActionCopyFiles.Log.FileExistsInfos=\u6587\u4EF6\u5B58\u5728\!
 ActionCopyFiles.Log.FileFolderRemoved=\u6587\u4EF6/\u76EE\u5F55 [{0}] 
\u88AB\u5220\u9664
-ActionCopyFiles.Log.FileOverwrite=\u6587\u4EF6[{0}] \u88AB\u8986\u76D6
 ActionCopyFiles.Log.FolderCopied=\u76EE\u5F55 [{0}] \u88AB\u590D\u5236\u5230 
[{1}]
 ActionCopyFiles.Log.FolderExists=\u76EE\u5F55 [{0}] \u5B58\u5728\!
-ActionCopyFiles.Log.FolderOverwrite=\u76EE\u5F55 [{0}] \u88AB\u8986\u76D6
-ActionCopyFiles.Log.IgnoringRow=\u5FFD\u7565\u4E86\u6E90\u6216\u76EE\u6807\u662F\u7A7A\u7684\u884C,\u6E90\u6587\u4EF6/\u76EE\u5F55\:
 [{0}], \u76EE\u6807\u6587\u4EF6/\u76EE\u5F55\: [{1}], \u901A\u914D\u7B26\: 
[{2}]
-ActionCopyFiles.Log.ProcessingRow=\u6B63\u5728\u5904\u7406\u884C, 
\u6E90\u6587\u4EF6/\u76EE\u5F55\: [{0}] ... 
\u76EE\u6807\u6587\u4EF6/\u76EE\u5F55 \: [{1}]... \u901A\u914D\u7B26 \: [{2}]
 ActionCopyFiles.Log.Starting=\u5F00\u59CB...
 ActionCopyFiles.Name=\u590D\u5236\u6587\u4EF6
 ActionCopyFiles.Name.Default=\u590D\u5236\u6587\u4EF6
diff --git a/plugins/actions/copyfiles/src/main/samples/actions/copy-files.hwf 
b/plugins/actions/copyfiles/src/main/samples/actions/copy-files.hwf
index 107c769e1e..844bc6a972 100644
--- a/plugins/actions/copyfiles/src/main/samples/actions/copy-files.hwf
+++ b/plugins/actions/copyfiles/src/main/samples/actions/copy-files.hwf
@@ -64,10 +64,8 @@ limitations under the License.
       <create_destination_folder>Y</create_destination_folder>
       <fields>
         <field>
-          
<source_filefolder>EMPTY_SOURCE_URL-0-${PROJECT_HOME}</source_filefolder>
-          
<source_configuration_name>STATIC-SOURCE-FILE-0</source_configuration_name>
-          
<destination_filefolder>EMPTY_DEST_URL-0-${PROJECT_HOME}/tmp-copy</destination_filefolder>
-          
<destination_configuration_name>STATIC-DEST-FILE-0</destination_configuration_name>
+          <source_filefolder>${PROJECT_HOME}</source_filefolder>
+          
<destination_filefolder>${PROJECT_HOME}/tmp-copy</destination_filefolder>
           <wildcard/>
         </field>
       </fields>
diff --git 
a/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesLoadSaveTest.java
 
b/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesLoadSaveTest.java
index 76e3796724..6abc59b782 100644
--- 
a/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesLoadSaveTest.java
+++ 
b/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesLoadSaveTest.java
@@ -16,19 +16,51 @@
  */
 package org.apache.hop.workflow.actions.copyfiles;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.InputStream;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
-import 
org.apache.hop.pipeline.transforms.loadsave.validator.ArrayLoadSaveValidator;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopXmlException;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
 import 
org.apache.hop.pipeline.transforms.loadsave.validator.IFieldLoadSaveValidator;
+import 
org.apache.hop.pipeline.transforms.loadsave.validator.ListLoadSaveValidator;
 import 
org.apache.hop.pipeline.transforms.loadsave.validator.StringLoadSaveValidator;
+import org.apache.hop.workflow.action.ActionMeta;
+import org.apache.hop.workflow.action.ActionSerializationTestUtil;
 import 
org.apache.hop.workflow.action.loadsave.WorkflowActionLoadSaveTestSupport;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
 
 class WorkflowActionCopyFilesLoadSaveTest
     extends WorkflowActionLoadSaveTestSupport<ActionCopyFiles> {
 
+  private static class CopyFilesItemValidator implements 
IFieldLoadSaveValidator<CopyFilesItem> {
+    private final StringLoadSaveValidator strings = new 
StringLoadSaveValidator();
+
+    @Override
+    public CopyFilesItem getTestObject() {
+      return new CopyFilesItem(
+          strings.getTestObject(), strings.getTestObject(), 
strings.getTestObject());
+    }
+
+    @Override
+    public boolean validateTestObject(CopyFilesItem original, Object actual) 
throws HopException {
+      return actual instanceof CopyFilesItem c && original.equals(c);
+    }
+  }
+
   @Override
   protected Class<ActionCopyFiles> getActionClass() {
     return ActionCopyFiles.class;
@@ -45,9 +77,7 @@ class WorkflowActionCopyFilesLoadSaveTest
         "addResultFilenames",
         "destinationIsAFile",
         "createDestinationFolder",
-        "sourceFileFolder",
-        "destinationFileFolder",
-        "wildcard");
+        "fileRows");
   }
 
   @Override
@@ -60,7 +90,8 @@ class WorkflowActionCopyFilesLoadSaveTest
         "removeSourceFiles", "isRemoveSourceFiles",
         "addResultFilenames", "isAddResultFilenames",
         "destinationIsAFile", "isDestinationIsAFile",
-        "createDestinationFolder", "isCreateDestinationFolder");
+        "createDestinationFolder", "isCreateDestinationFolder",
+        "fileRows", "getFileRows");
   }
 
   @Override
@@ -73,21 +104,123 @@ class WorkflowActionCopyFilesLoadSaveTest
         "removeSourceFiles", "setRemoveSourceFiles",
         "addResultFilenames", "setAddResultFilenames",
         "destinationIsAFile", "setDestinationIsAFile",
-        "createDestinationFolder", "setCreateDestinationFolder");
+        "createDestinationFolder", "setCreateDestinationFolder",
+        "fileRows", "setFileRows");
   }
 
   @Override
   protected Map<String, IFieldLoadSaveValidator<?>> 
createAttributeValidatorsMap() {
-    int fileArraySize = new Random().nextInt(5) + 1;
+    int rowCount = new Random().nextInt(5) + 1;
     Map<String, IFieldLoadSaveValidator<?>> attrMap = new HashMap<>();
-    attrMap.put(
-        "sourceFileFolder",
-        new ArrayLoadSaveValidator<>(new StringLoadSaveValidator(), 
fileArraySize));
-    attrMap.put(
-        "destinationFileFolder",
-        new ArrayLoadSaveValidator<>(new StringLoadSaveValidator(), 
fileArraySize));
-    attrMap.put(
-        "wildcard", new ArrayLoadSaveValidator<>(new 
StringLoadSaveValidator(), fileArraySize));
+    attrMap.put("fileRows", new ListLoadSaveValidator<>(new 
CopyFilesItemValidator(), rowCount));
     return attrMap;
   }
+
+  private static ActionCopyFiles loadCopyFilesFromXmlResource(String resource)
+      throws HopXmlException {
+    InputStream in = 
WorkflowActionCopyFilesLoadSaveTest.class.getResourceAsStream(resource);
+    assertNotNull(in, resource);
+    Document document = XmlHandler.loadXmlFile(in);
+    Node node = XmlHandler.getSubNode(document, ActionMeta.XML_TAG);
+    ActionCopyFiles action = new ActionCopyFiles();
+    action.setPluginId("COPY_FILES");
+    action.loadXml(node, new MemoryMetadataProvider(), new Variables());
+    return action;
+  }
+
+  @Test
+  void testActionXmlSerialization() throws Exception {
+    ActionCopyFiles action = 
loadCopyFilesFromXmlResource("/copy-files-action.xml");
+
+    assertFalse(action.isCopyEmptyFolders());
+    assertTrue(action.isArgFromPrevious());
+    assertTrue(action.isOverwriteFiles());
+    assertTrue(action.isIncludeSubFolders());
+    assertTrue(action.isRemoveSourceFiles());
+    assertTrue(action.isAddResultFilenames());
+    assertTrue(action.isDestinationIsAFile());
+    assertTrue(action.isCreateDestinationFolder());
+
+    assertEquals(2, action.getFileRows().size());
+    assertEquals("${SOURCE_DIR}/file1.txt", 
action.getFileRows().get(0).getSourceFileFolder());
+    assertEquals("/data/in", 
action.getFileRows().get(1).getSourceFileFolder());
+    assertEquals("${DEST_DIR}/out1.txt", 
action.getFileRows().get(0).getDestinationFileFolder());
+    assertEquals("/data/out", 
action.getFileRows().get(1).getDestinationFileFolder());
+    assertEquals("^file1\\.txt$", action.getFileRows().get(0).getWildcard());
+    assertTrue(Utils.isEmpty(action.getFileRows().get(1).getWildcard()));
+  }
+
+  @Test
+  void testCloneFromActionXml() throws Exception {
+    ActionCopyFiles action = 
loadCopyFilesFromXmlResource("/copy-files-action.xml");
+
+    ActionCopyFiles clone = (ActionCopyFiles) action.clone();
+
+    assertEquals(clone.getFileRows(), action.getFileRows());
+    assertEquals(clone.isCopyEmptyFolders(), action.isCopyEmptyFolders());
+    assertEquals(clone.isArgFromPrevious(), action.isArgFromPrevious());
+    assertEquals(clone.isDestinationIsAFile(), action.isDestinationIsAFile());
+  }
+
+  @Test
+  void testActionXmlRoundTripThroughGetXml() throws Exception {
+    ActionCopyFiles action = 
loadCopyFilesFromXmlResource("/copy-files-action.xml");
+    String wrapped = ActionSerializationTestUtil.getXml(action);
+    Document doc = XmlHandler.loadXmlString(wrapped);
+    Node node = XmlHandler.getSubNode(doc, ActionMeta.XML_TAG);
+    ActionCopyFiles again = new ActionCopyFiles();
+    again.setPluginId("COPY_FILES");
+    again.loadXml(node, new MemoryMetadataProvider(), new Variables());
+    assertEquals(action.getXml(), again.getXml());
+  }
+
+  @Test
+  void loadsLegacyFieldXmlWithWizardPrefixesAndConfigurationTags() throws 
Exception {
+    String xml =
+        "<?xml version=\"1.0\" 
encoding=\"UTF-8\"?><action><name>t</name><description/><type>COPY_FILES</type><attributes/>"
+            + 
"<copy_empty_folders>Y</copy_empty_folders><arg_from_previous>N</arg_from_previous>"
+            + 
"<overwrite_files>N</overwrite_files><include_subfolders>N</include_subfolders>"
+            + 
"<remove_source_files>N</remove_source_files><add_result_filesname>N</add_result_filesname>"
+            + 
"<destination_is_a_file>N</destination_is_a_file><create_destination_folder>Y</create_destination_folder>"
+            + "<fields><field>"
+            + "<source_filefolder>EMPTY_SOURCE_URL-0-${A}</source_filefolder>"
+            + 
"<source_configuration_name>STATIC-SOURCE-FILE-0</source_configuration_name>"
+            + 
"<destination_filefolder>EMPTY_DEST_URL-0-${A}/b</destination_filefolder>"
+            + 
"<destination_configuration_name>STATIC-DEST-FILE-0</destination_configuration_name>"
+            + "<wildcard>x</wildcard>"
+            + "</field></fields></action>";
+    Document doc = XmlHandler.loadXmlString(xml);
+    Node node = XmlHandler.getSubNode(doc, ActionMeta.XML_TAG);
+    ActionCopyFiles a = new ActionCopyFiles();
+    a.setPluginId("COPY_FILES");
+    a.loadXml(node, new MemoryMetadataProvider(), new Variables());
+    assertEquals("${A}", a.getFileRows().get(0).getSourceFileFolder());
+    assertEquals("${A}/b", a.getFileRows().get(0).getDestinationFileFolder());
+    assertEquals("x", a.getFileRows().get(0).getWildcard());
+  }
+
+  @Test
+  void legacyRowPrefixesAreStrippedOnLoadAndOmittedOnReserialize() throws 
Exception {
+    ActionCopyFiles a = new ActionCopyFiles();
+    a.setPluginId("COPY_FILES");
+    a.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                ActionCopyFiles.SOURCE_URL + "0-my/src",
+                ActionCopyFiles.DEST_URL + "0-my/dest",
+                ".*")));
+    String wrapped = ActionSerializationTestUtil.getXml(a);
+    Document doc = XmlHandler.loadXmlString(wrapped);
+    Node node = XmlHandler.getSubNode(doc, ActionMeta.XML_TAG);
+    ActionCopyFiles loaded = new ActionCopyFiles();
+    loaded.setPluginId("COPY_FILES");
+    loaded.loadXml(node, new MemoryMetadataProvider(), new Variables());
+    assertEquals("my/src", loaded.getFileRows().get(0).getSourceFileFolder());
+    assertEquals("my/dest", 
loaded.getFileRows().get(0).getDestinationFileFolder());
+    String out = loaded.getXml();
+    assertFalse(out.contains(ActionCopyFiles.SOURCE_URL));
+    assertFalse(out.contains(ActionCopyFiles.DEST_URL));
+    assertFalse(out.contains("source_configuration_name"));
+    assertFalse(out.contains("destination_configuration_name"));
+  }
 }
diff --git 
a/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesLocalTest.java
 
b/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesLocalTest.java
new file mode 100644
index 0000000000..26e62b9447
--- /dev/null
+++ 
b/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesLocalTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.
+ */
+package org.apache.hop.workflow.actions.copyfiles;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.Result;
+import org.apache.hop.core.RowMetaAndData;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.logging.LogLevel;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.engine.IWorkflowEngine;
+import org.apache.hop.workflow.engines.local.LocalWorkflowEngine;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.w3c.dom.Node;
+
+/**
+ * Integration-style tests using a real filesystem and full {@link 
ActionCopyFiles#execute} path.
+ */
+class WorkflowActionCopyFilesLocalTest {
+
+  @TempDir Path tempDir;
+
+  private ActionCopyFiles action;
+  private IWorkflowEngine<WorkflowMeta> workflow;
+
+  @BeforeAll
+  static void initLog() {
+    HopLogStore.init();
+  }
+
+  @BeforeEach
+  void setUp() {
+    workflow = new LocalWorkflowEngine(new WorkflowMeta());
+    action = new ActionCopyFiles();
+    action.setParentWorkflow(workflow);
+    action.setParentWorkflowMeta(mock(WorkflowMeta.class));
+    action.setLogLevel(LogLevel.ERROR);
+  }
+
+  @Test
+  void copyFileIntoExistingFolder() throws Exception {
+    Path srcFile = tempDir.resolve("src/hello.txt");
+    Files.createDirectories(srcFile.getParent());
+    Files.writeString(srcFile, "hello", StandardCharsets.UTF_8);
+    Path destDir = tempDir.resolve("dest");
+    Files.createDirectories(destDir);
+
+    action.setDestinationIsAFile(false);
+    action.setCreateDestinationFolder(false);
+    action.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                srcFile.toAbsolutePath().toString(), 
destDir.toAbsolutePath().toString(), "")));
+
+    Result result = action.execute(new Result(), 0);
+
+    assertTrue(result.isResult(), "copy should succeed");
+    assertEquals(0, result.getNrErrors());
+    Path out = destDir.resolve("hello.txt");
+    assertTrue(Files.isRegularFile(out));
+    assertEquals("hello", Files.readString(out, StandardCharsets.UTF_8));
+  }
+
+  @Test
+  void copyFileToFileCreatesParentWhenConfigured() throws Exception {
+    Path src = tempDir.resolve("source/a.txt");
+    Files.createDirectories(src.getParent());
+    Files.writeString(src, "content", StandardCharsets.UTF_8);
+    Path destFile = tempDir.resolve("nested/out/b.txt");
+
+    action.setDestinationIsAFile(true);
+    action.setCreateDestinationFolder(true);
+    action.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                src.toAbsolutePath().toString(), 
destFile.toAbsolutePath().toString(), "")));
+
+    Result result = action.execute(new Result(), 0);
+
+    assertTrue(result.isResult());
+    assertTrue(Files.isRegularFile(destFile));
+    assertEquals("content", Files.readString(destFile, 
StandardCharsets.UTF_8));
+  }
+
+  @Test
+  void failsWhenDestinationParentMissingAndCreateDisabled() throws Exception {
+    Path src = tempDir.resolve("only.txt");
+    Files.writeString(src, "x", StandardCharsets.UTF_8);
+    Path destFile = tempDir.resolve("noSuchParent/child/c.txt");
+
+    action.setDestinationIsAFile(true);
+    action.setCreateDestinationFolder(false);
+    action.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                src.toAbsolutePath().toString(), 
destFile.toAbsolutePath().toString(), "")));
+
+    Result result = action.execute(new Result(), 0);
+
+    assertFalse(result.isResult());
+    assertEquals(1, result.getNrErrors());
+  }
+
+  @Test
+  void folderSourceWithDestinationIsFileLogsError() throws Exception {
+    Path srcDir = tempDir.resolve("folderSrc");
+    Files.createDirectories(srcDir);
+    Files.writeString(srcDir.resolve("f.txt"), "in", StandardCharsets.UTF_8);
+    Path destFile = tempDir.resolve("destAsFile.txt");
+
+    action.setDestinationIsAFile(true);
+    action.setCreateDestinationFolder(true);
+    action.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                srcDir.toAbsolutePath().toString(), 
destFile.toAbsolutePath().toString(), "")));
+
+    Result result = action.execute(new Result(), 0);
+
+    assertFalse(result.isResult());
+    assertEquals(1, result.getNrErrors());
+  }
+
+  @Test
+  void copyFolderToFolderCopiesFileInRoot() throws Exception {
+    Path srcRoot = tempDir.resolve("srcTree");
+    Files.createDirectories(srcRoot);
+    Files.writeString(srcRoot.resolve("doc.txt"), "doc", 
StandardCharsets.UTF_8);
+    Path destRoot = tempDir.resolve("destTree");
+    Files.createDirectories(destRoot);
+
+    action.setDestinationIsAFile(false);
+    action.setIncludeSubFolders(false);
+    action.setCreateDestinationFolder(false);
+    action.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                srcRoot.toAbsolutePath().toString(), 
destRoot.toAbsolutePath().toString(), "")));
+
+    Result result = action.execute(new Result(), 0);
+
+    assertTrue(result.isResult());
+    Path copied = destRoot.resolve("doc.txt");
+    assertTrue(Files.isRegularFile(copied));
+    assertEquals("doc", Files.readString(copied, StandardCharsets.UTF_8));
+  }
+
+  @Test
+  void removeSourceFilesDeletesSourceAfterFileToFileCopy() throws Exception {
+    Path srcFile = tempDir.resolve("moveSrc/x.txt");
+    Files.createDirectories(srcFile.getParent());
+    Files.writeString(srcFile, "gone", StandardCharsets.UTF_8);
+    Path destFile = tempDir.resolve("moveDest/y.txt");
+    Files.createDirectories(destFile.getParent());
+
+    action.setDestinationIsAFile(true);
+    action.setCreateDestinationFolder(false);
+    action.setRemoveSourceFiles(true);
+    action.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                srcFile.toAbsolutePath().toString(), 
destFile.toAbsolutePath().toString(), "")));
+
+    Result result = action.execute(new Result(), 0);
+
+    assertTrue(result.isResult());
+    assertFalse(Files.exists(srcFile), "source file should be removed");
+    assertTrue(Files.isRegularFile(destFile));
+    assertEquals("gone", Files.readString(destFile, StandardCharsets.UTF_8));
+  }
+
+  @Test
+  void argFromPreviousUsesResultRows() throws Exception {
+    Path srcFile = tempDir.resolve("prev/a.txt");
+    Files.createDirectories(srcFile.getParent());
+    Files.writeString(srcFile, "row", StandardCharsets.UTF_8);
+    Path destDir = tempDir.resolve("prevDest");
+    Files.createDirectories(destDir);
+
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("source"));
+    rowMeta.addValueMeta(new ValueMetaString("dest"));
+    rowMeta.addValueMeta(new ValueMetaString("wildcard"));
+    RowMetaAndData row =
+        new RowMetaAndData(
+            rowMeta,
+            new Object[] {
+              srcFile.toAbsolutePath().toString(), 
destDir.toAbsolutePath().toString(), ""
+            });
+
+    Result previous = new Result();
+    List<RowMetaAndData> rows = new ArrayList<>();
+    rows.add(row);
+    previous.setRows(rows);
+
+    action.setArgFromPrevious(true);
+    action.setDestinationIsAFile(false);
+    action.setFileRows(List.of());
+
+    Result result = action.execute(previous, 0);
+
+    assertTrue(result.isResult());
+    assertTrue(Files.isRegularFile(destDir.resolve("a.txt")));
+  }
+
+  @Test
+  void normalizeLegacyVfsPathsStripsRowIndexedPrefixes() {
+    action.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                ActionCopyFiles.SOURCE_URL + "0-alpha", 
ActionCopyFiles.DEST_URL + "0-beta", "w")));
+    action.normalizeLegacyVfsPaths();
+    assertEquals("alpha", action.getFileRows().get(0).getSourceFileFolder());
+    assertEquals("beta", 
action.getFileRows().get(0).getDestinationFileFolder());
+    assertEquals("w", action.getFileRows().get(0).getWildcard());
+  }
+
+  @Test
+  void convertLegacyXmlDelegatesToNormalize() throws HopException {
+    action.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                ActionCopyFiles.SOURCE_URL + "0-q", ActionCopyFiles.DEST_URL + 
"0-r", null)));
+    action.convertLegacyXml(mock(Node.class));
+    assertEquals("q", action.getFileRows().get(0).getSourceFileFolder());
+    assertEquals("r", action.getFileRows().get(0).getDestinationFileFolder());
+  }
+
+  @Test
+  void checkPassesWhenSourceExists() throws Exception {
+    Path src = tempDir.resolve("check.txt");
+    Files.writeString(src, "ok", StandardCharsets.UTF_8);
+    action.setFileRows(
+        List.of(new CopyFilesItem(src.toAbsolutePath().toString(), 
tempDir.toString(), "")));
+
+    List<org.apache.hop.core.ICheckResult> remarks = new ArrayList<>();
+    action.check(remarks, new WorkflowMeta(), new Variables(), new 
MemoryMetadataProvider());
+
+    assertTrue(
+        remarks.stream()
+            .noneMatch(r -> r.getType() == 
org.apache.hop.core.ICheckResult.TYPE_RESULT_ERROR),
+        remarks::toString);
+  }
+
+  @Test
+  void isEvaluationTrue() {
+    assertTrue(new ActionCopyFiles().isEvaluation());
+  }
+}
diff --git 
a/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesTest.java
 
b/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesTest.java
index 3df55d729a..c496d88184 100644
--- 
a/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesTest.java
+++ 
b/plugins/actions/copyfiles/src/test/java/org/apache/hop/workflow/actions/copyfiles/WorkflowActionCopyFilesTest.java
@@ -27,10 +27,13 @@ import static org.mockito.Mockito.verify;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
 import org.apache.hop.core.Result;
 import org.apache.hop.core.logging.HopLogStore;
 import org.apache.hop.core.variables.Variables;
 import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
 import org.apache.hop.workflow.Workflow;
 import org.apache.hop.workflow.WorkflowMeta;
 import org.apache.hop.workflow.engine.IWorkflowEngine;
@@ -55,11 +58,17 @@ class WorkflowActionCopyFilesTest {
     entry = spy(entry);
   }
 
+  private static List<CopyFilesItem> rows(String[] src, String[] dst, String[] 
wild) {
+    List<CopyFilesItem> list = new ArrayList<>(src.length);
+    for (int i = 0; i < src.length; i++) {
+      list.add(new CopyFilesItem(src[i], dst[i], wild[i]));
+    }
+    return list;
+  }
+
   @Test
   void fileNotCopied() throws Exception {
-    entry.sourceFileFolder = new String[] {EMPTY};
-    entry.destinationFileFolder = new String[] {EMPTY};
-    entry.wildcard = new String[] {EMPTY};
+    entry.setFileRows(rows(new String[] {EMPTY}, new String[] {EMPTY}, new 
String[] {EMPTY}));
 
     entry.execute(new Result(), 0);
 
@@ -73,16 +82,14 @@ class WorkflowActionCopyFilesTest {
     String srcPath = "path/to/file";
     String destPath = "path/to/dir";
 
-    entry.sourceFileFolder = new String[] {srcPath};
-    entry.destinationFileFolder = new String[] {destPath};
-    entry.wildcard = new String[] {EMPTY};
+    entry.setFileRows(rows(new String[] {srcPath}, new String[] {destPath}, 
new String[] {EMPTY}));
 
     Result result = entry.execute(new Result(), 0);
 
     verify(entry)
         .processFileFolder(
             anyString(), anyString(), anyString(), any(Workflow.class), 
any(Result.class));
-    verify(entry, atLeast(1)).preprocessfilefilder(any(String[].class));
+    verify(entry, atLeast(1)).resolve(anyString());
     Assertions.assertFalse(result.isResult());
     Assertions.assertEquals(1, result.getNrErrors());
   }
@@ -92,9 +99,7 @@ class WorkflowActionCopyFilesTest {
     String[] srcPath = new String[] {"path1", "path2", "path3"};
     String[] destPath = new String[] {"dest1", "dest2", "dest3"};
 
-    entry.sourceFileFolder = srcPath;
-    entry.destinationFileFolder = destPath;
-    entry.wildcard = new String[] {EMPTY, EMPTY, EMPTY};
+    entry.setFileRows(rows(srcPath, destPath, new String[] {EMPTY, EMPTY, 
EMPTY}));
 
     Result result = entry.execute(new Result(), 0);
 
@@ -107,23 +112,21 @@ class WorkflowActionCopyFilesTest {
 
   @Test
   void saveLoad() throws Exception {
-    String[] srcPath = new String[] {"EMPTY_SOURCE_URL-0-"};
-    String[] destPath = new String[] {"EMPTY_DEST_URL-0-"};
-
-    entry.sourceFileFolder = srcPath;
-    entry.destinationFileFolder = destPath;
-    entry.wildcard = new String[] {EMPTY};
-
-    String xml = "<entry>" + entry.getXml() + "</entry>";
-    Assertions.assertTrue(xml.contains(srcPath[0]));
-    Assertions.assertTrue(xml.contains(destPath[0]));
-    ActionCopyFiles loadedentry = new ActionCopyFiles();
+    entry.setPluginId("COPY_FILES");
+    entry.setFileRows(
+        List.of(
+            new CopyFilesItem(
+                ActionCopyFiles.SOURCE_URL + "0-", ActionCopyFiles.DEST_URL + 
"0-", EMPTY)));
+
+    String xml = "<transform>" + entry.getXml() + "</transform>";
+    ActionCopyFiles loaded = new ActionCopyFiles();
+    loaded.setPluginId("COPY_FILES");
     InputStream is = new ByteArrayInputStream(xml.getBytes());
-    loadedentry.loadXml(
-        XmlHandler.getSubNode(XmlHandler.loadXmlFile(is, null, false, false), 
"entry"),
-        null,
+    loaded.loadXml(
+        XmlHandler.getSubNode(XmlHandler.loadXmlFile(is, null, false, false), 
"transform"),
+        new MemoryMetadataProvider(),
         new Variables());
-    Assertions.assertEquals(loadedentry.destinationFileFolder[0], destPath[0]);
-    Assertions.assertEquals(loadedentry.sourceFileFolder[0], srcPath[0]);
+    Assertions.assertEquals("", 
loaded.getFileRows().get(0).getDestinationFileFolder());
+    Assertions.assertEquals("", 
loaded.getFileRows().get(0).getSourceFileFolder());
   }
 }
diff --git a/plugins/actions/copyfiles/src/test/resources/copy-files-action.xml 
b/plugins/actions/copyfiles/src/test/resources/copy-files-action.xml
new file mode 100644
index 0000000000..578ccab6f2
--- /dev/null
+++ b/plugins/actions/copyfiles/src/test/resources/copy-files-action.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<action>
+    <name>Copy files</name>
+    <description>copy test</description>
+    <type>COPY_FILES</type>
+    <attributes/>
+    <copy_empty_folders>N</copy_empty_folders>
+    <arg_from_previous>Y</arg_from_previous>
+    <overwrite_files>Y</overwrite_files>
+    <include_subfolders>Y</include_subfolders>
+    <remove_source_files>Y</remove_source_files>
+    <add_result_filesname>Y</add_result_filesname>
+    <destination_is_a_file>Y</destination_is_a_file>
+    <create_destination_folder>Y</create_destination_folder>
+    <fields>
+        <field>
+            <source_filefolder>${SOURCE_DIR}/file1.txt</source_filefolder>
+            
<destination_filefolder>${DEST_DIR}/out1.txt</destination_filefolder>
+            <wildcard>^file1\.txt$</wildcard>
+        </field>
+        <field>
+            <source_filefolder>/data/in</source_filefolder>
+            <destination_filefolder>/data/out</destination_filefolder>
+            <wildcard/>
+        </field>
+    </fields>
+    <parallel>N</parallel>
+    <xloc>192</xloc>
+    <yloc>96</yloc>
+    <attributes_hac/>
+</action>


Reply via email to