changeset 5080:d7274e43ea7c attach-custom-data

allow plugins to store a customData in the Attachments table to e.g. store custom paths without requiring an external DB
author Alain Mazy <am@osimis.io>
date Thu, 08 Sep 2022 17:42:08 +0200
parents 4366b4c41441
children c673997507ea
files OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancFramework/Sources/FileStorage/FileInfo.cpp OrthancFramework/Sources/FileStorage/FileInfo.h OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp OrthancFramework/Sources/FileStorage/FilesystemStorage.h OrthancFramework/Sources/FileStorage/IStorageArea.h OrthancFramework/Sources/FileStorage/MemoryStorageArea.h OrthancFramework/Sources/FileStorage/StorageAccessor.cpp OrthancFramework/Sources/FileStorage/StorageAccessor.h OrthancFramework/UnitTestsSources/FileStorageTests.cpp OrthancServer/CMakeLists.txt OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Samples/AdvancedStorage/CMakeLists.txt OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp OrthancServer/Resources/Configuration.json OrthancServer/Sources/Database/DowngradeFrom7to6.sql OrthancServer/Sources/Database/PrepareDatabase.sql OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/Upgrade6To7.sql OrthancServer/Sources/OrthancInitialization.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/UnitTestsSources/ServerIndexTests.cpp TODO
diffstat 30 files changed, 1714 insertions(+), 212 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Thu Sep 08 17:42:08 2022 +0200
@@ -32,8 +32,9 @@
 #   * Orthanc 0.4.0 -> Orthanc 0.7.2 = version 3
 #   * Orthanc 0.7.3 -> Orthanc 0.8.4 = version 4
 #   * Orthanc 0.8.5 -> Orthanc 0.9.4 = version 5
-#   * Orthanc 0.9.5 -> mainline      = version 6
-set(ORTHANC_DATABASE_VERSION 6)
+#   * Orthanc 0.9.5 -> Orthanc 1.11.X = version 6
+#   * Orthanc 1.12.0 -> mainline      = version 7
+set(ORTHANC_DATABASE_VERSION 7)
 
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -41,7 +41,8 @@
   FileInfo::FileInfo(const std::string& uuid,
                      FileContentType contentType,
                      uint64_t size,
-                     const std::string& md5) :
+                     const std::string& md5,
+                     const std::string& customData) :
     valid_(true),
     uuid_(uuid),
     contentType_(contentType),
@@ -49,7 +50,8 @@
     uncompressedMD5_(md5),
     compressionType_(CompressionType_None),
     compressedSize_(size),
-    compressedMD5_(md5)
+    compressedMD5_(md5),
+    customData_(customData)
   {
   }
 
@@ -60,7 +62,8 @@
                      const std::string& uncompressedMD5,
                      CompressionType compressionType,
                      uint64_t compressedSize,
-                     const std::string& compressedMD5) :
+                     const std::string& compressedMD5,
+                     const std::string& customData) :
     valid_(true),
     uuid_(uuid),
     contentType_(contentType),
@@ -68,7 +71,8 @@
     uncompressedMD5_(uncompressedMD5),
     compressionType_(compressionType),
     compressedSize_(compressedSize),
-    compressedMD5_(compressedMD5)
+    compressedMD5_(compressedMD5),
+    customData_(customData)
   {
   }
 
@@ -168,4 +172,16 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
   }
+
+  const std::string& FileInfo::GetCustomData() const
+  {
+    if (valid_)
+    {
+      return customData_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
 }
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.h	Thu Sep 08 17:42:08 2022 +0200
@@ -41,6 +41,7 @@
     CompressionType  compressionType_;
     uint64_t         compressedSize_;
     std::string      compressedMD5_;
+    std::string      customData_;
 
   public:
     FileInfo();
@@ -51,7 +52,8 @@
     FileInfo(const std::string& uuid,
              FileContentType contentType,
              uint64_t size,
-             const std::string& md5);
+             const std::string& md5,
+             const std::string& customData);
 
     /**
      * Constructor for a compressed attachment.
@@ -62,7 +64,8 @@
              const std::string& uncompressedMD5,
              CompressionType compressionType,
              uint64_t compressedSize,
-             const std::string& compressedMD5);
+             const std::string& compressedMD5,
+             const std::string& customData);
 
     bool IsValid() const;
     
@@ -79,5 +82,7 @@
     const std::string& GetCompressedMD5() const;
 
     const std::string& GetUncompressedMD5() const;
+
+    const std::string& GetCustomData() const;
   };
 }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -242,6 +242,7 @@
   {
     namespace fs = boost::filesystem;
     typedef std::set<std::string> List;
+    std::string customDataNotUsed;
 
     List result;
     ListAllFiles(result);
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Thu Sep 08 17:42:08 2022 +0200
@@ -42,7 +42,7 @@
 
 namespace Orthanc
 {
-  class ORTHANC_PUBLIC FilesystemStorage : public IStorageArea
+  class ORTHANC_PUBLIC FilesystemStorage : public ICoreStorageArea
   {
     // TODO REMOVE THIS
     friend class FilesystemHttpSender;
--- a/OrthancFramework/Sources/FileStorage/IStorageArea.h	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h	Thu Sep 08 17:42:08 2022 +0200
@@ -32,6 +32,8 @@
 
 namespace Orthanc
 {
+  class DicomInstanceToStore;
+
   class IStorageArea : public boost::noncopyable
   {
   public:
@@ -39,8 +41,92 @@
     {
     }
 
+    virtual void CreateInstance(std::string& customData,
+                               const DicomInstanceToStore& instance,
+                               const std::string& uuid,
+                               const void* content,
+                               size_t size,
+                               FileContentType type,
+                               bool isCompressed) = 0;
+
+    virtual void CreateAttachment(std::string& customData,
+                                  const std::string& resourceId,
+                                  ResourceType resourceLevel,
+                                  const std::string& uuid,
+                                  const void* content,
+                                  size_t size,
+                                  FileContentType type,
+                                  bool isCompressed) = 0;
+
+    virtual IMemoryBuffer* Read(const std::string& uuid,
+                                FileContentType type,
+                                const std::string& customData) = 0;
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */,
+                                     const std::string& customData) = 0;
+
+    virtual bool HasReadRange() const = 0;
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type,
+                        const std::string& customData) = 0;
+  };
+
+  // storage area without customData (customData are used only in plugins)
+  class ICoreStorageArea : public IStorageArea
+  {
+  public:
+    virtual void CreateInstance(std::string& customData,
+                               const DicomInstanceToStore& instance,
+                               const std::string& uuid,
+                               const void* content,
+                               size_t size,
+                               FileContentType type,
+                               bool isCompressed)
+    {
+      Create(uuid, content, size, type);
+    }
+
+    virtual void CreateAttachment(std::string& customData,
+                                  const std::string& resourceId,
+                                  ResourceType resourceLevel,
+                                  const std::string& uuid,
+                                  const void* content,
+                                  size_t size,
+                                  FileContentType type,
+                                  bool isCompressed)
+    {
+      Create(uuid, content, size, type);
+    }
+
+    virtual IMemoryBuffer* Read(const std::string& uuid,
+                                FileContentType type,
+                                const std::string& /*customData*/)
+    {
+      return Read(uuid, type);
+    }
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */,
+                                     const std::string& /*customData */)
+    {
+      return ReadRange(uuid, type, start, end);
+    }
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type,
+                        const std::string& customData)
+    {
+      Remove(uuid, type);
+    }
+
     virtual void Create(const std::string& uuid,
-                        const void* content,
+                        const void* content, 
                         size_t size,
                         FileContentType type) = 0;
 
@@ -52,9 +138,8 @@
                                      uint64_t start /* inclusive */,
                                      uint64_t end /* exclusive */) = 0;
 
-    virtual bool HasReadRange() const = 0;
-
     virtual void Remove(const std::string& uuid,
                         FileContentType type) = 0;
+
   };
 }
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Thu Sep 08 17:42:08 2022 +0200
@@ -32,7 +32,7 @@
 
 namespace Orthanc
 {
-  class MemoryStorageArea : public IStorageArea
+  class MemoryStorageArea : public ICoreStorageArea
   {
   private:
     typedef std::map<std::string, std::string*>  Content;
@@ -43,6 +43,7 @@
   public:
     virtual ~MemoryStorageArea();
     
+  protected:
     virtual void Create(const std::string& uuid,
                         const void* content,
                         size_t size,
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -79,14 +79,15 @@
   }
 
 
-  FileInfo StorageAccessor::Write(const void* data,
-                                  size_t size,
-                                  FileContentType type,
-                                  CompressionType compression,
-                                  bool storeMd5)
+  FileInfo StorageAccessor::WriteInstance(std::string& customData,
+                                          const DicomInstanceToStore& instance,
+                                          const void* data,
+                                          size_t size,
+                                          FileContentType type,
+                                          CompressionType compression,
+                                          bool storeMd5,
+                                          const std::string& uuid)
   {
-    std::string uuid = Toolbox::GenerateUuid();
-
     std::string md5;
 
     if (storeMd5)
@@ -100,14 +101,14 @@
       {
         MetricsTimer timer(*this, METRICS_CREATE);
 
-        area_.Create(uuid, data, size, type);
+        area_.CreateInstance(customData, instance, uuid, data, size, type, false);
         
         if (cache_ != NULL)
         {
           cache_->Add(uuid, type, data, size);
         }
 
-        return FileInfo(uuid, type, size, md5);
+        return FileInfo(uuid, type, size, md5, customData);
       }
 
       case CompressionType_ZlibWithSize:
@@ -129,11 +130,11 @@
 
           if (compressed.size() > 0)
           {
-            area_.Create(uuid, &compressed[0], compressed.size(), type);
+            area_.CreateInstance(customData, instance, uuid, &compressed[0], compressed.size(), type, true);
           }
           else
           {
-            area_.Create(uuid, NULL, 0, type);
+            area_.CreateInstance(customData, instance, uuid, NULL, 0, type, true);
           }
         }
 
@@ -143,7 +144,7 @@
         }
 
         return FileInfo(uuid, type, size, md5,
-                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
+                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData);
       }
 
       default:
@@ -151,13 +152,78 @@
     }
   }
 
-  FileInfo StorageAccessor::Write(const std::string &data,
-                                  FileContentType type,
-                                  CompressionType compression,
-                                  bool storeMd5)
+  FileInfo StorageAccessor::WriteAttachment(std::string& customData,
+                                            const std::string& resourceId,
+                                            ResourceType resourceType,
+                                            const void* data,
+                                            size_t size,
+                                            FileContentType type,
+                                            CompressionType compression,
+                                            bool storeMd5,
+                                            const std::string& uuid)
   {
-    return Write((data.size() == 0 ? NULL : data.c_str()),
-                 data.size(), type, compression, storeMd5);
+    std::string md5;
+
+    if (storeMd5)
+    {
+      Toolbox::ComputeMD5(md5, data, size);
+    }
+
+    switch (compression)
+    {
+      case CompressionType_None:
+      {
+        MetricsTimer timer(*this, METRICS_CREATE);
+
+        area_.CreateAttachment(customData, resourceId, resourceType, uuid, data, size, type, false);
+        
+        if (cache_ != NULL)
+        {
+          cache_->Add(uuid, type, data, size);
+        }
+
+        return FileInfo(uuid, type, size, md5, customData);
+      }
+
+      case CompressionType_ZlibWithSize:
+      {
+        ZlibCompressor zlib;
+
+        std::string compressed;
+        zlib.Compress(compressed, data, size);
+
+        std::string compressedMD5;
+      
+        if (storeMd5)
+        {
+          Toolbox::ComputeMD5(compressedMD5, compressed);
+        }
+
+        {
+          MetricsTimer timer(*this, METRICS_CREATE);
+
+          if (compressed.size() > 0)
+          {
+            area_.CreateAttachment(customData, resourceId, resourceType, uuid, &compressed[0], compressed.size(), type, true);
+          }
+          else
+          {
+            area_.CreateAttachment(customData, resourceId, resourceType, uuid, NULL, 0, type, true);
+          }
+        }
+
+        if (cache_ != NULL)
+        {
+          cache_->Add(uuid, type, data, size);  // always add uncompressed data to cache
+        }
+
+        return FileInfo(uuid, type, size, md5,
+                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData);
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
   }
 
 
@@ -172,7 +238,7 @@
         case CompressionType_None:
         {
           MetricsTimer timer(*this, METRICS_READ);
-          std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType()));
+          std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
           buffer->MoveToString(content);
 
           break;
@@ -186,7 +252,7 @@
           
           {
             MetricsTimer timer(*this, METRICS_READ);
-            compressed.reset(area_.Read(info.GetUuid(), info.GetContentType()));
+            compressed.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
           }
           
           zlib.Uncompress(content, compressed->GetData(), compressed->GetSize());
@@ -217,14 +283,15 @@
     if (cache_ == NULL || !cache_->Fetch(content, info.GetUuid(), info.GetContentType()))
     {
       MetricsTimer timer(*this, METRICS_READ);
-      std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType()));
+      std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
       buffer->MoveToString(content);
     }
   }
 
 
   void StorageAccessor::Remove(const std::string& fileUuid,
-                               FileContentType type)
+                               FileContentType type,
+                               const std::string& customData)
   {
     if (cache_ != NULL)
     {
@@ -233,26 +300,27 @@
 
     {
       MetricsTimer timer(*this, METRICS_REMOVE);
-      area_.Remove(fileUuid, type);
+      area_.Remove(fileUuid, type, customData);
     }
   }
   
 
   void StorageAccessor::Remove(const FileInfo &info)
   {
-    Remove(info.GetUuid(), info.GetContentType());
+    Remove(info.GetUuid(), info.GetContentType(), info.GetCustomData());
   }
 
 
   void StorageAccessor::ReadStartRange(std::string& target,
                                        const std::string& fileUuid,
                                        FileContentType contentType,
-                                       uint64_t end /* exclusive */)
+                                       uint64_t end /* exclusive */,
+                                       const std::string& customData)
   {
     if (cache_ == NULL || !cache_->FetchStartRange(target, fileUuid, contentType, end))
     {
       MetricsTimer timer(*this, METRICS_READ);
-      std::unique_ptr<IMemoryBuffer> buffer(area_.ReadRange(fileUuid, contentType, 0, end));
+      std::unique_ptr<IMemoryBuffer> buffer(area_.ReadRange(fileUuid, contentType, 0, end, customData));
       assert(buffer->GetSize() == end);
       buffer->MoveToString(target);
 
@@ -341,4 +409,21 @@
     output.AnswerStream(transcoder);
   }
 #endif
+
+  // bool StorageAccessor::HandlesCustomData()
+  // {
+  //   return area_.HandlesCustomData();
+  // }
+
+  // void StorageAccessor::GetCustomData(std::string& customData,
+  //                                     const std::string& uuid,
+  //                                     const DicomInstanceToStore* instance,
+  //                                     const void* content, 
+  //                                     size_t size,
+  //                                     FileContentType type,
+  //                                     bool compression)
+  // {
+  //   area_.GetCustomData(customData, uuid, instance, content, size, type, compression);
+  // }
+
 }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Thu Sep 08 17:42:08 2022 +0200
@@ -85,16 +85,47 @@
                     StorageCache* cache,
                     MetricsRegistry& metrics);
 
-    FileInfo Write(const void* data,
-                   size_t size,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
+    // FileInfo Write(const void* data,
+    //                size_t size,
+    //                FileContentType type,
+    //                CompressionType compression,
+    //                bool storeMd5,
+    //                const std::string& uuid,
+    //                const std::string& customData);
+
+    // FileInfo Write(const std::string& data,
+    //                FileContentType type,
+    //                CompressionType compression,
+    //                bool storeMd5,
+    //                const std::string& uuid,
+    //                const std::string& customData);
 
-    FileInfo Write(const std::string& data,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
+    FileInfo WriteInstance(std::string& customData,
+                           const DicomInstanceToStore& instance,
+                           const void* data,
+                           size_t size,
+                           FileContentType type,
+                           CompressionType compression,
+                           bool storeMd5,
+                           const std::string& uuid);
+
+    FileInfo WriteAttachment(std::string& customData,
+                             const std::string& resourceId,
+                             ResourceType resourceType,
+                             const void* data,
+                             size_t size,
+                             FileContentType type,
+                             CompressionType compression,
+                             bool storeMd5,
+                             const std::string& uuid);
+
+    // FileInfo Write(std::string& customData,
+    //                const std::string& data,
+    //                FileContentType type,
+    //                CompressionType compression,
+    //                bool storeMd5,
+    //                const std::string& uuid,
+    //                const std::string& customData);
 
     void Read(std::string& content,
               const FileInfo& info);
@@ -105,10 +136,12 @@
     void ReadStartRange(std::string& target,
                         const std::string& fileUuid,
                         FileContentType fullFileContentType,
-                        uint64_t end /* exclusive */);
+                        uint64_t end /* exclusive */,
+                        const std::string& customData);
 
     void Remove(const std::string& fileUuid,
-                FileContentType type);
+                FileContentType type,
+                const std::string& customData);
 
     void Remove(const FileInfo& info);
 
@@ -129,5 +162,15 @@
                     const FileInfo& info,
                     const std::string& mime);
 #endif
+
+    bool HandlesCustomData();
+
+    // void GetCustomData(std::string& customData,
+    //                    const std::string& uuid,
+    //                    const DicomInstanceToStore* instance,
+    //                    const void* content, 
+    //                    size_t size,
+    //                    FileContentType type,
+    //                    bool compression);
   };
 }
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -130,7 +130,8 @@
   StorageAccessor accessor(s, &cache);
 
   std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true);
+  std::string uuid = Toolbox::GenerateUuid();
+  FileInfo info = accessor.WriteAttachment(data, "", ResourceType_Instance, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, uuid);
   
   std::string r;
   accessor.Read(r, info);
@@ -152,7 +153,8 @@
   StorageAccessor accessor(s, &cache);
 
   std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true);
+  std::string uuid = Toolbox::GenerateUuid();
+  FileInfo info = accessor.WriteAttachment(data, "", ResourceType_Instance, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, uuid);
   
   std::string r;
   accessor.Read(r, info);
@@ -163,6 +165,7 @@
   ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
   ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5());
   ASSERT_NE(info.GetUncompressedMD5(), info.GetCompressedMD5());
+  ASSERT_EQ(uuid, info.GetUuid());
 }
 
 
@@ -176,9 +179,9 @@
   std::string compressedData = "Hello";
   std::string uncompressedData = "HelloWorld";
 
-  FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom, CompressionType_ZlibWithSize, false);  
-  FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom, CompressionType_None, false);
-  
+  FileInfo compressedInfo = accessor.WriteAttachment(compressedData, "", ResourceType_Instance, compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, Toolbox::GenerateUuid());
+  FileInfo uncompressedInfo = accessor.WriteAttachment(uncompressedData, "", ResourceType_Instance, uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, Toolbox::GenerateUuid());
+
   accessor.Read(r, compressedInfo);
   ASSERT_EQ(compressedData, r);
 
--- a/OrthancServer/CMakeLists.txt	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/CMakeLists.txt	Thu Sep 08 17:42:08 2022 +0200
@@ -61,6 +61,7 @@
 SET(BUILD_CONNECTIVITY_CHECKS ON CACHE BOOL "Whether to build the ConnectivityChecks plugin")
 SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin")
 SET(BUILD_DELAYED_DELETION ON CACHE BOOL "Whether to build the DelayedDeletion plugin")
+SET(BUILD_ADVANCED_STORAGE ON CACHE BOOL "Whether to build the AdvancedStorage plugin")
 SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins")
 SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests")
 
@@ -228,6 +229,7 @@
   PREPARE_DATABASE             ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql
   UPGRADE_DATABASE_3_TO_4      ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql
   UPGRADE_DATABASE_4_TO_5      ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql
+  UPGRADE_DATABASE_6_TO_7      ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade6To7.sql
 
   INSTALL_TRACK_ATTACHMENTS_SIZE
   ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql
@@ -318,6 +320,7 @@
   -DMODALITY_WORKLISTS_VERSION="${ORTHANC_VERSION}"
   -DSERVE_FOLDERS_VERSION="${ORTHANC_VERSION}"
   -DHOUSEKEEPER_VERSION="${ORTHANC_VERSION}"
+  -DADVANCED_STORAGE_VERSION="${ORTHANC_VERSION}"
   )
 
 
@@ -430,7 +433,7 @@
 #####################################################################
 
 if (ENABLE_PLUGINS AND
-    (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER))
+    (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER OR BUILD_ADVANCED_STORAGE))
   add_library(ThirdPartyPlugins STATIC
     ${BOOST_SOURCES}
     ${JSONCPP_SOURCES}
@@ -730,6 +733,77 @@
 
 
 #####################################################################
+## Build the "AdvancedStorage" plugin
+#####################################################################
+
+if (ENABLE_PLUGINS AND BUILD_ADVANCED_STORAGE)
+
+  set(AdvancedStorageFlags)
+
+  if (CMAKE_TOOLCHAIN_FILE)
+    # Take absolute path to the toolchain
+    get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR})
+    list(APPEND AdvancedStorageFlags -DCMAKE_TOOLCHAIN_FILE=${TMP})
+  endif()
+
+  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+    list(APPEND AdvancedStorageFlags
+      -DLSB_CC=${CMAKE_LSB_CC}
+      -DLSB_CXX=${CMAKE_LSB_CXX}
+      )
+  endif()
+
+  externalproject_add(AdvancedStorage
+    SOURCE_DIR "${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage"
+
+    # We explicitly provide a build directory, in order to avoid paths
+    # that are too long on our Visual Studio 2008 CIS
+    BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/AdvancedStorage-build"
+
+    # this helps triggering build when changing the external project
+    BUILD_ALWAYS 1
+
+    CMAKE_ARGS
+    -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
+    -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
+    -DPLUGIN_VERSION=${ORTHANC_VERSION}
+    -DSTATIC_BUILD=${STATIC_BUILD}
+    -DALLOW_DOWNLOADS=${ALLOW_DOWNLOADS}
+    -DUSE_SYSTEM_BOOST=${USE_SYSTEM_BOOST}
+    -DUSE_LEGACY_JSONCPP=${USE_LEGACY_JSONCPP}
+    -DUSE_LEGACY_BOOST=${USE_LEGACY_BOOST}
+    ${AdvancedStorageFlags}
+
+    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
+    -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
+    -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
+    -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
+    -DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}
+    -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}
+    )
+
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    if (MSVC)
+      set(Prefix "")
+    else()
+      set(Prefix "lib")  # MinGW
+    endif()
+
+    install(FILES
+      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}AdvancedStorage.dll
+      DESTINATION "lib")
+  else()
+    list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
+    list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 Suffix)
+    install(FILES
+      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}AdvancedStorage${Suffix}
+      ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}AdvancedStorage${Suffix}.${ORTHANC_VERSION}
+      DESTINATION "share/orthanc/plugins")
+  endif()
+endif()
+
+
+#####################################################################
 ## Build the companion tool to recover files compressed using Orthanc
 #####################################################################
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -82,13 +82,15 @@
     
     static FileInfo Convert(const OrthancPluginAttachment& attachment)
     {
+      std::string customData;
       return FileInfo(attachment.uuid,
                       static_cast<FileContentType>(attachment.contentType),
                       attachment.uncompressedSize,
                       attachment.uncompressedHash,
                       static_cast<CompressionType>(attachment.compressionType),
                       attachment.compressedSize,
-                      attachment.compressedHash);
+                      attachment.compressedHash,
+                      customData);
     }
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -61,13 +61,15 @@
 
     static FileInfo Convert(const OrthancPluginAttachment& attachment)
     {
+      std::string customData;
       return FileInfo(attachment.uuid,
                       static_cast<FileContentType>(attachment.contentType),
                       attachment.uncompressedSize,
                       attachment.uncompressedHash,
                       static_cast<CompressionType>(attachment.compressionType),
                       attachment.compressedSize,
-                      attachment.compressedHash);
+                      attachment.compressedHash,
+                      customData);
     }
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -58,6 +58,7 @@
 #include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/OrthancConfiguration.h"
 #include "../../Sources/OrthancFindRequestHandler.h"
+#include "../../Sources/Search/DatabaseConstraint.h"
 #include "../../Sources/Search/HierarchicalMatcher.h"
 #include "../../Sources/ServerContext.h"
 #include "../../Sources/ServerToolbox.h"
@@ -413,6 +414,99 @@
     }
   };
   
+  class OrthancPlugins::IDicomInstance : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomInstance()
+    {
+    }
+
+    virtual bool CanBeFreed() const = 0;
+
+    virtual const DicomInstanceToStore& GetInstance() const = 0;
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance
+  {
+  private:
+    const DicomInstanceToStore&  instance_;
+
+  public:
+    explicit DicomInstanceFromCallback(const DicomInstanceToStore& instance) :
+      instance_(instance)
+    {
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return instance_;
+    };
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance
+  {
+  private:
+    std::string                            buffer_;
+    std::unique_ptr<DicomInstanceToStore>  instance_;
+
+  public:
+    DicomInstanceFromBuffer(const void* buffer,
+                            size_t size)
+    {
+      buffer_.assign(reinterpret_cast<const char*>(buffer), size);
+
+      instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_));
+      instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return *instance_;
+    };
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance
+  {
+  private:
+    std::unique_ptr<ParsedDicomFile>       parsed_;
+    std::unique_ptr<DicomInstanceToStore>  instance_;
+
+  public:
+    explicit DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) :
+      parsed_(transcoded.ReleaseAsParsedDicomFile())
+    {
+      if (parsed_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      
+      instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_));
+      instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return *instance_;
+    };
+  };
 
   static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
                                  const void* data,
@@ -547,8 +641,8 @@
       }
     };
   
-
-    class StorageAreaBase : public IStorageArea
+    // "legacy" storage plugins don't store customData -> derive from ICoreStorageArea
+    class PluginStorageAreaBase : public ICoreStorageArea
     {
     private:
       OrthancPluginStorageCreate create_;
@@ -602,9 +696,9 @@
       }      
       
     public:
-      StorageAreaBase(OrthancPluginStorageCreate create,
-                      OrthancPluginStorageRemove remove,
-                      PluginsErrorDictionary&  errorDictionary) : 
+      PluginStorageAreaBase(OrthancPluginStorageCreate create,
+                            OrthancPluginStorageRemove remove,
+                            PluginsErrorDictionary&  errorDictionary) : 
         create_(create),
         remove_(remove),
         errorDictionary_(errorDictionary)
@@ -646,7 +740,7 @@
     };
 
 
-    class PluginStorageArea : public StorageAreaBase
+    class PluginStorageArea : public PluginStorageAreaBase
     {
     private:
       OrthancPluginStorageRead   read_;
@@ -663,7 +757,7 @@
     public:
       PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks,
                         PluginsErrorDictionary&  errorDictionary) :
-        StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
+        PluginStorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
         read_(callbacks.read),
         free_(callbacks.free)
       {
@@ -712,7 +806,7 @@
 
 
     // New in Orthanc 1.9.0
-    class PluginStorageArea2 : public StorageAreaBase
+    class PluginStorageArea2 : public PluginStorageAreaBase
     {
     private:
       OrthancPluginStorageReadWhole  readWhole_;
@@ -721,7 +815,7 @@
     public:
       PluginStorageArea2(const _OrthancPluginRegisterStorageArea2& callbacks,
                          PluginsErrorDictionary&  errorDictionary) :
-        StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
+        PluginStorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
         readWhole_(callbacks.readWhole),
         readRange_(callbacks.readRange)
       {
@@ -806,19 +900,252 @@
     };
 
 
+    // New in Orthanc 1.12.0
+    class PluginStorageArea3 : public IStorageArea
+    {
+    private:
+      OrthancPluginStorageCreateInstance createInstance_;
+      OrthancPluginStorageCreateAttachment createAttachment_;
+      OrthancPluginStorageRemove2 remove2_;
+      OrthancPluginStorageReadWhole2  readWhole2_;
+      OrthancPluginStorageReadRange2  readRange2_;
+
+      PluginsErrorDictionary&    errorDictionary_;
+
+    protected:
+      PluginsErrorDictionary& GetErrorDictionary() const
+      {
+        return errorDictionary_;
+      }
+
+      IMemoryBuffer* RangeFromWhole(const std::string& uuid,
+                                    const std::string& customData,
+                                    FileContentType type,
+                                    uint64_t start /* inclusive */,
+                                    uint64_t end /* exclusive */)
+      {
+        if (start > end)
+        {
+          throw OrthancException(ErrorCode_BadRange);
+        }
+        else if (start == end)
+        {
+          return new StringMemoryBuffer;  // Empty
+        }
+        else
+        {
+          std::unique_ptr<IMemoryBuffer> whole(Read(uuid, type, customData));
+
+          if (start == 0 &&
+              end == whole->GetSize())
+          {
+            return whole.release();
+          }
+          else if (end > whole->GetSize())
+          {
+            throw OrthancException(ErrorCode_BadRange);
+          }
+          else
+          {
+            std::string range;
+            range.resize(end - start);
+            assert(!range.empty());
+            
+            memcpy(&range[0], reinterpret_cast<const char*>(whole->GetData()) + start, range.size());
+
+            whole.reset(NULL);
+            return StringMemoryBuffer::CreateFromSwap(range);
+          }
+        }
+      }      
+      
+    public:
+      PluginStorageArea3(const _OrthancPluginRegisterStorageArea3& callbacks,
+                      PluginsErrorDictionary&  errorDictionary) : 
+        createInstance_(callbacks.createInstance),
+        createAttachment_(callbacks.createAttachment),
+        remove2_(callbacks.remove),
+        readWhole2_(callbacks.readWhole),
+        readRange2_(callbacks.readRange),
+        errorDictionary_(errorDictionary)
+      {
+        if (createInstance_ == NULL ||
+            createAttachment_ == NULL ||
+            remove2_ == NULL ||
+            readWhole2_ == NULL)
+        {
+          throw OrthancException(ErrorCode_Plugin, "Storage area plugin doesn't implement all the required primitives (create, remove, readWhole");
+        }
+      }
+
+      virtual void CreateInstance(std::string& customData,
+                                const DicomInstanceToStore& instance,
+                                const std::string& uuid,
+                                const void* content,
+                                size_t size,
+                                FileContentType type,
+                                bool isCompressed) ORTHANC_OVERRIDE
+      {
+        OrthancPluginMemoryBuffer customDataBuffer;
+        Orthanc::OrthancPlugins::DicomInstanceFromCallback wrapped(instance);
+
+        OrthancPluginErrorCode error = createInstance_(&customDataBuffer,
+                                                       uuid.c_str(),
+                                                       reinterpret_cast<OrthancPluginDicomInstance*>(&wrapped),
+                                                       content, size, Plugins::Convert(type),
+                                                       isCompressed);
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+
+        if (customDataBuffer.size > 0)
+        {
+          customData.assign(reinterpret_cast<char*>(customDataBuffer.data), 
+                            static_cast<size_t>(customDataBuffer.size));
+        }
+      }
+
+      virtual void CreateAttachment(std::string& customData,
+                                    const std::string& resourceId,
+                                    ResourceType resourceLevel,
+                                    const std::string& uuid,
+                                    const void* content,
+                                    size_t size,
+                                    FileContentType type,
+                                    bool isCompressed) ORTHANC_OVERRIDE
+      {
+        OrthancPluginMemoryBuffer customDataBuffer;
+
+        OrthancPluginErrorCode error = createAttachment_(&customDataBuffer,
+                                                         uuid.c_str(),
+                                                         resourceId.c_str(),
+                                                         Plugins::Convert(resourceLevel),
+                                                         content, size, Plugins::Convert(type),
+                                                         isCompressed);
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+
+        if (customDataBuffer.size > 0)
+        {
+          customData.assign(reinterpret_cast<char*>(customDataBuffer.data), 
+                            static_cast<size_t>(customDataBuffer.size));
+        }
+      }
+
+      virtual void Remove(const std::string& uuid,
+                          FileContentType type,
+                          const std::string& customData) ORTHANC_OVERRIDE
+      {
+        OrthancPluginErrorCode error = remove2_
+          (uuid.c_str(), customData.c_str(), Plugins::Convert(type));
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+      }
+
+      virtual IMemoryBuffer* Read(const std::string& uuid,
+                                  FileContentType type,
+                                  const std::string& customData) ORTHANC_OVERRIDE
+      {
+        std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer);
+
+        OrthancPluginMemoryBuffer64 buffer;
+        buffer.size = 0;
+        buffer.data = NULL;
+        
+        OrthancPluginErrorCode error = readWhole2_(&buffer, uuid.c_str(), customData.c_str(), Plugins::Convert(type));
+
+        if (error == OrthancPluginErrorCode_Success)
+        {
+          result->Assign(buffer.data, buffer.size, ::free);
+          return result.release();
+        }
+        else
+        {
+          GetErrorDictionary().LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+      }
+
+      virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                       FileContentType type,
+                                       uint64_t start /* inclusive */,
+                                       uint64_t end /* exclusive */,
+                                       const std::string& customData) ORTHANC_OVERRIDE
+      {
+        if (readRange2_ == NULL)
+        {
+          return RangeFromWhole(uuid, customData, type, start, end);
+        }
+        else
+        {
+          if (start > end)
+          {
+            throw OrthancException(ErrorCode_BadRange);
+          }
+          else if (start == end)
+          {
+            return new StringMemoryBuffer;
+          }
+          else
+          {
+            std::string range;
+            range.resize(end - start);
+            assert(!range.empty());
+
+            OrthancPluginMemoryBuffer64 buffer;
+            buffer.data = &range[0];
+            buffer.size = static_cast<uint64_t>(range.size());
+
+            OrthancPluginErrorCode error =
+              readRange2_(&buffer, uuid.c_str(), customData.c_str(), Plugins::Convert(type), start);
+
+            if (error == OrthancPluginErrorCode_Success)
+            {
+              return StringMemoryBuffer::CreateFromSwap(range);
+            }
+            else
+            {
+              GetErrorDictionary().LogError(error, true);
+              throw OrthancException(static_cast<ErrorCode>(error));
+            }
+          }
+        }
+      }
+
+      virtual bool HasReadRange() const ORTHANC_OVERRIDE
+      {
+        return (readRange2_ != NULL);
+      }
+
+    };
+
+
     class StorageAreaFactory : public boost::noncopyable
     {
     private:
       enum Version
       {
         Version1,
-        Version2
+        Version2,
+        Version3
       };
       
       SharedLibrary&                      sharedLibrary_;
       Version                             version_;
       _OrthancPluginRegisterStorageArea   callbacks_;
       _OrthancPluginRegisterStorageArea2  callbacks2_;
+      _OrthancPluginRegisterStorageArea3  callbacks3_;
       PluginsErrorDictionary&             errorDictionary_;
 
       static void WarnNoReadRange()
@@ -852,6 +1179,20 @@
         }
       }
 
+      StorageAreaFactory(SharedLibrary& sharedLibrary,
+                         const _OrthancPluginRegisterStorageArea3& callbacks,
+                         PluginsErrorDictionary&  errorDictionary) :
+        sharedLibrary_(sharedLibrary),
+        version_(Version3),
+        callbacks3_(callbacks),
+        errorDictionary_(errorDictionary)
+      {
+        if (callbacks.readRange == NULL)
+        {
+          WarnNoReadRange();
+        }
+      }
+
       SharedLibrary&  GetSharedLibrary()
       {
         return sharedLibrary_;
@@ -867,6 +1208,9 @@
           case Version2:
             return new PluginStorageArea2(callbacks2_, errorDictionary_);
 
+          case Version3:
+            return new PluginStorageArea3(callbacks3_, errorDictionary_);
+
           default:
             throw OrthancException(ErrorCode_InternalError);
         }
@@ -2458,101 +2802,6 @@
   }
 
 
-  class OrthancPlugins::IDicomInstance : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomInstance()
-    {
-    }
-
-    virtual bool CanBeFreed() const = 0;
-
-    virtual const DicomInstanceToStore& GetInstance() const = 0;
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance
-  {
-  private:
-    const DicomInstanceToStore&  instance_;
-
-  public:
-    explicit DicomInstanceFromCallback(const DicomInstanceToStore& instance) :
-      instance_(instance)
-    {
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return instance_;
-    };
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance
-  {
-  private:
-    std::string                            buffer_;
-    std::unique_ptr<DicomInstanceToStore>  instance_;
-
-  public:
-    DicomInstanceFromBuffer(const void* buffer,
-                            size_t size)
-    {
-      buffer_.assign(reinterpret_cast<const char*>(buffer), size);
-
-      instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_));
-      instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return true;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return *instance_;
-    };
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance
-  {
-  private:
-    std::unique_ptr<ParsedDicomFile>       parsed_;
-    std::unique_ptr<DicomInstanceToStore>  instance_;
-
-  public:
-    explicit DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) :
-      parsed_(transcoded.ReleaseAsParsedDicomFile())
-    {
-      if (parsed_.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-      
-      instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_));
-      instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return true;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return *instance_;
-    };
-  };
-
-
   void OrthancPlugins::SignalStoredInstance(const std::string& instanceId,
                                             const DicomInstanceToStore& instance,
                                             const Json::Value& simplifiedTags)
@@ -4830,7 +5079,8 @@
       {
         const _OrthancPluginStorageAreaCreate& p =
           *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters);
-        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
+        PluginStorageAreaBase& storage = *reinterpret_cast<PluginStorageAreaBase*>(p.storageArea);
+        std::string customDataNotUsed;
         storage.Create(p.uuid, p.content, static_cast<size_t>(p.size), Plugins::Convert(p.type));
         return true;
       }
@@ -4840,7 +5090,8 @@
         const _OrthancPluginStorageAreaRead& p =
           *reinterpret_cast<const _OrthancPluginStorageAreaRead*>(parameters);
         IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        std::unique_ptr<IMemoryBuffer> content(storage.Read(p.uuid, Plugins::Convert(p.type)));
+        std::string customDataNotUsed;
+        std::unique_ptr<IMemoryBuffer> content(storage.Read(p.uuid, Plugins::Convert(p.type), customDataNotUsed));
         CopyToMemoryBuffer(*p.target, content->GetData(), content->GetSize());
         return true;
       }
@@ -4850,7 +5101,8 @@
         const _OrthancPluginStorageAreaRemove& p =
           *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters);
         IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        storage.Remove(p.uuid, Plugins::Convert(p.type));
+        std::string customDataNotUsed;
+        storage.Remove(p.uuid, Plugins::Convert(p.type), customDataNotUsed);
         return true;
       }
 
@@ -5411,23 +5663,34 @@
 
       case _OrthancPluginService_RegisterStorageArea:
       case _OrthancPluginService_RegisterStorageArea2:
-      {
-        CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area";
-        
+      case _OrthancPluginService_RegisterStorageArea3:
+      {
         if (pimpl_->storageArea_.get() == NULL)
         {
           if (service == _OrthancPluginService_RegisterStorageArea)
           {
+            CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v1)";
+    
             const _OrthancPluginRegisterStorageArea& p = 
               *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters);
             pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary()));
           }
           else if (service == _OrthancPluginService_RegisterStorageArea2)
           {
+            CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v2)";
+
             const _OrthancPluginRegisterStorageArea2& p = 
               *reinterpret_cast<const _OrthancPluginRegisterStorageArea2*>(parameters);
             pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary()));
           }
+          else if (service == _OrthancPluginService_RegisterStorageArea3)
+          {
+            CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v3)";
+
+            const _OrthancPluginRegisterStorageArea3& p = 
+              *reinterpret_cast<const _OrthancPluginRegisterStorageArea3*>(parameters);
+            pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary()));
+          }
           else
           {
             throw OrthancException(ErrorCode_InternalError);
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Thu Sep 08 17:42:08 2022 +0200
@@ -87,11 +87,14 @@
     class HttpClientChunkedAnswer;
     class HttpServerChunkedReader;
     class IDicomInstance;
-    class DicomInstanceFromCallback;
     class DicomInstanceFromBuffer;
     class DicomInstanceFromTranscoded;
     class WebDavCollection;
-    
+
+public:
+    class DicomInstanceFromCallback;
+
+private:
     void RegisterRestCallback(const void* parameters,
                               bool lock);
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Sep 08 17:42:08 2022 +0200
@@ -469,6 +469,7 @@
     _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017,  /* New in Orthanc 1.10.0 */
     _OrthancPluginService_RegisterReceivedInstanceCallback = 1018,  /* New in Orthanc 1.10.0 */
     _OrthancPluginService_RegisterWebDavCollection = 1019,     /* New in Orthanc 1.10.1 */
+    _OrthancPluginService_RegisterStorageArea3 = 1020,         /* New in Orthanc 1.12.0 */
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -1259,7 +1260,7 @@
    * @param type The content type corresponding to this file. 
    * @return 0 if success, other value if error.
    * @ingroup Callbacks
-   * @deprecated New plugins should use OrthancPluginStorageRead2
+   * @deprecated New plugins should use OrthancPluginStorageReadWhole2 and OrthancPluginStorageReadRange2
    * 
    * @warning The "content" buffer *must* have been allocated using
    * the "malloc()" function of your C standard library (i.e. nor
@@ -1334,6 +1335,121 @@
 
 
 
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
+   *
+   * @param customData The custom data of the attachment (out)
+   * @param uuid The UUID of the file.
+   * @param instance The DICOM instance being stored.
+   * @param content The content of the file (might be compressed data, hence the need for the DICOM instance arg to access tags).
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreateInstance) (
+    OrthancPluginMemoryBuffer* customData,
+    const char* uuid,
+    const OrthancPluginDicomInstance*  instance,
+    const void* content,
+    int64_t size,
+    OrthancPluginContentType type,
+    bool isCompressed);
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
+   *
+   * @param customData The custom data of the attachment (out)
+   * @param uuid The UUID of the file.
+   * @param resourceId The resource ID the file is attached to.
+   * @param resourceType The resource Type the file is attached to.
+   * @param content The content of the file (might be compressed data, hence the need for the DICOM instance arg to access tags).
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreateAttachment) (
+    OrthancPluginMemoryBuffer* customData,
+    const char* uuid,
+    const char* resourceId,
+    OrthancPluginResourceType resourceType,
+    const void* content,
+    int64_t size,
+    OrthancPluginContentType type,
+    bool isCompressed);
+
+
+
+  /**
+   * @brief Callback for reading a whole file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * reads a whole file from the storage area.
+   *
+   * @param 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.
+   * @param uuid The UUID of the file of interest.
+   * @param customData The custom data of the file to be removed.
+   * @param type The content type corresponding to this file. 
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageReadWhole2) (
+    OrthancPluginMemoryBuffer64* target,
+    const char* uuid,
+    const char* customData,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for reading a range of a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * reads a portion of a file from the storage area. Orthanc
+   * indicates the start position and the length of the range.
+   *
+   * @param 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.
+   * @param uuid The UUID of the file of interest.
+   * @param customData The custom data of the file to be removed.
+   * @param type The content type corresponding to this file. 
+   * @param rangeStart Start position of the requested range in the file.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) (
+    OrthancPluginMemoryBuffer64* target,
+    const char* uuid,
+    const char* customData,
+    OrthancPluginContentType type,
+    uint64_t rangeStart);
+
+
+
+  /**
+   * @brief Callback for removing a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area.
+   *
+   * @param uuid The UUID of the file to be removed.
+   * @param customData The custom data of the file to be removed.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) (
+    const char* uuid,
+    const char* customData,
+    OrthancPluginContentType type);
+
+
   /**
    * @brief Callback to handle the C-Find SCP requests for worklists.
    *
@@ -9034,6 +9150,50 @@
   }
 
 
+  typedef struct
+  {
+    OrthancPluginStorageCreateInstance       createInstance;
+    OrthancPluginStorageCreateAttachment     createAttachment;
+    OrthancPluginStorageReadWhole2    readWhole;
+    OrthancPluginStorageReadRange2    readRange;
+    OrthancPluginStorageRemove2       remove;
+  } _OrthancPluginRegisterStorageArea3;
+
+  /**
+   * @brief Register a custom storage area, with support for custom data.
+   *
+   * This function registers a custom storage area, to replace the
+   * built-in way Orthanc stores its files on the filesystem. This
+   * function must be called during the initialization of the plugin,
+   * i.e. inside the OrthancPluginInitialize() public function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param create The callback function to store a file on the custom storage area.
+   * @param readWhole The callback function to read a whole file from the custom storage area.
+   * @param readRange The callback function to read some range of a file from the custom storage area.
+   * If this feature is not supported by the plugin, this value can be set to NULL.
+   * @param remove The callback function to remove a file from the custom storage area.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3(
+    OrthancPluginContext*             context,
+    // OrthancPluginStorageGetCustomData getCustomData,
+    OrthancPluginStorageCreateInstance         createInstance,
+    OrthancPluginStorageCreateAttachment       createAttachement,
+    OrthancPluginStorageReadWhole2    readWhole,
+    OrthancPluginStorageReadRange2    readRange,
+    OrthancPluginStorageRemove2       remove)
+  {
+    _OrthancPluginRegisterStorageArea3 params;
+    // params.getCustomData = getCustomData;
+    params.createAttachment = createAttachement;
+    params.createInstance = createInstance;
+    params.readWhole = readWhole;
+    params.readRange = readRange;
+    params.remove = remove;
+    context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, &params);
+  }
+
 #ifdef  __cplusplus
 }
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/AdvancedStorage/CMakeLists.txt	Thu Sep 08 17:42:08 2022 +0200
@@ -0,0 +1,81 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2022 Osimis S.A., Belgium
+# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+cmake_minimum_required(VERSION 2.8)
+cmake_policy(SET CMP0058 NEW)
+
+project(AdvancedStorage)
+
+SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin")
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake)
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+include(${CMAKE_CURRENT_LIST_DIR}/../Common/OrthancPluginsExports.cmake)
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/WindowsResources.py
+    ${PLUGIN_VERSION} AdvancedStorage AdvancedStorage.dll "Orthanc plugin to extend Orthanc Storage"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/AdvancedStorage.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  list(APPEND ADDITIONAL_RESOURCES ${AUTOGENERATED_DIR}/AdvancedStorage.rc)
+endif()  
+
+add_definitions(
+  -DHAS_ORTHANC_EXCEPTION=1
+  -DORTHANC_PLUGIN_VERSION="${PLUGIN_VERSION}"
+  )
+
+include_directories(
+  ${CMAKE_SOURCE_DIR}/../../Include/
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/
+  )
+
+add_library(AdvancedStorage SHARED
+  ${ADDITIONAL_RESOURCES}
+  ${AUTOGENERATED_SOURCES}
+  ${ORTHANC_CORE_SOURCES_DEPENDENCIES}
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Enumerations.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Logging.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/OrthancException.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SystemToolbox.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Toolbox.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+  Plugin.cpp
+  )
+
+set_target_properties(
+  AdvancedStorage PROPERTIES 
+  VERSION ${PLUGIN_VERSION} 
+  SOVERSION ${PLUGIN_VERSION}
+  )
+
+install(
+  TARGETS AdvancedStorage
+  DESTINATION .
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -0,0 +1,504 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/SystemToolbox.h"
+#include "../../../../OrthancFramework/Sources/Toolbox.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/iostreams/device/file_descriptor.hpp>
+#include <boost/iostreams/stream.hpp>
+
+
+#include <json/value.h>
+#include <json/writer.h>
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+#include <map>
+#include <list>
+#include <time.h>
+
+namespace fs = boost::filesystem;
+
+fs::path absoluteRootPath_;
+bool fsyncOnWrite_ = true;
+
+
+fs::path GetLegacyRelativePath(const std::string& uuid)
+{
+
+  if (!Orthanc::Toolbox::IsUuid(uuid))
+  {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+
+  fs::path path = absoluteRootPath_;
+
+  path /= std::string(&uuid[0], &uuid[2]);
+  path /= std::string(&uuid[2], &uuid[4]);
+  path /= uuid;
+
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+  path.make_preferred();
+#endif
+
+  return path;
+}
+
+fs::path GetAbsolutePath(const std::string& uuid, const std::string& customData)
+{
+  fs::path path = absoluteRootPath_;
+
+  if (!customData.empty())
+  {
+    if (customData.substr(0, 2) == "1.") // version 1
+    {
+      path /= customData.substr(2);
+    }
+    else
+    {
+      throw "TODO: unknown version";
+    }
+
+  }
+  else
+  {
+    path /= GetLegacyRelativePath(uuid);
+  }
+
+  path.make_preferred();
+  return path;
+}
+
+std::string GetCustomData(const fs::path& path)
+{
+  return std::string("1.") + path.string(); // prefix the relative path with a version
+}
+
+void AddDateDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
+{
+  if (tags.isMember(tagName) && tags[tagName].asString().size() == 8)
+  {
+    std::string date = tags[tagName].asString();
+    path /= date.substr(0, 4);
+    path /= date.substr(4, 2);
+    path /= date.substr(6, 2);
+  }
+  else if (defaultValue != NULL)
+  {
+    path /= defaultValue;
+  }
+}
+
+void AddSringDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
+{
+  if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0)
+  {
+    path /= tags[tagName].asString();
+  }
+  else if (defaultValue != NULL)
+  {
+    path /= defaultValue;
+  }
+}
+
+void AddIntDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, size_t zeroPaddingWidth = 0, const char* defaultValue = NULL)
+{
+  if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0)
+  {
+    std::string tagValue = tags[tagName].asString();
+    if (zeroPaddingWidth > 0 && tagValue.size() < zeroPaddingWidth)
+    {
+      std::string padding(zeroPaddingWidth - tagValue.size(), '0');
+      path /= padding + tagValue; 
+    }
+    else
+    {
+      path /= tagValue;
+    }
+  }
+  else if (defaultValue != NULL)
+  {
+    path /= defaultValue;
+  }
+}
+
+fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed)
+{
+  fs::path path;
+  
+  if (type == OrthancPluginContentType_Dicom || type == OrthancPluginContentType_DicomUntilPixelData)
+  {
+    // TODO: allow customization ... note: right now, we always need the uuid in the path !!
+
+    AddDateDicomTagToPath(path, tags, "StudyDate", "NO_STUDY_DATE");
+    AddSringDicomTagToPath(path, tags, "PatientID");  // no default value, tag is always present if the instance is accepted by Orthanc  
+    AddSringDicomTagToPath(path, tags, "StudyInstanceUID");
+    AddSringDicomTagToPath(path, tags, "SeriesInstanceUID");
+    //AddIntDicomTagToPath(path, tags, "InstanceNumber", 8, uuid);
+    path /= uuid;
+  }
+  else
+  {
+    path = GetLegacyRelativePath(uuid);
+  }
+
+  std::string extension;
+
+  switch (type)
+  {
+    case OrthancPluginContentType_Dicom:
+      extension = ".dcm";
+      break;
+    case OrthancPluginContentType_DicomUntilPixelData:
+      extension = ".dcm.head";
+      break;
+    default:
+      extension = ".unk";
+  }
+  if (isCompressed)
+  {
+    extension = extension + ".cmp"; // compression is zlib + size -> we can not use the .zip extension
+  }
+  
+  path += extension;
+
+  return path;
+}
+
+
+OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData,
+                                             const char* uuid,
+                                             const Json::Value& tags,
+                                             const void* content,
+                                             int64_t size,
+                                             OrthancPluginContentType type,
+                                             bool isCompressed)
+{
+  fs::path relativePath = GetRelativePathFromTags(tags, uuid, type, isCompressed);
+  std::string customDataString = GetCustomData(relativePath);
+
+  fs::path absolutePath = absoluteRootPath_ / relativePath;
+
+  if (fs::exists(absolutePath))
+  {
+    // Extremely unlikely case if uuid is included in the path: This Uuid has already been created
+    // in the past.
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+
+    // TODO for the future: handle duplicates path (e.g: there's no uuid in the path and we are uploading the same file again)
+    // OrthancPlugins::LogWarning(std::string("Overwriting file \"") + path.string() + "\" (" + uuid + ")");
+  }
+
+  if (fs::exists(absolutePath.parent_path()))
+  {
+    if (!fs::is_directory(absolutePath.parent_path()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_DirectoryOverFile);
+    }
+  }
+  else
+  {
+    if (!fs::create_directories(absolutePath.parent_path()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_FileStorageCannotWrite);
+    }
+  }
+
+  Orthanc::SystemToolbox::WriteFile(content, size, absolutePath.string(), fsyncOnWrite_);
+
+  OrthancPluginCreateMemoryBuffer(OrthancPlugins::GetGlobalContext(), customData, customDataString.size());
+  memcpy(customData->data, customDataString.data(), customDataString.size());
+
+  return OrthancPluginErrorCode_Success;
+
+}
+
+OrthancPluginErrorCode StorageCreateInstance(OrthancPluginMemoryBuffer* customData,
+                                             const char* uuid,
+                                             const OrthancPluginDicomInstance*  instance,
+                                             const void* content,
+                                             int64_t size,
+                                             OrthancPluginContentType type,
+                                             bool isCompressed)
+{
+  try
+  {
+    OrthancPlugins::LogInfo(std::string("Creating instance attachment \"") + uuid + "\"");
+
+    OrthancPlugins::DicomInstance dicomInstance(instance);
+    Json::Value tags;
+    dicomInstance.GetSimplifiedJson(tags);
+
+    return StorageCreate(customData, uuid, tags, content, size, type, isCompressed);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+  }
+  catch (...)
+  {
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode StorageCreateAttachment(OrthancPluginMemoryBuffer* customData,
+                                               const char* uuid,
+                                               const char* resourceId,
+                                               OrthancPluginResourceType resourceType,
+                                               const void* content,
+                                               int64_t size,
+                                               OrthancPluginContentType type,
+                                               bool isCompressed)
+{
+  try
+  {
+    OrthancPlugins::LogInfo(std::string("Creating attachment \"") + uuid + "\"");
+
+    //TODO_CUSTOM_DATA: get tags from the Rest API...
+    Json::Value tags;
+
+    return StorageCreate(customData, uuid, tags, content, size, type, isCompressed);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+  }
+  catch (...)
+  {
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target,
+                                        const char* uuid,
+                                        const char* customData,
+                                        OrthancPluginContentType type)
+{
+  OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\"");
+
+  std::string path = GetAbsolutePath(uuid, customData).string();
+
+  if (!Orthanc::SystemToolbox::IsRegularFile(path))
+  {
+    OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+    return OrthancPluginErrorCode_InexistentFile;
+  }
+
+  try
+  {
+    fs::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+      return OrthancPluginErrorCode_InexistentFile;
+    }
+
+    // get file size
+    f.seekg(0, std::ios::end);
+    std::streamsize fileSize = f.tellg();
+    f.seekg(0, std::ios::beg);
+
+    // The ReadWhole must allocate the buffer itself
+    if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, fileSize) != OrthancPluginErrorCode_Success)
+    {
+      OrthancPlugins::LogError(std::string("Unable to allocate memory to read file: ") + path);
+      return OrthancPluginErrorCode_NotEnoughMemory;
+    }
+
+    if (fileSize != 0)
+    {
+      f.read(reinterpret_cast<char*>(target->data), fileSize);
+    }
+
+    f.close();
+  }
+  catch (...)
+  {
+    OrthancPlugins::LogError(std::string("Unexpected error while reading: ") + path);
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode StorageReadRange (OrthancPluginMemoryBuffer64* target,
+                                         const char* uuid,
+                                         const char* customData,
+                                         OrthancPluginContentType type,
+                                         uint64_t rangeStart)
+{
+  OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\"");
+
+  std::string path = GetAbsolutePath(uuid, customData).string();
+
+  if (!Orthanc::SystemToolbox::IsRegularFile(path))
+  {
+    OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+    return OrthancPluginErrorCode_InexistentFile;
+  }
+
+  try
+  {
+    fs::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+      return OrthancPluginErrorCode_InexistentFile;
+    }
+
+    f.seekg(rangeStart, std::ios::beg);
+
+    // The ReadRange uses a target that has already been allocated by orthanc
+    f.read(reinterpret_cast<char*>(target->data), target->size);
+
+    f.close();
+  }
+  catch (...)
+  {
+    OrthancPlugins::LogError(std::string("Unexpected error while reading: ") + path);
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode StorageRemove (const char* uuid,
+                                      const char* customData,
+                                      OrthancPluginContentType type)
+{
+  // LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type);
+
+  fs::path p = GetAbsolutePath(uuid, customData);
+
+  try
+  {
+    fs::remove(p);
+  }
+  catch (...)
+  {
+    // Ignore the error
+  }
+
+  // Remove the empty parent directories, (ignoring the error code if these directories are not empty)
+
+  try
+  {
+    fs::path parent = p.parent_path();
+
+    while (parent != absoluteRootPath_)
+    {
+      fs::remove(parent);
+      parent = parent.parent_path();
+    }
+  }
+  catch (...)
+  {
+    // Ignore the error
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+extern "C"
+{
+
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    OrthancPlugins::SetGlobalContext(context);
+    Orthanc::Logging::InitializePluginContext(context);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(context) == 0)
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      return -1;
+    }
+
+    OrthancPlugins::LogWarning("AdvancedStorage plugin is initializing");
+    OrthancPluginSetDescription(context, "Provides alternative layout for your storage.");
+
+    OrthancPlugins::OrthancConfiguration orthancConfiguration;
+
+    OrthancPlugins::OrthancConfiguration advancedStorage;
+    orthancConfiguration.GetSection(advancedStorage, "AdvancedStorage");
+
+    bool enabled = advancedStorage.GetBooleanValue("Enable", false);
+    if (enabled)
+    {
+      /*
+        {
+          "AdvancedStorage": {
+            
+            // Enables/disables the plugin
+            "Enable": false,
+          }
+        }
+      */
+
+      absoluteRootPath_ = fs::absolute(fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage")));
+      LOG(WARNING) << "AdvancedStorage - Path to the storage area: " << absoluteRootPath_.string();
+
+      OrthancPluginRegisterStorageArea3(context, StorageCreateInstance, StorageCreateAttachment, StorageReadWhole, StorageReadRange, StorageRemove);
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("AdvancedStorage plugin is disabled by the configuration file");
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPlugins::LogWarning("AdvancedStorage plugin is finalizing");
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "advanced-storage";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- a/OrthancServer/Resources/Configuration.json	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Resources/Configuration.json	Thu Sep 08 17:42:08 2022 +0200
@@ -10,11 +10,15 @@
   // Path to the directory that holds the heavyweight files (i.e. the
   // raw DICOM instances). Backslashes must be either escaped by
   // doubling them, or replaced by forward slashes "/".
+  // If a relative path is provided, it is relative to the configuration
+  // file path.  It is advised to provide an absolute path.
   "StorageDirectory" : "OrthancStorage",
 
   // Path to the directory that holds the SQLite index (if unset, the
   // value of StorageDirectory is used). This index could be stored on
   // a RAM-drive or a SSD device for performance reasons.
+  // If a relative path is provided, it is relative to the configuration
+  // file path.  It is advised to provide an absolute path.
   "IndexDirectory" : "OrthancStorage",
 
   // Path to the directory where Orthanc stores its large temporary
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/DowngradeFrom7to6.sql	Thu Sep 08 17:42:08 2022 +0200
@@ -0,0 +1,45 @@
+-- Orthanc - A Lightweight, RESTful DICOM Store
+-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+-- Department, University Hospital of Liege, Belgium
+-- Copyright (C) 2017-2022 Osimis S.A., Belgium
+-- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+--
+-- This program is free software: you can redistribute it and/or
+-- modify it under the terms of the GNU 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
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+--
+-- This SQLite script updates the version of the Orthanc database from 6 to 7.
+--
+
+-- Add a new column to AttachedFiles
+
+ALTER TABLE AttachedFiles DROP COLUMN metadata;
+
+-- update the triggers (back to v6)
+DROP TRIGGER AttachedFileDeleted
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           old.uncompressedMD5, old.compressedMD5);
+END;
+
+
+-- Change the database version
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+
+UPDATE GlobalProperties SET value="6" WHERE property=1;
+
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Sources/Database/PrepareDatabase.sql	Thu Sep 08 17:42:08 2022 +0200
@@ -63,6 +63,7 @@
        compressionType INTEGER,
        uncompressedMD5 TEXT,  -- New in Orthanc 0.7.3 (database v4)
        compressedMD5 TEXT,    -- New in Orthanc 0.7.3 (database v4)
+       customData TEXT,       -- New in Orthanc 1.12.0 (database v7)
        PRIMARY KEY(id, fileType)
        );              
 
@@ -114,7 +115,9 @@
   SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
                            old.compressionType, old.compressedSize,
                            -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
-                           old.uncompressedMD5, old.compressedMD5);
+                           old.uncompressedMD5, old.compressedMD5,
+                           -- customData is new in Orthanc 1.12.0 (database v7)
+                           old.customData);
 END;
 
 CREATE TRIGGER ResourceDeleted
@@ -143,4 +146,4 @@
 
 -- Set the version of the database schema
 -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
-INSERT INTO GlobalProperties VALUES (1, "6");
+INSERT INTO GlobalProperties VALUES (1, "7");
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -324,7 +324,9 @@
                                int64_t revision) ORTHANC_OVERRIDE
     {
       // TODO - REVISIONS
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+        "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5, customData) "
+        "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)");
       s.BindInt64(0, id);
       s.BindInt(1, attachment.GetContentType());
       s.BindString(2, attachment.GetUuid());
@@ -333,10 +335,10 @@
       s.BindInt(5, attachment.GetCompressionType());
       s.BindString(6, attachment.GetUncompressedMD5());
       s.BindString(7, attachment.GetCompressedMD5());
+      s.BindString(8, attachment.GetCustomData());
       s.Run();
     }
 
-
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
                                       const std::vector<DatabaseConstraint>& lookup,
@@ -801,7 +803,7 @@
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE, 
                           "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
-                          "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
+                          "uncompressedMD5, compressedMD5, customData FROM AttachedFiles WHERE id=? AND fileType=?");
       s.BindInt64(0, id);
       s.BindInt(1, contentType);
 
@@ -817,7 +819,8 @@
                               s.ColumnString(4),
                               static_cast<CompressionType>(s.ColumnInt(2)),
                               s.ColumnInt64(3),
-                              s.ColumnString(5));
+                              s.ColumnString(5),
+                              s.ColumnString(6));
         revision = 0;   // TODO - REVISIONS
         return true;
       }
@@ -1098,14 +1101,14 @@
 
     virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
     {
-      return 7;
+      return 8;
     }
 
     virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
     {
       if (sqlite_.activeTransaction_ != NULL)
       {
-        std::string uncompressedMD5, compressedMD5;
+        std::string uncompressedMD5, compressedMD5, customData;
 
         if (!context.IsNullValue(5))
         {
@@ -1117,13 +1120,19 @@
           compressedMD5 = context.GetStringValue(6);
         }
 
+        if (!context.IsNullValue(7))
+        {
+          customData = context.GetStringValue(7);
+        }
+
         FileInfo info(context.GetStringValue(0),
                       static_cast<FileContentType>(context.GetIntValue(1)),
                       static_cast<uint64_t>(context.GetInt64Value(2)),
                       uncompressedMD5,
                       static_cast<CompressionType>(context.GetIntValue(3)),
                       static_cast<uint64_t>(context.GetInt64Value(4)),
-                      compressedMD5);
+                      compressedMD5,
+                      customData);
 
         sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info);
       }
@@ -1351,7 +1360,7 @@
       }
 
       // New in Orthanc 1.5.1
-      if (version_ == 6)
+      if (version_ >= 6)
       {
         if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true /* unused in SQLite */) ||
             tmp != "1")
@@ -1393,7 +1402,7 @@
   {
     boost::mutex::scoped_lock lock(mutex_);
 
-    if (targetVersion != 6)
+    if (targetVersion != 7)
     {
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
     }
@@ -1403,7 +1412,8 @@
     if (version_ != 3 &&
         version_ != 4 &&
         version_ != 5 &&
-        version_ != 6)
+        version_ != 6 &&
+        version_ != 7)
     {
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
     }
@@ -1444,6 +1454,14 @@
       
       version_ = 6;
     }
+
+    if (version_ == 6)
+    {
+      LOG(WARNING) << "Upgrading database version from 6 to 7";
+      ExecuteUpgradeScript(db_, ServerResources::UPGRADE_DATABASE_6_TO_7);
+      version_ = 7;
+    }
+
   }
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Upgrade6To7.sql	Thu Sep 08 17:42:08 2022 +0200
@@ -0,0 +1,46 @@
+-- Orthanc - A Lightweight, RESTful DICOM Store
+-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+-- Department, University Hospital of Liege, Belgium
+-- Copyright (C) 2017-2022 Osimis S.A., Belgium
+-- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+--
+-- This program is free software: you can redistribute it and/or
+-- modify it under the terms of the GNU 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
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+--
+-- This SQLite script updates the version of the Orthanc database from 6 to 7.
+--
+
+-- Add a new column to AttachedFiles
+
+ALTER TABLE AttachedFiles ADD COLUMN customData TEXT;
+
+-- update the triggers
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
+                           old.uncompressedMD5, old.compressedMD5,
+                           -- Next argument new in Orthanc 1.12.0 (database v7)
+                           old.customData);
+END;
+
+-- Change the database version
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+
+UPDATE GlobalProperties SET value="7" WHERE property=1;
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -420,7 +420,7 @@
   {
     // Anonymous namespace to avoid clashes between compilation modules
 
-    class FilesystemStorageWithoutDicom : public IStorageArea
+    class FilesystemStorageWithoutDicom : public ICoreStorageArea
     {
     private:
       FilesystemStorage storage_;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -2393,6 +2393,7 @@
  
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
+    ResourceType resourceType = StringToResourceType(call.GetFullUri()[0].c_str());
 
     FileContentType contentType = StringToContentType(name);
     if (IsUserContentType(contentType))  // It is forbidden to modify internal attachments
@@ -2416,7 +2417,7 @@
       }
 
       int64_t newRevision;
-      context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(),
+      context.AddAttachment(newRevision, publicId, resourceType, StringToContentType(name), call.GetBodyData(),
                             call.GetBodySize(), hasOldRevision, oldRevision, oldMD5);
 
       SetBufferContentETag(call.GetOutput(), newRevision, call.GetBodyData(), call.GetBodySize());  // New in Orthanc 1.9.2
@@ -2536,9 +2537,10 @@
 
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
+    ResourceType resourceType = StringToResourceType(call.GetFullUri()[0].c_str());
     FileContentType contentType = StringToContentType(name);
 
-    OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression);
+    OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, resourceType, contentType, compression);
     call.GetOutput().AnswerBuffer("{}", MimeType_Json);
   }
 
--- a/OrthancServer/Sources/ServerContext.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -486,10 +486,11 @@
 
 
   void ServerContext::RemoveFile(const std::string& fileUuid,
-                                 FileContentType type)
+                                 FileContentType type,
+                                 const std::string& customData)
   {
     StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
-    accessor.Remove(fileUuid, type);
+    accessor.Remove(fileUuid, type, customData);
   }
 
 
@@ -599,8 +600,11 @@
       // TODO Should we use "gzip" instead?
       CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
-      FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), 
-                                          FileContentType_Dicom, compression, storeMD5_);
+      std::string dicomCustomData;
+      std::string dicomUuid = Toolbox::GenerateUuid();
+
+      FileInfo dicomInfo = accessor.WriteInstance(dicomCustomData, dicom, dicom.GetBufferData(), dicom.GetBufferSize(), 
+                                          FileContentType_Dicom, compression, storeMD5_, dicomUuid);
 
       ServerIndex::Attachments attachments;
       attachments.push_back(dicomInfo);
@@ -610,8 +614,11 @@
           (!area_.HasReadRange() ||
            compressionEnabled_))
       {
-        dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, 
-                                             FileContentType_DicomUntilPixelData, compression, storeMD5_);
+        std::string dicomHeaderCustomData;
+        std::string dicomHeaderUuid = Toolbox::GenerateUuid();
+
+        dicomUntilPixelData = accessor.WriteInstance(dicomHeaderCustomData, dicom, dicom.GetBufferData(), pixelDataOffset, 
+                                             FileContentType_DicomUntilPixelData, compression, storeMD5_, dicomHeaderUuid);
         attachments.push_back(dicomUntilPixelData);
       }
 
@@ -858,6 +865,7 @@
 
 
   void ServerContext::ChangeAttachmentCompression(const std::string& resourceId,
+                                                  ResourceType resourceType,
                                                   FileContentType attachmentType,
                                                   CompressionType compression)
   {
@@ -884,8 +892,25 @@
     StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
     accessor.Read(content, attachment);
 
-    FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(),
-                                       content.size(), attachmentType, compression, storeMD5_);
+    std::string newUuid = Toolbox::GenerateUuid();
+    std::string newCustomData;
+    FileInfo modified;
+
+    // if (attachmentType == FileContentType_Dicom || attachmentType == FileContentType_DicomUntilPixelData)
+    // {
+    //   // DicomInstanceToStore instance;
+    //   // TODO_CUSTOM_DATA: get the Instance such that we can call accessor.GetCustomData ...
+    //   // modified = accessor.WriteInstance(newCustomData, instance, content.empty() ? NULL : content.c_str(),
+    //   //                                 content.size(), attachmentType, compression, storeMD5_, newUuid);
+    // }
+    // else
+    {
+      ResourceType resourceType = ResourceType_Instance; //TODO_CUSTOM_DATA: get it from above in the stack
+      modified = accessor.WriteAttachment(newCustomData, resourceId, resourceType, content.empty() ? NULL : content.c_str(),
+                                       content.size(), attachmentType, compression, storeMD5_, newUuid);
+    }
+
+
 
     try
     {
@@ -1004,7 +1029,7 @@
         
         {
           StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
-          accessor.ReadStartRange(dicom, attachment.GetUuid(), FileContentType_Dicom, pixelDataOffset);
+          accessor.ReadStartRange(dicom, attachment.GetUuid(), FileContentType_Dicom, pixelDataOffset, attachment.GetCustomData());
         }
         
         assert(dicom.size() == pixelDataOffset);
@@ -1070,9 +1095,9 @@
                 compressionEnabled_)
             {
               int64_t newRevision;
-              AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData,
+              AddAttachment(newRevision, instancePublicId, ResourceType_Instance, FileContentType_DicomUntilPixelData,
                             dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset,
-                            false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */);
+                             false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */);
             }
           }
         }
@@ -1134,7 +1159,7 @@
 
         StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
 
-        accessor.ReadStartRange(dicom, attachment.GetUuid(), attachment.GetContentType(), pixelDataOffset);
+        accessor.ReadStartRange(dicom, attachment.GetUuid(), attachment.GetContentType(), pixelDataOffset, attachment.GetCustomData());
         assert(dicom.size() == pixelDataOffset);
         
         return true;   // Success
@@ -1262,6 +1287,7 @@
 
   bool ServerContext::AddAttachment(int64_t& newRevision,
                                     const std::string& resourceId,
+                                    ResourceType resourceType,
                                     FileContentType attachmentType,
                                     const void* data,
                                     size_t size,
@@ -1275,7 +1301,13 @@
     CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
     StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry());
-    FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
+    
+    std::string uuid = Toolbox::GenerateUuid();
+    std::string customData;
+
+    assert(attachmentType != FileContentType_Dicom && attachmentType != FileContentType_DicomUntilPixelData); // this method can not be used to store instances
+
+    FileInfo attachment = accessor.WriteAttachment(customData, resourceId, resourceType, data, size, attachmentType, compression, storeMD5_, uuid);
 
     try
     {
--- a/OrthancServer/Sources/ServerContext.h	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Thu Sep 08 17:42:08 2022 +0200
@@ -268,7 +268,8 @@
 
     // This method must only be called from "ServerIndex"!
     void RemoveFile(const std::string& fileUuid,
-                    FileContentType type);
+                    FileContentType type,
+                    const std::string& customData);
 
     // This DicomModification object is intended to be used as a
     // "rules engine" when de-identifying logs for C-Find, C-Get, and
@@ -325,6 +326,7 @@
 
     bool AddAttachment(int64_t& newRevision,
                        const std::string& resourceId,
+                       ResourceType resourceType,
                        FileContentType attachmentType,
                        const void* data,
                        size_t size,
@@ -346,6 +348,7 @@
                           FileContentType content);
 
     void ChangeAttachmentCompression(const std::string& resourceId,
+                                     ResourceType resourceType,
                                      FileContentType attachmentType,
                                      CompressionType compression);
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -46,12 +46,14 @@
     struct FileToRemove
     {
     private:
-      std::string  uuid_;
-      FileContentType  type_;
+      std::string       uuid_;
+      std::string       customData_;
+      FileContentType   type_;
 
     public:
       explicit FileToRemove(const FileInfo& info) :
-        uuid_(info.GetUuid()), 
+        uuid_(info.GetUuid()),
+        customData_(info.GetCustomData()),
         type_(info.GetContentType())
       {
       }
@@ -61,6 +63,11 @@
         return uuid_;
       }
 
+      const std::string& GetCustomData() const
+      {
+        return customData_;
+      }
+
       FileContentType GetContentType() const 
       {
         return type_;
@@ -94,7 +101,7 @@
       {
         try
         {
-          context_.RemoveFile(it->GetUuid(), it->GetContentType());
+          context_.RemoveFile(it->GetUuid(), it->GetContentType(), it->GetCustomData());
         }
         catch (OrthancException& e)
         {
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Wed Aug 31 10:36:38 2022 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -291,9 +291,9 @@
   ASSERT_EQ(0u, md.size());
 
   transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", 
-                                             CompressionType_ZlibWithSize, 21, "compressedMD5"), 42);
-  transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"), 43);
-  transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"), 44);
+                                             CompressionType_ZlibWithSize, 21, "compressedMD5", "customData"), 42);
+  transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5", "customData"), 43);
+  transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5", "customData"), 44);
   
   // TODO - REVISIONS - "42" is revision number, that is not currently stored (*)
   transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42);
@@ -401,7 +401,7 @@
 
   std::string tmp;
   ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true));
-  ASSERT_EQ("6", tmp);
+  ASSERT_EQ("7", tmp);
   ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep, true));
   ASSERT_EQ("World", tmp);
   ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true));
@@ -473,7 +473,7 @@
     std::string p = "Patient " + boost::lexical_cast<std::string>(i);
     patients.push_back(transaction_->CreateResource(p, ResourceType_Patient));
     transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, 
-                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
+                                                      "md5-" + boost::lexical_cast<std::string>(i), "customData"), 42);
     ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
   }
 
@@ -534,7 +534,7 @@
     std::string p = "Patient " + boost::lexical_cast<std::string>(i);
     patients.push_back(transaction_->CreateResource(p, ResourceType_Patient));
     transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10,
-                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
+                                                      "md5-" + boost::lexical_cast<std::string>(i), "customData"), 42);
     ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
   }
 
@@ -774,7 +774,7 @@
 
   for (size_t i = 0; i < ids.size(); i++)
   {
-    FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5");
+    FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5", "customData");
     int64_t revision = -1;
     index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1, "");
     ASSERT_EQ(0, revision);
--- a/TODO	Wed Aug 31 10:36:38 2022 +0200
+++ b/TODO	Thu Sep 08 17:42:08 2022 +0200
@@ -1,3 +1,16 @@
+TODO_CUSTOM_DATA branch
+- add REVISIONS in SQLite since we change the DB schema
+- upgrade DB automatically such that it does not need a specific launch with --update , add --downgrade + --no-auto-upgrade command lines
+- expand the DB plugin SDK to handle customDATA
+- implement OrthancPluginDataBaseV4
+- handle all TODO_CUSTOM_DATA
+- check /attachments/...  routes for path returned
+- AdvancedStoragePlugin 
+  - support multiple roots (multiple disks)
+  - show warning if a tag is missing when generating the path from tags (option to disable this warning)
+  - generate path from tags from resource (CreateAttachment)
+
+
 =======================
 === Orthanc Roadmap ===
 =======================