MINIFICPP-1410 Add permissions property support for Putfile processor

Move CI Boost tests to Ubuntu 20.04 environment

There is an incompatilibity issue with libboost and gcc 4.8 which causes
throw of std::bad_allow or segmentation fault in tests.

Signed-off-by: Arpad Boda <aboda@apache.org>

This closes #942
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1babf65..eb19e11 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -98,7 +98,7 @@
         run: |
           sudo apt-add-repository -y "ppa:ubuntu-toolchain-r/test"
           sudo apt update
-          sudo apt install -y gcc-4.8 g++-4.8 bison flex libboost-all-dev uuid-dev openssl libcurl4-openssl-dev ccache libpython3-dev liblua5.1-0-dev libssh2-1-dev
+          sudo apt install -y gcc-4.8 g++-4.8 bison flex uuid-dev openssl libcurl4-openssl-dev ccache libpython3-dev liblua5.1-0-dev libssh2-1-dev
           echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV
           sudo unlink /usr/bin/gcc && sudo ln -s /usr/bin/gcc-4.8 /usr/bin/gcc
           sudo unlink /usr/bin/g++ && sudo ln -s /usr/bin/g++-4.8 /usr/bin/g++
@@ -121,7 +121,7 @@
             ubuntu-20.04-ccache-refs/heads/main-
       - id: install_deps
         run: |
-          sudo apt install -y ccache libfl-dev libpcap-dev
+          sudo apt install -y ccache libfl-dev libpcap-dev libboost-all-dev
           echo "PATH=/usr/lib/ccache:$PATH" >> $GITHUB_ENV
       - id: build
         run: ./bootstrap.sh -e -t && cd build  && cmake -DUSE_SHARED_LIBS= -DENABLE_PCAP=ON -DSTRICT_GSL_CHECKS=AUDIT .. && make -j4 VERBOSE=1  && make test ARGS="--timeout 300 -j2 --output-on-failure"
diff --git a/PROCESSORS.md b/PROCESSORS.md
index 13a71d1..f94b7d7 100644
--- a/PROCESSORS.md
+++ b/PROCESSORS.md
@@ -899,6 +899,8 @@
 |**Create Missing Directories**|true||If true, then missing destination directories will be created. If false, flowfiles are penalized and sent to failure.|
 |Directory|.||The output directory to which to put files<br/>**Supports Expression Language: true**|
 |Maximum File Count|-1||Specifies the maximum number of files that can exist in the output directory|
+|Permissions|||Sets the permissions on the output file to the value of this attribute. Must be an octal number (e.g. 644 or 0755). Not supported on Windows systems.|
+|Directory Permissions|||Sets the permissions on the directories being created if 'Create Missing Directories' property is set. Must be an octal number (e.g. 644 or 0755). Not supported on Windows systems.|
 ### Relationships
 
 | Name | Description |
diff --git a/cmake/BuildTests.cmake b/cmake/BuildTests.cmake
index 6df7240..40acb88 100644
--- a/cmake/BuildTests.cmake
+++ b/cmake/BuildTests.cmake
@@ -22,7 +22,7 @@
   SET(dirlist "")
   FOREACH(child ${children})
     IF( "${child}" MATCHES ^[^.].*\\.cpp)
-  
+
       LIST(APPEND dirlist ${child})
     ENDIF()
   ENDFOREACH()
@@ -32,7 +32,10 @@
 set(NANOFI_TEST_DIR "${CMAKE_SOURCE_DIR}/nanofi/tests/")
 
 if(NOT EXCLUDE_BOOST)
-	find_package(Boost COMPONENTS system filesystem)
+  find_package(Boost COMPONENTS system filesystem)
+  if(Boost_FOUND)
+    add_definitions(-DUSE_BOOST)
+  endif()
 endif()
 
 function(appendIncludes testName)
@@ -95,7 +98,7 @@
 else()
    	target_include_directories(${TEST_BASE_LIB} BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/libminifi/opsys/posix")
 endif()
- 
+
 SET(CATCH_MAIN_LIB catch_main)
 add_library(${CATCH_MAIN_LIB} STATIC "${TEST_DIR}/CatchMain.cpp")
 target_include_directories(${CATCH_MAIN_LIB} BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/thirdparty/catch")
@@ -133,6 +136,10 @@
 
     MATH(EXPR UNIT_TEST_COUNT "${UNIT_TEST_COUNT}+1")
     add_test(NAME "${testfilename}" COMMAND "${testfilename}" WORKING_DIRECTORY ${TEST_DIR})
+    if (Boost_FOUND)
+      target_link_libraries(${testfilename} ${Boost_SYSTEM_LIBRARY})
+      target_link_libraries(${testfilename} ${Boost_FILESYSTEM_LIBRARY})
+    endif()
 ENDFOREACH()
 message("-- Finished building ${UNIT_TEST_COUNT} NanoFi unit test file(s)...")
 endif(NOT WIN32)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 2b45fc9..746b56c 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -39,7 +39,6 @@
   wget \
   gdb \
   musl-dev \
-  boost-dev \
   vim \
   util-linux-dev \
   curl-dev \
diff --git a/docker/test/integration/minifi/__init__.py b/docker/test/integration/minifi/__init__.py
index 631ab61..18efddd 100644
--- a/docker/test/integration/minifi/__init__.py
+++ b/docker/test/integration/minifi/__init__.py
@@ -552,13 +552,13 @@
 class PutFile(Processor):
     def __init__(self, output_dir, schedule={'scheduling strategy': 'EVENT_DRIVEN'}):
         super(PutFile, self).__init__('PutFile',
-                                      properties={'Directory': output_dir},
+                                      properties={'Directory': output_dir, 'Directory Permissions': '777', 'Permissions': '777'},
                                       auto_terminate=['success', 'failure'],
                                       schedule=schedule)
 
     def nifi_property_key(self, key):
-        if key == 'Output Directory':
-            return 'Directory'
+        if key == 'Directory Permissions':
+            return None
         else:
             return key
 
@@ -914,6 +914,8 @@
             proc_property = Element('property')
             proc_property_name = Element('name')
             proc_property_name.text = connectable.nifi_property_key(property_key)
+            if not proc_property_name.text:
+                continue
             proc_property.append(proc_property_name)
             proc_property_value = Element('value')
             proc_property_value.text = property_value
diff --git a/docker/test/integration/minifi/test/__init__.py b/docker/test/integration/minifi/test/__init__.py
index a9591b5..6dbef12 100644
--- a/docker/test/integration/minifi/test/__init__.py
+++ b/docker/test/integration/minifi/test/__init__.py
@@ -305,8 +305,6 @@
         self.valid = False
         full_dir = os.path.join(self.output_dir, self.subdir)
         logging.info("Output folder: %s", full_dir)
-        if "GITHUB_WORKSPACE" in os.environ:
-            subprocess.call(['sudo', 'chmod', '-R', '0777', full_dir])
 
         if not os.path.isdir(full_dir):
             return self.valid
@@ -345,9 +343,6 @@
 
         full_dir = self.output_dir + dir
         logging.info("Output folder: %s", full_dir)
-        if "GITHUB_WORKSPACE" in os.environ:
-            subprocess.call(['sudo', 'chmod', '-R', '0777', full_dir])
-
         listing = listdir(full_dir)
         if listing:
             self.valid = all(os.path.getsize(os.path.join(full_dir,x)) == 0 for x in listing)
@@ -368,9 +363,6 @@
 
         full_dir = self.output_dir + dir
         logging.info("Output folder: %s", full_dir)
-        if "GITHUB_WORKSPACE" in os.environ:
-            subprocess.call(['sudo', 'chmod', '-R', '0777', full_dir])
-
         listing = listdir(full_dir)
 
         self.valid = not bool(listing)
diff --git a/extensions/standard-processors/processors/PutFile.cpp b/extensions/standard-processors/processors/PutFile.cpp
index bfc9490..ddf9146 100644
--- a/extensions/standard-processors/processors/PutFile.cpp
+++ b/extensions/standard-processors/processors/PutFile.cpp
@@ -54,6 +54,19 @@
 core::Property PutFile::MaxDestFiles(
     core::PropertyBuilder::createProperty("Maximum File Count")->withDescription("Specifies the maximum number of files that can exist in the output directory")->withDefaultValue<int>(-1)->build());
 
+#ifndef WIN32
+core::Property PutFile::Permissions(
+    core::PropertyBuilder::createProperty("Permissions")
+      ->withDescription("Sets the permissions on the output file to the value of this attribute. "
+                        "Must be an octal number (e.g. 644 or 0755). Not supported on Windows systems.")
+      ->build());
+core::Property PutFile::DirectoryPermissions(
+    core::PropertyBuilder::createProperty("Directory Permissions")
+      ->withDescription("Sets the permissions on the directories being created if 'Create Missing Directories' property is set. "
+                        "Must be an octal number (e.g. 644 or 0755). Not supported on Windows systems.")
+      ->build());
+#endif
+
 core::Relationship PutFile::Success("success", "All files are routed to success");
 core::Relationship PutFile::Failure("failure", "Failed files (conflict, write failure, etc.) are transferred to failure");
 
@@ -64,6 +77,10 @@
   properties.insert(ConflictResolution);
   properties.insert(CreateDirs);
   properties.insert(MaxDestFiles);
+#ifndef WIN32
+  properties.insert(Permissions);
+  properties.insert(DirectoryPermissions);
+#endif
   setSupportedProperties(properties);
   // Set the supported relationships
   std::set<core::Relationship> relationships;
@@ -84,6 +101,11 @@
   if (context->getProperty(MaxDestFiles.getName(), value)) {
     core::Property::StringToInt(value, max_dest_files_);
   }
+
+#ifndef WIN32
+  getPermissions(context);
+  getDirectoryPermissions(context);
+#endif
 }
 
 void PutFile::onTrigger(core::ProcessContext *context, core::ProcessSession *session) {
@@ -199,7 +221,15 @@
 
       if (!dir_path_component.empty()) {
         logger_->log_debug("Attempting to create directory if it does not already exist: %s", dir_path);
-        utils::file::FileUtils::create_dir(dir_path);
+        if (!utils::file::FileUtils::exists(dir_path)) {
+          utils::file::FileUtils::create_dir(dir_path, false);
+#ifndef WIN32
+          if (directory_permissions_.valid()) {
+            utils::file::FileUtils::set_permissions(dir_path, directory_permissions_.getValue());
+          }
+#endif
+        }
+
         dir_path_stream << utils::file::FileUtils::get_separator();
       } else if (pos == 0) {
         // Support absolute paths
@@ -227,6 +257,12 @@
     }
   }
 
+#ifndef WIN32
+  if (permissions_.valid()) {
+    utils::file::FileUtils::set_permissions(destFile, permissions_.getValue());
+  }
+#endif
+
   if (success) {
     session->transfer(flowFile, Success);
     return true;
@@ -236,6 +272,44 @@
   return false;
 }
 
+#ifndef WIN32
+void PutFile::getPermissions(core::ProcessContext *context) {
+  std::string permissions_str;
+  context->getProperty(Permissions.getName(), permissions_str);
+  if (permissions_str.empty()) {
+    return;
+  }
+
+  try {
+    permissions_.setValue(std::stoi(permissions_str, 0, 8));
+  } catch(const std::exception&) {
+    throw Exception(PROCESS_SCHEDULE_EXCEPTION, "Permissions property is invalid");
+  }
+
+  if (!permissions_.valid()) {
+    throw Exception(PROCESS_SCHEDULE_EXCEPTION, "Permissions property is invalid: out of bounds");
+  }
+}
+
+void PutFile::getDirectoryPermissions(core::ProcessContext *context) {
+  std::string dir_permissions_str;
+  context->getProperty(DirectoryPermissions.getName(), dir_permissions_str);
+  if (dir_permissions_str.empty()) {
+    return;
+  }
+
+  try {
+    directory_permissions_.setValue(std::stoi(dir_permissions_str, 0, 8));
+  } catch(const std::exception&) {
+    throw Exception(PROCESS_SCHEDULE_EXCEPTION, "Directory Permissions property is invalid");
+  }
+
+  if (!directory_permissions_.valid()) {
+    throw Exception(PROCESS_SCHEDULE_EXCEPTION, "Directory Permissions property is invalid: out of bounds");
+  }
+}
+#endif
+
 PutFile::ReadCallback::ReadCallback(const std::string &tmp_file, const std::string &dest_file)
     : tmp_file_(tmp_file),
       dest_file_(dest_file) {
diff --git a/extensions/standard-processors/processors/PutFile.h b/extensions/standard-processors/processors/PutFile.h
index f938483..acba760 100644
--- a/extensions/standard-processors/processors/PutFile.h
+++ b/extensions/standard-processors/processors/PutFile.h
@@ -61,6 +61,10 @@
   static core::Property ConflictResolution;
   static core::Property CreateDirs;
   static core::Property MaxDestFiles;
+#ifndef WIN32
+  static core::Property Permissions;
+  static core::Property DirectoryPermissions;
+#endif
   // Supported Relationships
   static core::Relationship Success;
   static core::Relationship Failure;
@@ -110,6 +114,22 @@
                const std::string &destDir);
   std::shared_ptr<logging::Logger> logger_;
   static std::shared_ptr<utils::IdGenerator> id_generator_;
+
+#ifndef WIN32
+  class FilePermissions {
+    static const uint32_t MINIMUM_INVALID_PERMISSIONS_VALUE = 1 << 9;
+   public:
+    bool valid() { return permissions_ < MINIMUM_INVALID_PERMISSIONS_VALUE; }
+    uint32_t getValue() const { return permissions_; }
+    void setValue(uint32_t perms) { permissions_ = perms; }
+   private:
+    uint32_t permissions_ = MINIMUM_INVALID_PERMISSIONS_VALUE;
+  };
+  FilePermissions permissions_;
+  FilePermissions directory_permissions_;
+  void getPermissions(core::ProcessContext *context);
+  void getDirectoryPermissions(core::ProcessContext *context);
+#endif
 };
 
 REGISTER_RESOURCE(PutFile, "Writes the contents of a FlowFile to the local file system");
diff --git a/extensions/standard-processors/processors/TailFile.cpp b/extensions/standard-processors/processors/TailFile.cpp
index 547184a..3f10859 100644
--- a/extensions/standard-processors/processors/TailFile.cpp
+++ b/extensions/standard-processors/processors/TailFile.cpp
@@ -358,7 +358,7 @@
       throw minifi::Exception(ExceptionType::PROCESSOR_EXCEPTION, "Base directory is required for multiple tail mode.");
     }
 
-    if (utils::file::FileUtils::is_directory(base_dir_.c_str()) == 0) {
+    if (!utils::file::FileUtils::is_directory(base_dir_.c_str())) {
       throw minifi::Exception(ExceptionType::PROCESSOR_EXCEPTION, "Base directory does not exist or is not a directory");
     }
 
diff --git a/extensions/standard-processors/tests/unit/PutFileTests.cpp b/extensions/standard-processors/tests/unit/PutFileTests.cpp
index fdf5a30..2d422e2 100644
--- a/extensions/standard-processors/tests/unit/PutFileTests.cpp
+++ b/extensions/standard-processors/tests/unit/PutFileTests.cpp
@@ -427,3 +427,44 @@
   is.seekg(0, is.end);
   REQUIRE(is.tellg() == 0);
 }
+
+#ifndef WIN32
+TEST_CASE("TestPutFilePermissions", "[PutFilePermissions]") {
+  TestController testController;
+
+  LogTestController::getInstance().setDebug<minifi::processors::GetFile>();
+  LogTestController::getInstance().setDebug<TestPlan>();
+  LogTestController::getInstance().setDebug<minifi::processors::PutFile>();
+
+  std::shared_ptr<TestPlan> plan = testController.createPlan();
+
+  std::shared_ptr<core::Processor> getfile = plan->addProcessor("GetFile", "getfileCreate2");
+
+  std::shared_ptr<core::Processor> putfile = plan->addProcessor("PutFile", "putfile", core::Relationship("success", "description"), true);
+
+  char format[] = "/tmp/gt.XXXXXX";
+  auto dir = testController.createTempDirectory(format);
+  char format2[] = "/tmp/ft.XXXXXX";
+  auto putfiledir = testController.createTempDirectory(format2) + utils::file::FileUtils::get_separator() + "test_dir";
+
+  plan->setProperty(getfile, org::apache::nifi::minifi::processors::GetFile::Directory.getName(), dir);
+  plan->setProperty(putfile, org::apache::nifi::minifi::processors::PutFile::Directory.getName(), putfiledir);
+  plan->setProperty(putfile, org::apache::nifi::minifi::processors::PutFile::Permissions.getName(), "644");
+  plan->setProperty(putfile, org::apache::nifi::minifi::processors::PutFile::DirectoryPermissions.getName(), "0777");
+
+  std::fstream file;
+  file.open(std::string(dir) + utils::file::FileUtils::get_separator() + "tstFile.ext", std::ios::out);
+  file << "tempFile";
+  file.close();
+
+  plan->runNextProcessor();  // Get
+  plan->runNextProcessor();  // Put
+
+  auto path = std::string(putfiledir) + utils::file::FileUtils::get_separator() + "tstFile.ext";
+  uint32_t perms = 0;
+  REQUIRE(utils::file::FileUtils::get_permissions(path, perms));
+  REQUIRE(perms == 0644);
+  REQUIRE(utils::file::FileUtils::get_permissions(putfiledir, perms));
+  REQUIRE(perms == 0777);
+}
+#endif
diff --git a/libminifi/CMakeLists.txt b/libminifi/CMakeLists.txt
index 464e71e..30d9499 100644
--- a/libminifi/CMakeLists.txt
+++ b/libminifi/CMakeLists.txt
@@ -113,6 +113,15 @@
         target_include_directories(core-minifi PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/include")
 endif()
 
+if(NOT EXCLUDE_BOOST)
+  find_package(Boost COMPONENTS system filesystem)
+  if(Boost_FOUND)
+    add_definitions(-DUSE_BOOST)
+    target_include_directories(core-minifi PRIVATE "${Boost_INCLUDE_DIRS}")
+    list(APPEND LIBMINIFI_LIBRARIES ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY})
+  endif()
+endif()
+
 list(APPEND LIBMINIFI_LIBRARIES yaml-cpp ZLIB::ZLIB concurrentqueue RapidJSON spdlog cron Threads::Threads gsl-lite optional-lite libsodium)
 if(NOT WIN32)
 	list(APPEND LIBMINIFI_LIBRARIES OSSP::libuuid++)
diff --git a/libminifi/include/utils/file/DiffUtils.h b/libminifi/include/utils/file/DiffUtils.h
index dbf1a58..be0733c 100644
--- a/libminifi/include/utils/file/DiffUtils.h
+++ b/libminifi/include/utils/file/DiffUtils.h
@@ -20,7 +20,7 @@
 #include <fstream>
 #include <sstream>
 
-#ifdef BOOST_VERSION
+#ifdef USE_BOOST
 #include <boost/filesystem.hpp>
 
 #else
diff --git a/libminifi/include/utils/file/FileManager.h b/libminifi/include/utils/file/FileManager.h
index 79cef44..f11e8bc 100644
--- a/libminifi/include/utils/file/FileManager.h
+++ b/libminifi/include/utils/file/FileManager.h
@@ -20,7 +20,7 @@
 #include <string>
 #include <vector>
 
-#ifdef BOOST_VERSION
+#ifdef USE_BOOST
 #include <boost/filesystem.hpp>
 
 #else
@@ -79,11 +79,11 @@
   }
 
   std::string unique_file(bool keep = false) {
-#ifdef BOOST_VERSION
+#ifdef USE_BOOST
     return boost::filesystem::unique_path().native();
-#else  // BOOST_VERSION
+#else  // USE_BOOST
     return unique_file(std::string{}, keep);
-#endif  // BOOST_VERSION
+#endif  // USE_BOOST
   }
 
 
diff --git a/libminifi/include/utils/file/FileUtils.h b/libminifi/include/utils/file/FileUtils.h
index be022eb..632eddb 100644
--- a/libminifi/include/utils/file/FileUtils.h
+++ b/libminifi/include/utils/file/FileUtils.h
@@ -24,8 +24,10 @@
 #include <utility>
 #include <vector>
 
-#ifdef BOOST_VERSION
+#ifdef USE_BOOST
+#include <dirent.h>
 #include <boost/filesystem.hpp>
+#include <boost/system/error_code.hpp>
 
 #else
 #include <errno.h>
@@ -100,7 +102,7 @@
 #ifdef WIN32
   return _mkdir(path.c_str());
 #else
-  return mkdir(path.c_str(), 0700);
+  return mkdir(path.c_str(), 0777);
 #endif
 }
 }  // namespace detail
@@ -141,7 +143,7 @@
 }
 
 inline int64_t delete_dir(const std::string &path, bool delete_files_recursively = true) {
-#ifdef BOOST_VERSION
+#ifdef USE_BOOST
   try {
     if (boost::filesystem::exists(path)) {
       if (delete_files_recursively) {
@@ -228,8 +230,12 @@
 }
 
 inline uint64_t last_write_time(const std::string &path) {
-#ifdef BOOST_VERSION
-  return boost::filesystem::last_write_time(movedFile.str());
+#ifdef USE_BOOST
+  boost::system::error_code ec;
+  auto result = boost::filesystem::last_write_time(path, ec);
+  if (ec.value() == 0) {
+    return result;
+  }
 #else
 #ifdef WIN32
   struct _stat result;
@@ -266,7 +272,10 @@
 }
 
 inline bool set_last_write_time(const std::string &path, uint64_t write_time) {
-#ifdef WIN32
+#ifdef USE_BOOST
+  boost::filesystem::last_write_time(path, write_time);
+  return true;
+#elif defined(WIN32)
   struct __utimbuf64 utim;
   utim.actime = write_time;
   utim.modtime = write_time;
@@ -288,6 +297,16 @@
   }
   return false;
 }
+
+inline int set_permissions(const std::string &path, const uint32_t permissions) {
+#ifdef USE_BOOST
+  boost::system::error_code ec;
+  boost::filesystem::permissions(path, static_cast<boost::filesystem::perms>(permissions), ec);
+  return ec.value();
+#else
+  return chmod(path.c_str(), permissions);
+#endif
+}
 #endif
 
 #ifndef WIN32
@@ -302,56 +321,75 @@
 }
 #endif
 
-inline int is_directory(const char * path) {
-    struct stat dir_stat;
-    if (stat(path, &dir_stat) < 0) {
-        return 0;
-    }
-    return S_ISDIR(dir_stat.st_mode);
+inline bool is_directory(const char * path) {
+  struct stat dir_stat;
+  if (stat(path, &dir_stat) < 0) {
+      return false;
+  }
+  return S_ISDIR(dir_stat.st_mode) != 0;
+}
+
+inline bool exists(const std::string& path) {
+#ifdef USE_BOOST
+  return boost::filesystem::exists(path);
+#else
+#ifdef WIN32
+  struct _stat statbuf;
+  return _stat(path.c_str(), &statbuf) == 0;
+#else
+  struct stat statbuf;
+  return stat(path.c_str(), &statbuf) == 0;
+#endif
+#endif
 }
 
 inline int create_dir(const std::string& path, bool recursive = true) {
-#ifdef BOOST_VERSION
+#ifdef USE_BOOST
   boost::filesystem::path dir(path);
-  if (boost::filesystem::create_directory(dir)) {
-    return 0;
+  boost::system::error_code ec;
+  if (!recursive) {
+    boost::filesystem::create_directory(dir, ec);
   } else {
-    return -1;
+    boost::filesystem::create_directories(dir, ec);
   }
+  if (ec.value() == 0 || (ec.value() == EEXIST && is_directory(path.c_str()))) {
+    return 0;
+  }
+  return ec.value();
 #else
   if (!recursive) {
-      if (detail::platform_create_dir(path) != 0 && errno != EEXIST) {
-          return -1;
-      }
-      return 0;
+    if (detail::platform_create_dir(path) != 0 && errno != EEXIST) {
+      return -1;
+    }
+    return 0;
   }
   if (detail::platform_create_dir(path) == 0) {
-      return 0;
+    return 0;
   }
 
   switch (errno) {
   case ENOENT: {
-      size_t found = path.find_last_of(get_separator());
+    size_t found = path.find_last_of(get_separator());
 
-      if (found == std::string::npos) {
-          return -1;
-      }
+    if (found == std::string::npos) {
+      return -1;
+    }
 
-      const std::string dir = path.substr(0, found);
-      int res = create_dir(dir);
-      if (res < 0) {
-          return -1;
-      }
-      return detail::platform_create_dir(path);
+    const std::string dir = path.substr(0, found);
+    int res = create_dir(dir, recursive);
+    if (res < 0) {
+      return -1;
+    }
+    return detail::platform_create_dir(path);
   }
   case EEXIST: {
-      if (is_directory(path.c_str())) {
-          return 0;
-      }
-      return -1;
+    if (is_directory(path.c_str())) {
+      return 0;
+    }
+    return -1;
   }
   default:
-      return -1;
+    return -1;
   }
   return -1;
 #endif
diff --git a/libminifi/test/unit/FileUtilsTests.cpp b/libminifi/test/unit/FileUtilsTests.cpp
index 3060299..1afcd48 100644
--- a/libminifi/test/unit/FileUtilsTests.cpp
+++ b/libminifi/test/unit/FileUtilsTests.cpp
@@ -145,9 +145,29 @@
 
   std::string test_dir_path = std::string(dir) + FileUtils::get_separator() + "random_dir";
 
+  REQUIRE(FileUtils::create_dir(test_dir_path, false) == 0);  // Dir has to be created successfully
+  struct stat buffer;
+  REQUIRE(stat(test_dir_path.c_str(), &buffer) == 0);  // Check if directory exists
+  REQUIRE(FileUtils::create_dir(test_dir_path, false) == 0);  // Dir already exists, success should be returned
+  REQUIRE(FileUtils::delete_dir(test_dir_path, false) == 0);  // Delete should be successful as well
+  test_dir_path += "/random_dir2";
+  REQUIRE(FileUtils::create_dir(test_dir_path, false) != 0);  // Create dir should fail for multiple directories if recursive option is not set
+}
+
+TEST_CASE("TestFileUtils::create_dir recursively", "[TestCreateDir]") {
+  TestController testController;
+
+  char format[] = "/tmp/gt.XXXXXX";
+  auto dir = testController.createTempDirectory(format);
+
+  std::string test_dir_path = std::string(dir) + FileUtils::get_separator() + "random_dir" + FileUtils::get_separator() +
+    "random_dir2" + FileUtils::get_separator() + "random_dir3";
+
   REQUIRE(FileUtils::create_dir(test_dir_path) == 0);  // Dir has to be created successfully
+  struct stat buffer;
+  REQUIRE(stat(test_dir_path.c_str(), &buffer) == 0);  // Check if directory exists
   REQUIRE(FileUtils::create_dir(test_dir_path) == 0);  // Dir already exists, success should be returned
-  REQUIRE(FileUtils::delete_dir(test_dir_path) == 0);  // Delete should be successful as welll
+  REQUIRE(FileUtils::delete_dir(test_dir_path) == 0);  // Delete should be successful as well
 }
 
 TEST_CASE("TestFileUtils::getFullPath", "[TestGetFullPath]") {
@@ -351,3 +371,32 @@
   REQUIRE(FileUtils::computeChecksum(another_file, 8192) == CHECKSUM_OF_8192_BYTES);
   REQUIRE(FileUtils::computeChecksum(another_file, 9000) == CHECKSUM_OF_8192_BYTES);
 }
+
+#ifndef WIN32
+TEST_CASE("FileUtils::set_permissions", "[TestSetPermissions]") {
+  TestController testController;
+
+  char format[] = "/tmp/gt.XXXXXX";
+  auto dir = testController.createTempDirectory(format);
+  auto path = dir + FileUtils::get_separator() + "test_file.txt";
+  std::ofstream outfile(path, std::ios::out | std::ios::binary);
+
+  REQUIRE(FileUtils::set_permissions(path, 0644) == 0);
+  uint32_t perms;
+  REQUIRE(FileUtils::get_permissions(path, perms));
+  REQUIRE(perms == 0644);
+}
+#endif
+
+TEST_CASE("FileUtils::exists", "[TestExists]") {
+  TestController testController;
+
+  char format[] = "/tmp/gt.XXXXXX";
+  auto dir = testController.createTempDirectory(format);
+  auto path = dir + FileUtils::get_separator() + "test_file.txt";
+  std::ofstream outfile(path, std::ios::out | std::ios::binary);
+  auto invalid_path = dir + FileUtils::get_separator() + "test_file2.txt";
+
+  REQUIRE(FileUtils::exists(path));
+  REQUIRE(!FileUtils::exists(invalid_path));
+}