changeset 77:80792bb9600e

new HybridMode
author Alain Mazy <am@osimis.io>
date Fri, 14 Oct 2022 10:36:02 +0200
parents ac596874d997
children d7295e8678d7
files Aws/AwsS3StoragePlugin.cpp Aws/AwsS3StoragePlugin.h Aws/CMakeLists.txt Azure/CMakeLists.txt Common/BaseStoragePlugin.cpp Common/BaseStoragePlugin.h Common/FileSystemStoragePlugin.cpp Common/FileSystemStoragePlugin.h Common/IStoragePlugin.h Common/StoragePlugin.cpp Google/CMakeLists.txt Google/GoogleStoragePlugin.cpp NEWS README.md
diffstat 14 files changed, 551 insertions(+), 141 deletions(-) [+]
line wrap: on
line diff
--- a/Aws/AwsS3StoragePlugin.cpp	Tue Aug 30 14:59:58 2022 +0200
+++ b/Aws/AwsS3StoragePlugin.cpp	Fri Oct 14 10:36:02 2022 +0200
@@ -34,7 +34,6 @@
 #include <fstream>
 
 const char* ALLOCATION_TAG = "OrthancS3";
-static const char* const PLUGIN_SECTION = "AwsS3Storage";
 
 class AwsS3StoragePlugin : public BaseStoragePlugin
 {
@@ -46,14 +45,13 @@
 
 public:
 
-  AwsS3StoragePlugin(const Aws::S3::S3Client& client, const std::string& bucketName, bool enableLegacyStorageStructure, bool storageContainsUnknownFiles);
+  AwsS3StoragePlugin(const std::string& nameForLogs,  const Aws::S3::S3Client& client, const std::string& bucketName, bool enableLegacyStorageStructure, bool storageContainsUnknownFiles);
 
   virtual ~AwsS3StoragePlugin();
 
   virtual IWriter* GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
   virtual IReader* GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
   virtual void DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
-  virtual const char* GetConfigurationSectionName() {return PLUGIN_SECTION;};
 };
 
 
@@ -260,7 +258,7 @@
 static std::unique_ptr<Aws::SDKOptions>  sdkOptions_;
 
 
-IStoragePlugin* AwsS3StoragePluginFactory::CreateStoragePlugin(const OrthancPlugins::OrthancConfiguration& orthancConfig)
+IStoragePlugin* AwsS3StoragePluginFactory::CreateStoragePlugin(const std::string& nameForLogs, const OrthancPlugins::OrthancConfiguration& orthancConfig)
 {
   if (sdkOptions_.get() != NULL)
   {
@@ -278,14 +276,14 @@
   bool enableLegacyStorageStructure;
   bool storageContainsUnknownFiles;
 
-  if (!orthancConfig.IsSection(PLUGIN_SECTION))
+  if (!orthancConfig.IsSection(GetConfigurationSectionName()))
   {
     OrthancPlugins::LogWarning(std::string(GetStoragePluginName()) + " plugin, section missing.  Plugin is not enabled.");
     return nullptr;
   }
 
   OrthancPlugins::OrthancConfiguration pluginSection;
-  orthancConfig.GetSection(pluginSection, PLUGIN_SECTION);
+  orthancConfig.GetSection(pluginSection, GetConfigurationSectionName());
 
   if (!BaseStoragePlugin::ReadCommonConfiguration(enableLegacyStorageStructure, storageContainsUnknownFiles, pluginSection))
   {
@@ -347,7 +345,7 @@
       
       OrthancPlugins::LogInfo("AWS S3 storage initialized");
 
-      return new AwsS3StoragePlugin(client, bucketName, enableLegacyStorageStructure, storageContainsUnknownFiles);
+      return new AwsS3StoragePlugin(nameForLogs, client, bucketName, enableLegacyStorageStructure, storageContainsUnknownFiles);
     } 
     else
     {
@@ -357,7 +355,7 @@
 
       OrthancPlugins::LogInfo("AWS S3 storage initialized");
 
-      return new AwsS3StoragePlugin(client, bucketName, enableLegacyStorageStructure, storageContainsUnknownFiles);
+      return new AwsS3StoragePlugin(nameForLogs, client, bucketName, enableLegacyStorageStructure, storageContainsUnknownFiles);
     }  
   }
   catch (const std::exception& e)
@@ -377,8 +375,8 @@
 }
 
 
-AwsS3StoragePlugin::AwsS3StoragePlugin(const Aws::S3::S3Client& client, const std::string& bucketName, bool enableLegacyStorageStructure, bool storageContainsUnknownFiles)
-  : BaseStoragePlugin(enableLegacyStorageStructure),
+AwsS3StoragePlugin::AwsS3StoragePlugin(const std::string& nameForLogs, const Aws::S3::S3Client& client, const std::string& bucketName, bool enableLegacyStorageStructure, bool storageContainsUnknownFiles)
+  : BaseStoragePlugin(nameForLogs, enableLegacyStorageStructure),
     client_(client),
     bucketName_(bucketName),
     storageContainsUnknownFiles_(storageContainsUnknownFiles)
--- a/Aws/AwsS3StoragePlugin.h	Tue Aug 30 14:59:58 2022 +0200
+++ b/Aws/AwsS3StoragePlugin.h	Fri Oct 14 10:36:02 2022 +0200
@@ -24,6 +24,7 @@
 {
 public:
   static const char* GetStoragePluginName();
-  static IStoragePlugin* CreateStoragePlugin(const OrthancPlugins::OrthancConfiguration& orthancConfig);
+  static IStoragePlugin* CreateStoragePlugin(const std::string& nameForLogs, const OrthancPlugins::OrthancConfiguration& orthancConfig);
+  static const char* GetConfigurationSectionName() {return "AwsS3Storage";}
 };
 
--- a/Aws/CMakeLists.txt	Tue Aug 30 14:59:58 2022 +0200
+++ b/Aws/CMakeLists.txt	Fri Oct 14 10:36:02 2022 +0200
@@ -129,6 +129,8 @@
   ${CMAKE_SOURCE_DIR}/../Common/EncryptionHelpers.h
   ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.cpp
   ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.h
+  ${CMAKE_SOURCE_DIR}/../Common/FileSystemStoragePlugin.h
+  ${CMAKE_SOURCE_DIR}/../Common/FileSystemStoragePlugin.cpp
   ${CMAKE_SOURCE_DIR}/../Common/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
 
   ${AWS_SOURCES}
--- a/Azure/CMakeLists.txt	Tue Aug 30 14:59:58 2022 +0200
+++ b/Azure/CMakeLists.txt	Fri Oct 14 10:36:02 2022 +0200
@@ -93,6 +93,8 @@
     ${CMAKE_SOURCE_DIR}/../Common/EncryptionHelpers.h
     ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.cpp
     ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.h
+    ${CMAKE_SOURCE_DIR}/../Common/FileSystemStoragePlugin.h
+    ${CMAKE_SOURCE_DIR}/../Common/FileSystemStoragePlugin.cpp
     ${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
 
     ${ORTHANC_CORE_SOURCES}
--- a/Common/BaseStoragePlugin.cpp	Tue Aug 30 14:59:58 2022 +0200
+++ b/Common/BaseStoragePlugin.cpp	Fri Oct 14 10:36:02 2022 +0200
@@ -20,7 +20,7 @@
 #include "BaseStoragePlugin.h"
 #include <boost/filesystem/fstream.hpp>
 
-std::string BaseStoragePlugin::GetOrthancFileSystemPath(const std::string& uuid, const std::string& fileSystemRootPath)
+boost::filesystem::path BaseStoragePlugin::GetOrthancFileSystemPath(const std::string& uuid, const std::string& fileSystemRootPath)
 {
   boost::filesystem::path path = fileSystemRootPath;
 
@@ -30,7 +30,7 @@
 
   path.make_preferred();
 
-  return path.string();
+  return path;
 }
 
 
@@ -38,7 +38,7 @@
 {
   if (enableLegacyStorageStructure_)
   {
-    return GetOrthancFileSystemPath(uuid, rootPath_);
+    return GetOrthancFileSystemPath(uuid, rootPath_).string();
   }
   else
   {
--- a/Common/BaseStoragePlugin.h	Tue Aug 30 14:59:58 2022 +0200
+++ b/Common/BaseStoragePlugin.h	Fri Oct 14 10:36:02 2022 +0200
@@ -20,6 +20,9 @@
 #pragma once
 
 #include "IStoragePlugin.h"
+#include <boost/filesystem.hpp>
+
+namespace fs = boost::filesystem;
 
 class BaseStoragePlugin : public IStoragePlugin
 {
@@ -28,7 +31,8 @@
 
 protected:
 
-  BaseStoragePlugin(bool enableLegacyStorageStructure):
+  BaseStoragePlugin(const std::string& nameForLogs, bool enableLegacyStorageStructure):
+    IStoragePlugin(nameForLogs),
     enableLegacyStorageStructure_(enableLegacyStorageStructure)
   {}
 
@@ -41,7 +45,7 @@
   }
 
   static std::string GetPath(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled, bool legacyFileStructure, const std::string& rootFolder);
-  static std::string GetOrthancFileSystemPath(const std::string& uuid, const std::string& fileSystemRootPath);
+  static fs::path GetOrthancFileSystemPath(const std::string& uuid, const std::string& fileSystemRootPath);
 
   static bool ReadCommonConfiguration(bool& enableLegacyStorageStructure, bool& storageContainsUnknownFiles, const OrthancPlugins::OrthancConfiguration& pluginSection);
 };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Common/FileSystemStoragePlugin.cpp	Fri Oct 14 10:36:02 2022 +0200
@@ -0,0 +1,149 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2020-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "FileSystemStoragePlugin.h"
+#include "BaseStoragePlugin.h"
+
+#include <SystemToolbox.h>
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+
+namespace fs = boost::filesystem;
+
+void FileSystemStoragePlugin::FileSystemWriter::Write(const char* data, size_t size)
+{
+  Orthanc::SystemToolbox::MakeDirectory(path_.parent_path().string());
+
+  Orthanc::SystemToolbox::WriteFile(reinterpret_cast<const void*>(data), size, path_.string(), fsync_);
+}
+
+size_t FileSystemStoragePlugin::FileSystemReader::GetSize()
+{
+  if (!Orthanc::SystemToolbox::IsRegularFile(path_.string()))
+  {
+    throw StoragePluginException(std::string("The path does not point to a regular file: ") + path_.string());
+  }
+
+  try
+  {
+    fs::ifstream f;
+    f.open(path_.string(), std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      throw StoragePluginException(std::string("The path does not point to a regular file: ") + path_.string());
+    }
+
+    f.seekg(0, std::ios::end);
+    std::streamsize fileSize = f.tellg();
+    f.seekg(0, std::ios::beg);
+
+    f.close();
+
+    return fileSize;
+  }
+  catch (...)
+  {
+    throw StoragePluginException(std::string("Unexpected error while reading: ") + path_.string());
+  }
+
+}
+
+void FileSystemStoragePlugin::FileSystemReader::ReadWhole(char* data, size_t size)
+{
+  ReadRange(data, size, 0);
+}
+
+void FileSystemStoragePlugin::FileSystemReader::ReadRange(char* data, size_t size, size_t fromOffset)
+{
+  if (!Orthanc::SystemToolbox::IsRegularFile(path_.string()))
+  {
+    throw StoragePluginException(std::string("The path does not point to a regular file: ") + path_.string());
+  }
+
+  try
+  {
+    fs::ifstream f;
+    f.open(path_.string(), std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      throw StoragePluginException(std::string("The path does not point to a regular file: ") + path_.string());
+    }
+
+    f.seekg(fromOffset, std::ios::beg);
+
+    f.read(reinterpret_cast<char*>(data), size);
+
+    f.close();
+  }
+  catch (...)
+  {
+    throw StoragePluginException(std::string("Unexpected error while reading: ") + path_.string());
+  }
+}
+
+
+
+IStoragePlugin::IWriter* FileSystemStoragePlugin::GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  return new FileSystemWriter(BaseStoragePlugin::GetOrthancFileSystemPath(uuid, fileSystemRootPath_), fsync_);
+}
+
+IStoragePlugin::IReader* FileSystemStoragePlugin::GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  return new FileSystemReader(BaseStoragePlugin::GetOrthancFileSystemPath(uuid, fileSystemRootPath_));
+}
+
+void FileSystemStoragePlugin::DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+{
+  try
+  {
+    namespace fs = boost::filesystem;
+
+    fs::path path = BaseStoragePlugin::GetOrthancFileSystemPath(uuid, fileSystemRootPath_);
+
+    try
+    {
+      fs::remove(path.string());
+    }
+    catch (...)
+    {
+      // Ignore the error
+    }
+
+    // Remove the two parent directories, ignoring the error code if
+    // these directories are not empty
+
+    try
+    {
+      boost::system::error_code err;
+      fs::remove(path.parent_path().string(), err);
+      fs::remove(path.parent_path().parent_path().string(), err);
+    }
+    catch (...)
+    {
+      // Ignore the error
+    }
+  }
+  catch(Orthanc::OrthancException& e)
+  {
+    OrthancPlugins::LogError(GetNameForLogs() + ": error while deleting object " + std::string(uuid) + ": " + std::string(e.What()));
+  }
+
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Common/FileSystemStoragePlugin.h	Fri Oct 14 10:36:02 2022 +0200
@@ -0,0 +1,73 @@
+/**
+ * Cloud storage plugins for Orthanc
+ * Copyright (C) 2020-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IStoragePlugin.h"
+#include <boost/filesystem.hpp>
+
+namespace fs = boost::filesystem;
+
+
+class FileSystemStoragePlugin: public IStoragePlugin
+{
+public:
+  class FileSystemWriter: public IStoragePlugin::IWriter
+  {
+    const fs::path path_;
+    bool fsync_;
+  public:
+    FileSystemWriter(const fs::path& path, bool fsync)
+    : path_(path),
+      fsync_(fsync)
+    {}
+
+    virtual ~FileSystemWriter() {}
+    virtual void Write(const char* data, size_t size);
+  };
+
+  class FileSystemReader: public IStoragePlugin::IReader
+  {
+    const fs::path path_;
+  public:
+    FileSystemReader(const fs::path& path)
+    : path_(path)
+    {}
+
+    virtual ~FileSystemReader() {}
+    virtual size_t GetSize();
+    virtual void ReadWhole(char* data, size_t size);
+    virtual void ReadRange(char* data, size_t size, size_t fromOffset);
+  };
+
+  std::string fileSystemRootPath_;
+  bool fsync_;
+public:
+  FileSystemStoragePlugin(const std::string& nameForLogs, const std::string& fileSystemRootPath, bool fsync)
+  : IStoragePlugin(nameForLogs),
+    fileSystemRootPath_(fileSystemRootPath),
+    fsync_(fsync)
+  {}
+
+  virtual void SetRootPath(const std::string& rootPath) {}
+
+  virtual IStoragePlugin::IWriter* GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+  virtual IStoragePlugin::IReader* GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+  virtual void DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+};
\ No newline at end of file
--- a/Common/IStoragePlugin.h	Tue Aug 30 14:59:58 2022 +0200
+++ b/Common/IStoragePlugin.h	Fri Oct 14 10:36:02 2022 +0200
@@ -67,11 +67,17 @@
     virtual void ReadRange(char* data, size_t size, size_t fromOffset) = 0;
   };
 
+  std::string nameForLogs_;
 public:
+  IStoragePlugin(const std::string& nameForLogs):
+    nameForLogs_(nameForLogs)
+  {}
+
   virtual void SetRootPath(const std::string& rootPath) = 0;
 
   virtual IWriter* GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled) = 0;
   virtual IReader* GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled) = 0;
-  virtual void DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled) = 0;
-  virtual const char* GetConfigurationSectionName() = 0;
+  virtual void DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled) = 0;  // returns true only if 100% sure that the file has been deleted, false otherwise
+
+  const std::string& GetNameForLogs() {return nameForLogs_;}
 };
--- a/Common/StoragePlugin.cpp	Tue Aug 30 14:59:58 2022 +0200
+++ b/Common/StoragePlugin.cpp	Fri Oct 14 10:36:02 2022 +0200
@@ -40,17 +40,41 @@
 
 #include "../Common/EncryptionHelpers.h"
 #include "../Common/EncryptionConfigurator.h"
+#include "../Common/FileSystemStoragePlugin.h"
 
 #include <Logging.h>
 #include <SystemToolbox.h>
 
-static std::unique_ptr<IStoragePlugin> plugin;
+static std::unique_ptr<IStoragePlugin> primaryPlugin;
+static std::unique_ptr<IStoragePlugin> secondaryPlugin;
 
 static std::unique_ptr<EncryptionHelpers> crypto;
 static bool cryptoEnabled = false;
 static std::string fileSystemRootPath;
-static bool migrationFromFileSystemEnabled = false;
 static std::string objectsRootPath;
+static std::string hybridModeNameForLogs = "";
+
+typedef enum 
+{
+  HybridMode_WriteToFileSystem,       // write to disk, try to read first from disk and then, from object-storage
+  HybridMode_WriteToObjectStorage,    // write to object storage, try to read first from object storage and then, from disk
+  HybridMode_Disabled                 // read and write only from/to object-storage
+} HybridMode;  
+
+static HybridMode hybridMode = HybridMode_Disabled;
+
+static bool IsReadFromDisk()
+{
+  return hybridMode != HybridMode_Disabled;
+}
+
+static bool IsHybridModeEnabled()
+{
+  return hybridMode != HybridMode_Disabled;
+}
+
+typedef void LogErrorFunction(const std::string& message);
+
 
 
 static OrthancPluginErrorCode StorageCreate(const char* uuid,
@@ -60,7 +84,8 @@
 {
   try
   {
-    std::unique_ptr<IStoragePlugin::IWriter> writer(plugin->GetWriterForObject(uuid, type, cryptoEnabled));
+    OrthancPlugins::LogInfo(primaryPlugin->GetNameForLogs() + ": creating attachment " + std::string(uuid) + " of type " + boost::lexical_cast<std::string>(type));
+    std::unique_ptr<IStoragePlugin::IWriter> writer(primaryPlugin->GetWriterForObject(uuid, type, cryptoEnabled));
 
     if (cryptoEnabled)
     {
@@ -72,7 +97,7 @@
       }
       catch (EncryptionException& ex)
       {
-        OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while encrypting object " + std::string(uuid) + ": " + ex.what());
+        OrthancPlugins::LogError(primaryPlugin->GetNameForLogs() + ": error while encrypting object " + std::string(uuid) + ": " + ex.what());
         return OrthancPluginErrorCode_StorageAreaPlugin;
       }
 
@@ -85,7 +110,7 @@
   }
   catch (StoragePluginException& ex)
   {
-    OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while creating object " + std::string(uuid) + ": " + ex.what());
+    OrthancPlugins::LogError(primaryPlugin->GetNameForLogs() + ": error while creating object " + std::string(uuid) + ": " + ex.what());
     return OrthancPluginErrorCode_StorageAreaPlugin;
   }
 
@@ -93,7 +118,9 @@
 }
 
 
-static OrthancPluginErrorCode StorageReadRange(OrthancPluginMemoryBuffer64* target, // Memory buffer where to store the content of the range.  The memory buffer is allocated and freed by Orthanc. The length of the range of interest corresponds to the size of this buffer.
+static OrthancPluginErrorCode StorageReadRange(IStoragePlugin* plugin,
+                                               LogErrorFunction logErrorFunction,
+                                               OrthancPluginMemoryBuffer64* target, // Memory buffer where to store the content of the range.  The memory buffer is allocated and freed by Orthanc. The length of the range of interest corresponds to the size of this buffer.
                                                const char* uuid,
                                                OrthancPluginContentType type,
                                                uint64_t rangeStart)
@@ -102,42 +129,55 @@
 
   try
   {
+    OrthancPlugins::LogInfo(plugin->GetNameForLogs() + ": reading range of attachment " + std::string(uuid) + " of type " + boost::lexical_cast<std::string>(type));
+    
     std::unique_ptr<IStoragePlugin::IReader> reader(plugin->GetReaderForObject(uuid, type, cryptoEnabled));
     reader->ReadRange(reinterpret_cast<char*>(target->data), target->size, rangeStart);
+    
+    return OrthancPluginErrorCode_Success;
   }
   catch (StoragePluginException& ex)
   {
-    if (migrationFromFileSystemEnabled)
-    {
-      try
-      {
-        OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ": " + ex.what() + ", will now try to read it from legacy orthanc storage");
-        std::string path = BaseStoragePlugin::GetOrthancFileSystemPath(uuid, fileSystemRootPath);
-
-        std::string stringBuffer;
-        Orthanc::SystemToolbox::ReadFileRange(stringBuffer, path, rangeStart, rangeStart + target->size, true);
+    logErrorFunction(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ": " + std::string(ex.what()));
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+}
 
-        memcpy(target->data, stringBuffer.data(), stringBuffer.size());
+static OrthancPluginErrorCode StorageReadRange(OrthancPluginMemoryBuffer64* target, // Memory buffer where to store the content of the range.  The memory buffer is allocated and freed by Orthanc. The length of the range of interest corresponds to the size of this buffer.
+                                               const char* uuid,
+                                               OrthancPluginContentType type,
+                                               uint64_t rangeStart)
+{
+  OrthancPluginErrorCode res = StorageReadRange(primaryPlugin.get(),
+                                                (IsHybridModeEnabled() ? OrthancPlugins::LogWarning : OrthancPlugins::LogError), // log errors as warning on first try
+                                                target,
+                                                uuid,
+                                                type,
+                                                rangeStart);
 
-        return OrthancPluginErrorCode_Success;
-      }
-      catch(Orthanc::OrthancException& e)
-      {
-        OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ": " + std::string(e.What()));
-        return OrthancPluginErrorCode_StorageAreaPlugin;
-      }
-    }
+  if (res != OrthancPluginErrorCode_Success && IsHybridModeEnabled())
+  {
+    res = StorageReadRange(secondaryPlugin.get(),
+                           OrthancPlugins::LogError, // log errors as errors on second try
+                           target,
+                           uuid,
+                           type,
+                           rangeStart);
   }
-  return OrthancPluginErrorCode_Success;
+  return res;
 }
 
 
-static OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target, // Memory buffer where to store the content of the file. It must be allocated by the plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it.
+
+static OrthancPluginErrorCode StorageReadWhole(IStoragePlugin* plugin,
+                                               LogErrorFunction logErrorFunction,
+                                               OrthancPluginMemoryBuffer64* target, // Memory buffer where to store the content of the file. It must be allocated by the plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it.
                                                const char* uuid,
                                                OrthancPluginContentType type)
 {
   try
   {
+    OrthancPlugins::LogInfo(plugin->GetNameForLogs() + ": reading whole attachment " + std::string(uuid) + " of type " + boost::lexical_cast<std::string>(type));
     std::unique_ptr<IStoragePlugin::IReader> reader(plugin->GetReaderForObject(uuid, type, cryptoEnabled));
 
     size_t fileSize = reader->GetSize();
@@ -154,13 +194,13 @@
 
     if (size <= 0)
     {
-      OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ", size of file is too small: " + boost::lexical_cast<std::string>(fileSize) + " bytes");
+      logErrorFunction(plugin->GetNameForLogs() + ": error while reading object " + std::string(uuid) + ", size of file is too small: " + boost::lexical_cast<std::string>(fileSize) + " bytes");
       return OrthancPluginErrorCode_StorageAreaPlugin;
     }
 
     if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, size) != OrthancPluginErrorCode_Success)
     {
-      OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ", cannot allocate memory of size " + boost::lexical_cast<std::string>(size) + " bytes");
+      logErrorFunction(plugin->GetNameForLogs() + ": error while reading object " + std::string(uuid) + ", cannot allocate memory of size " + boost::lexical_cast<std::string>(size) + " bytes");
       return OrthancPluginErrorCode_StorageAreaPlugin;
     }
 
@@ -175,7 +215,7 @@
       }
       catch (EncryptionException& ex)
       {
-        OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while decrypting object " + std::string(uuid) + ": " + ex.what());
+        logErrorFunction(plugin->GetNameForLogs() + ": error while decrypting object " + std::string(uuid) + ": " + ex.what());
         return OrthancPluginErrorCode_StorageAreaPlugin;
       }
     }
@@ -186,42 +226,34 @@
   }
   catch (StoragePluginException& ex)
   {
-    if (migrationFromFileSystemEnabled)
-    {
-      try
-      {
-        OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ": " + ex.what() + ", will now try to read it from legacy orthanc storage");
-        std::string path = BaseStoragePlugin::GetOrthancFileSystemPath(uuid, fileSystemRootPath);
-
-        std::string stringBuffer;
-        Orthanc::SystemToolbox::ReadFile(stringBuffer, path);
-
-        if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, stringBuffer.size()) != OrthancPluginErrorCode_Success)
-        {
-          OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ", cannot allocate memory of size " + boost::lexical_cast<std::string>(stringBuffer.size()) + " bytes");
-          return OrthancPluginErrorCode_StorageAreaPlugin;
-        }
-
-        memcpy(target->data, stringBuffer.data(), stringBuffer.size());
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch(Orthanc::OrthancException& e)
-      {
-        OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ": " + std::string(e.What()));
-        return OrthancPluginErrorCode_StorageAreaPlugin;
-      }
-    }
-    else
-    {
-      OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while reading object " + std::string(uuid) + ": " + ex.what());
-      return OrthancPluginErrorCode_StorageAreaPlugin;
-    }
+    logErrorFunction(plugin->GetNameForLogs() + ": error while decrypting object " + std::string(uuid) + ": " + ex.what());
+    return OrthancPluginErrorCode_StorageAreaPlugin;
   }
 
   return OrthancPluginErrorCode_Success;
 }
 
+static OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target, // Memory buffer where to store the content of the file. It must be allocated by the plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it.
+                                               const char* uuid,
+                                               OrthancPluginContentType type)
+{
+  OrthancPluginErrorCode res = StorageReadWhole(primaryPlugin.get(),
+                                                (IsHybridModeEnabled() ? OrthancPlugins::LogWarning : OrthancPlugins::LogError), // log errors as warning on first try
+                                                target,
+                                                uuid,
+                                                type);
+
+  if (res != OrthancPluginErrorCode_Success && IsHybridModeEnabled())
+  {
+    res = StorageReadWhole(secondaryPlugin.get(),
+                           OrthancPlugins::LogError, // log errors as errors on second try
+                           target,
+                           uuid,
+                           type);
+  }
+  return res;
+}
+
 static OrthancPluginErrorCode StorageReadWholeLegacy(void** content,
                                                      int64_t* size,
                                                      const char* uuid,
@@ -240,63 +272,91 @@
 }
 
 
-static OrthancPluginErrorCode StorageRemove(const char* uuid,
+// static bool StorageRemoveFromDisk(const char* uuid,
+//                                   OrthancPluginContentType type)
+// {
+//   try
+//   {
+//     namespace fs = boost::filesystem;
+//     bool fileExisted = false;
+//     fs::path path = BaseStoragePlugin::GetOrthancFileSystemPath(uuid, fileSystemRootPath);
+
+//     try
+//     {
+//       fs::remove(path);
+//       fileExisted = true;
+//     }
+//     catch (...)
+//     {
+//       // Ignore the error
+//       fileExisted = false;
+//     }
+
+//     // Remove the two parent directories, ignoring the error code if
+//     // these directories are not empty
+
+//     try
+//     {
+//       boost::system::error_code err;
+//       fs::remove(path.parent_path(), err);
+//       fs::remove(path.parent_path().parent_path(), err);
+//     }
+//     catch (...)
+//     {
+//       // Ignore the error
+//     }
+
+//     return fileExisted;
+//   }
+//   catch(Orthanc::OrthancException& e)
+//   {
+//     OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while deleting object " + std::string(uuid) + ": " + std::string(e.What()));
+//     return false;
+//   }
+
+// }
+
+
+static OrthancPluginErrorCode StorageRemove(IStoragePlugin* plugin,
+                                            LogErrorFunction logErrorFunction,
+                                            const char* uuid,
                                             OrthancPluginContentType type)
 {
   try
   {
+    OrthancPlugins::LogInfo(plugin->GetNameForLogs() + ": deleting attachment " + std::string(uuid) + " of type " + boost::lexical_cast<std::string>(type));
     plugin->DeleteObject(uuid, type, cryptoEnabled);
+    if ((plugin == primaryPlugin.get()) && IsHybridModeEnabled())
+    {
+      // not 100% sure the file has been deleted, try the secondary plugin
+      return OrthancPluginErrorCode_StorageAreaPlugin; 
+    }
+    
+    return OrthancPluginErrorCode_Success;
   }
   catch (StoragePluginException& ex)
   {
-    if (migrationFromFileSystemEnabled)
-    {
-      try
-      {
-        OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while deleting object " + std::string(uuid) + ": " + ex.what() + ", will now try to delete it from legacy orthanc storage");
-        namespace fs = boost::filesystem;
-
-        fs::path path = BaseStoragePlugin::GetOrthancFileSystemPath(uuid, fileSystemRootPath);
-
-        try
-        {
-          fs::remove(path);
-        }
-        catch (...)
-        {
-          // Ignore the error
-        }
-
-        // Remove the two parent directories, ignoring the error code if
-        // these directories are not empty
+    logErrorFunction(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while deleting object " + std::string(uuid) + ": " + std::string(ex.what()));
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+}
 
-        try
-        {
-          boost::system::error_code err;
-          fs::remove(path.parent_path(), err);
-          fs::remove(path.parent_path().parent_path(), err);
-        }
-        catch (...)
-        {
-          // Ignore the error
-        }
+static OrthancPluginErrorCode StorageRemove(const char* uuid,
+                                            OrthancPluginContentType type)
+{
+  OrthancPluginErrorCode res = StorageRemove(primaryPlugin.get(),
+                                             (IsHybridModeEnabled() ? OrthancPlugins::LogWarning : OrthancPlugins::LogError), // log errors as warning on first try
+                                             uuid,
+                                             type);
 
-        return OrthancPluginErrorCode_Success;
-      }
-      catch(Orthanc::OrthancException& e)
-      {
-        OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while deleting object " + std::string(uuid) + ": " + std::string(e.What()));
-        return OrthancPluginErrorCode_StorageAreaPlugin;
-      }
-    }
-    else
-    {
-      OrthancPlugins::LogError(std::string(StoragePluginFactory::GetStoragePluginName()) + ": error while deleting object " + std::string(uuid) + ": " + ex.what());
-      return OrthancPluginErrorCode_StorageAreaPlugin;
-    }
+  if (res != OrthancPluginErrorCode_Success && IsHybridModeEnabled())
+  {
+    res = StorageRemove(secondaryPlugin.get(),
+                        OrthancPlugins::LogError, // log errors as errors on second try
+                        uuid,
+                        type);
   }
-
-  return OrthancPluginErrorCode_Success;
+  return res;
 }
 
 
@@ -328,14 +388,7 @@
 
     try
     {
-      plugin.reset(StoragePluginFactory::CreateStoragePlugin(orthancConfig));
-
-      if (plugin.get() == nullptr)
-      {
-        return -1;
-      }
-
-      const char* pluginSectionName = plugin->GetConfigurationSectionName();
+      const char* pluginSectionName = StoragePluginFactory::GetConfigurationSectionName();
       static const char* const ENCRYPTION_SECTION = "StorageEncryption";
 
       if (orthancConfig.IsSection(pluginSectionName))
@@ -343,12 +396,33 @@
         OrthancPlugins::OrthancConfiguration pluginSection;
         orthancConfig.GetSection(pluginSection, pluginSectionName);
 
-        migrationFromFileSystemEnabled = pluginSection.GetBooleanValue("MigrationFromFileSystemEnabled", false);
+        bool migrationFromFileSystemEnabled = pluginSection.GetBooleanValue("MigrationFromFileSystemEnabled", false);
+        std::string hybridModeString = pluginSection.GetStringValue("HybridMode", "Disabled");
 
-        if (migrationFromFileSystemEnabled)
+        if (migrationFromFileSystemEnabled && hybridModeString == "Disabled")
+        {
+          hybridMode = HybridMode_WriteToObjectStorage;
+          OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": 'MigrationFromFileSystemEnabled' configuration is deprecated, use 'HybridMode': 'WriteToObjectStorage' instead");
+        }
+        else if (hybridModeString == "WriteToObjectStorage")
+        {
+          hybridMode = HybridMode_WriteToObjectStorage;
+          OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": WriteToObjectStorage HybridMode is enabled: writing to object-storage, try reading first from object-storage and, then, from file system");
+        }
+        else if (hybridModeString == "WriteToFileSystem")
+        {
+          hybridMode = HybridMode_WriteToFileSystem;
+          OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": WriteToFileSystem HybridMode is enabled: writing to file system, try reading first from file system and, then, from object-storage");
+        }
+        else
+        {
+          OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": HybridMode is disabled enabled: writing to object-storage and reading only from object-storage");
+        }
+
+        if (IsReadFromDisk())
         {
           fileSystemRootPath = orthancConfig.GetStringValue("StorageDirectory", "OrthancStorageNotDefined");
-          OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": migration from file system enabled, source: " + fileSystemRootPath);
+          OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": HybridMode: reading from file system is enabled, source: " + fileSystemRootPath);
         }
 
         objectsRootPath = pluginSection.GetStringValue("RootPath", std::string());
@@ -359,7 +433,57 @@
           return -1;
         }
 
-        plugin->SetRootPath(objectsRootPath);
+        std::string objecstStoragePluginName = StoragePluginFactory::GetStoragePluginName();
+        if (hybridMode == HybridMode_WriteToFileSystem)
+        {
+          objecstStoragePluginName += " (Secondary: object-storage)";
+        }
+        else if (hybridMode == HybridMode_WriteToObjectStorage)
+        {
+          objecstStoragePluginName += " (Primary: object-storage)";
+        }
+
+        std::unique_ptr<IStoragePlugin> objectStoragePlugin(StoragePluginFactory::CreateStoragePlugin(objecstStoragePluginName, orthancConfig));
+
+        if (objectStoragePlugin.get() == nullptr)
+        {
+          return -1;
+        }
+
+        objectStoragePlugin->SetRootPath(objectsRootPath);
+
+        std::unique_ptr<IStoragePlugin> fileSystemStoragePlugin;
+        if (IsHybridModeEnabled())
+        {
+          bool fsync = orthancConfig.GetBooleanValue("SyncStorageArea", true);
+
+          std::string filesystemStoragePluginName = StoragePluginFactory::GetStoragePluginName();
+          if (hybridMode == HybridMode_WriteToFileSystem)
+          {
+            filesystemStoragePluginName += " (Primary: file-system)";
+          }
+          else if (hybridMode == HybridMode_WriteToObjectStorage)
+          {
+            filesystemStoragePluginName += " (Secondary: file-system)";
+          }
+
+          fileSystemStoragePlugin.reset(new FileSystemStoragePlugin(filesystemStoragePluginName, fileSystemRootPath, fsync));
+        }
+
+        if (hybridMode == HybridMode_Disabled || hybridMode == HybridMode_WriteToObjectStorage)
+        {
+          primaryPlugin.reset(objectStoragePlugin.release());
+          
+          if (hybridMode == HybridMode_WriteToObjectStorage)
+          {
+            secondaryPlugin.reset(fileSystemStoragePlugin.release());
+          }
+        }
+        else if (hybridMode == HybridMode_WriteToFileSystem)
+        {
+          primaryPlugin.reset(fileSystemStoragePlugin.release());
+          secondaryPlugin.reset(objectStoragePlugin.release());
+        }
 
         if (pluginSection.IsSection(ENCRYPTION_SECTION))
         {
@@ -378,6 +502,8 @@
         {
           OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + ": client-side encryption is disabled");
         }
+
+
       }
 
       if (cryptoEnabled)
@@ -403,7 +529,8 @@
   ORTHANC_PLUGINS_API void OrthancPluginFinalize()
   {
     OrthancPlugins::LogWarning(std::string(StoragePluginFactory::GetStoragePluginName()) + " plugin is finalizing");
-    plugin.reset();
+    primaryPlugin.reset();
+    secondaryPlugin.reset();
     Orthanc::FinalizeFramework();
   }
 
--- a/Google/CMakeLists.txt	Tue Aug 30 14:59:58 2022 +0200
+++ b/Google/CMakeLists.txt	Fri Oct 14 10:36:02 2022 +0200
@@ -67,6 +67,8 @@
     ${CMAKE_SOURCE_DIR}/../Common/EncryptionHelpers.h
     ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.cpp
     ${CMAKE_SOURCE_DIR}/../Common/EncryptionConfigurator.h
+    ${CMAKE_SOURCE_DIR}/../Common/FileSystemStoragePlugin.h
+    ${CMAKE_SOURCE_DIR}/../Common/FileSystemStoragePlugin.cpp
     ${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
 
     ${ORTHANC_CORE_SOURCES}
--- a/Google/GoogleStoragePlugin.cpp	Tue Aug 30 14:59:58 2022 +0200
+++ b/Google/GoogleStoragePlugin.cpp	Fri Oct 14 10:36:02 2022 +0200
@@ -44,7 +44,7 @@
 
   virtual IWriter* GetWriterForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
   virtual IReader* GetReaderForObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
-  virtual void DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
+  virtual bool DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled);
   virtual const char* GetConfigurationSectionName() {return PLUGIN_SECTION;}
 };
 
@@ -314,7 +314,7 @@
   return new Reader(bucketName_, paths, mainClient_);
 }
 
-void GoogleStoragePlugin::DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
+bool GoogleStoragePlugin::DeleteObject(const char* uuid, OrthancPluginContentType type, bool encryptionEnabled)
 {
   gcs::Client client(mainClient_);
 
@@ -327,4 +327,5 @@
     throw StoragePluginException("GoogleCloudStorage: error while deleting file " + std::string(path) + ": " + deletionStatus.message());
   }
 
+  return false; // not 100% sure that the file has been deleted
 }
--- a/NEWS	Tue Aug 30 14:59:58 2022 +0200
+++ b/NEWS	Fri Oct 14 10:36:02 2022 +0200
@@ -1,3 +1,8 @@
+* new option "HybridMode" allows selecting where to write new data (FileSystem or ObjectStorage) and reads
+  from both storages.  Allowed values: "WriteToFileSystem", "WriteToObjectStorage", "Disabled".
+  If disabled (default), the plugin writes all data to the object storage and tries to read them only from the
+  object storage.
+
 2022-08-30 - v 2.0.0
 ====================
 
--- a/README.md	Tue Aug 30 14:59:58 2022 +0200
+++ b/README.md	Fri Oct 14 10:36:02 2022 +0200
@@ -133,6 +133,46 @@
         "RootPath": "",                 // optional: folder in which files are stored (ex: my/path/to/myfolder)
         "StorageEncryption" : {...},    // optional
         "StorageStructure" : "flat",    // optional
-        "MigrationFromFileSystemEnabled" : false // optional
+        "MigrationFromFileSystemEnabled" : false, // optional (deprecated, is now equivalent to "HybridMode": "WriteToObjectStorage")
+        "HybridMode": "WriteToDisk"     // "WriteToDisk", "WriteToObjectStorage", "Disabled"
     }
-```
\ No newline at end of file
+```
+
+### Testing the S3 plugin with minio
+
+```
+docker run -p 9000:9000 -p 9001:9001 -e MINIO_REGION=eu-west-1 -e MINIO_ROOT_USER=minio -e MINIO_ROOT_PASSWORD=miniopwd minio/minio server /data --console-address ":9001"
+```
+
+config file:
+```
+    "AwsS3Storage" : {
+        "BucketName": "orthanc",
+        "Region": "eu-west-1",
+        "Endpoint": "http://127.0.0.1:9000/",
+        "AccessKey": "minio",
+        "SecretKey": "miniopwd",
+        "VirtualAddressing": false
+
+        // "StorageEncryption" : {
+        //     "Enable": true,
+        //     "MasterKey": [1, "/home/test/encryption/test.key"],
+        //     "MaxConcurrentInputSize": 1024,
+        //     "Verbose": true         
+        // }                  // optional: see the section related to encryption
+      }
+
+```
+
+Test the hybrid mode
+- start in "HybridMode": "WriteToFileSystem", 
+  - upload instances 1 & 2
+- restart in "HybridMode": "WriteToObjectStorage", 
+  - check that you can read instance 1 and that you can delete it
+  - upload instances 3 & 4
+- restart in "HybridMode": "WriteToFileSystem",
+  - check that you can read instance 3 and that you can delete it
+- final check:
+  - there should be only one file in the disk storage
+  - there should be only one file in the S3 bucket
+