changeset 6112:91dde382f780 attach-custom-data

integration mainline->attach-custom-data
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 16 May 2025 17:19:35 +0200
parents e08274ea166e (diff) 4e7f565e2604 (current diff)
children 7dcc5e0a23b7
files NEWS OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp
diffstat 56 files changed, 3221 insertions(+), 657 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Fri May 16 17:15:54 2025 +0200
+++ b/.hgignore	Fri May 16 17:19:35 2025 +0200
@@ -15,3 +15,4 @@
 .project
 Resources/Testing/Issue32/Java/bin
 Resources/Testing/Issue32/Java/target
+build/
--- a/NEWS	Fri May 16 17:15:54 2025 +0200
+++ b/NEWS	Fri May 16 17:19:35 2025 +0200
@@ -1,6 +1,19 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* SQLite default DB engine now supports metadata and attachment revisions.
+* Upgraded the DB to allow plugins to store customData for each attachment.
+
+Plugins
+-------
+
+* New database plugin SDK (vX) to handle customData for attachments, key-value stores and queues.
+* New storage plugin SDK (v3) to handle customData for attachments.
+* New functions available to all plugins to store key-values and queues.
+
 Maintenance
 -----------
 
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri May 16 17:19:35 2025 +0200
@@ -170,6 +170,7 @@
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Enumerations.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FileInfo.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/MemoryStorageArea.cpp
+  ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/PluginStorageAreaAdapter.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/CStringMatcher.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpContentNegociation.cpp
   ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpToolbox.cpp
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Fri May 16 17:19:35 2025 +0200
@@ -42,7 +42,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),
@@ -50,7 +51,8 @@
     uncompressedMD5_(md5),
     compressionType_(CompressionType_None),
     compressedSize_(size),
-    compressedMD5_(md5)
+    compressedMD5_(md5),
+    customData_(customData)
   {
   }
 
@@ -61,7 +63,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),
@@ -69,7 +72,8 @@
     uncompressedMD5_(uncompressedMD5),
     compressionType_(compressionType),
     compressedSize_(compressedSize),
-    compressedMD5_(compressedMD5)
+    compressedMD5_(compressedMD5),
+    customData_(customData)
   {
   }
 
@@ -169,4 +173,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	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.h	Fri May 16 17:19:35 2025 +0200
@@ -42,6 +42,7 @@
     CompressionType  compressionType_;
     uint64_t         compressedSize_;
     std::string      compressedMD5_;
+    std::string      customData_;
 
   public:
     FileInfo();
@@ -52,7 +53,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.
@@ -63,7 +65,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;
     
@@ -80,5 +83,7 @@
     const std::string& GetCompressedMD5() const;
 
     const std::string& GetUncompressedMD5() const;
+
+    const std::string& GetCustomData() const;
   };
 }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Fri May 16 17:19:35 2025 +0200
@@ -187,8 +187,8 @@
   }
 
 
-  IMemoryBuffer* FilesystemStorage::Read(const std::string& uuid,
-                                         FileContentType type)
+  IMemoryBuffer* FilesystemStorage::ReadWhole(const std::string& uuid,
+                                              FileContentType type)
   {
     Toolbox::ElapsedTimer timer;
     LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
@@ -221,12 +221,6 @@
   }
 
 
-  bool FilesystemStorage::HasReadRange() const
-  {
-    return true;
-  }
-
-
   uintmax_t FilesystemStorage::GetSize(const std::string& uuid) const
   {
     boost::filesystem::path path = GetPath(uuid);
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Fri May 16 17:19:35 2025 +0200
@@ -80,15 +80,19 @@
                         size_t size,
                         FileContentType type) ORTHANC_OVERRIDE;
 
-    virtual IMemoryBuffer* Read(const std::string& uuid,
-                                FileContentType type) ORTHANC_OVERRIDE;
+    // This flavor is only used in the "DelayedDeletion" plugin
+    IMemoryBuffer* ReadWhole(const std::string& uuid,
+                             FileContentType type);
 
     virtual IMemoryBuffer* ReadRange(const std::string& uuid,
                                      FileContentType type,
                                      uint64_t start /* inclusive */,
                                      uint64_t end /* exclusive */) ORTHANC_OVERRIDE;
 
-    virtual bool HasReadRange() const ORTHANC_OVERRIDE;
+    virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
 
     virtual void Remove(const std::string& uuid,
                         FileContentType type) ORTHANC_OVERRIDE;
--- a/OrthancFramework/Sources/FileStorage/IStorageArea.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h	Fri May 16 17:19:35 2025 +0200
@@ -24,8 +24,9 @@
 
 #pragma once
 
+#include "../Compatibility.h"
+#include "../Enumerations.h"
 #include "../IMemoryBuffer.h"
-#include "../Enumerations.h"
 
 #include <stdint.h>
 #include <string>
@@ -33,6 +34,8 @@
 
 namespace Orthanc
 {
+  class DicomInstanceToStore;
+
   class IStorageArea : public boost::noncopyable
   {
   public:
@@ -45,17 +48,44 @@
                         size_t size,
                         FileContentType type) = 0;
 
-    virtual IMemoryBuffer* Read(const std::string& uuid,
-                                FileContentType type) = 0;
-
     virtual IMemoryBuffer* ReadRange(const std::string& uuid,
                                      FileContentType type,
                                      uint64_t start /* inclusive */,
                                      uint64_t end /* exclusive */) = 0;
 
-    virtual bool HasReadRange() const = 0;
+    virtual bool HasEfficientReadRange() const = 0;
 
     virtual void Remove(const std::string& uuid,
                         FileContentType type) = 0;
   };
+
+
+  // storage area with customData (customData are used only in plugins)
+  class IPluginStorageArea : public boost::noncopyable
+  {
+  public:
+    virtual ~IPluginStorageArea()
+    {
+    }
+
+    virtual void Create(std::string& customData /* out */,
+                        const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type,
+                        CompressionType compression,
+                        const DicomInstanceToStore* dicomInstance /* can be NULL if not a DICOM instance */) = 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 HasEfficientReadRange() const = 0;
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type,
+                        const std::string& customData) = 0;
+  };
 }
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp	Fri May 16 17:19:35 2025 +0200
@@ -69,31 +69,6 @@
   }
 
   
-  IMemoryBuffer* MemoryStorageArea::Read(const std::string& uuid,
-                                         FileContentType type) 
-  {
-    LOG(INFO) << "Reading attachment \"" << uuid << "\" of \""
-              << static_cast<int>(type) << "\" content type";
-
-    Mutex::ScopedLock lock(mutex_);
-
-    Content::const_iterator found = content_.find(uuid);
-
-    if (found == content_.end())
-    {
-      throw OrthancException(ErrorCode_InexistentFile);
-    }
-    else if (found->second == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else
-    {
-      return StringMemoryBuffer::CreateFromCopy(*found->second);
-    }
-  }
-      
-
   IMemoryBuffer* MemoryStorageArea::ReadRange(const std::string& uuid,
                                               FileContentType type,
                                               uint64_t start /* inclusive */,
@@ -143,12 +118,6 @@
   }
 
 
-  bool MemoryStorageArea::HasReadRange() const
-  {
-    return true;
-  }
-
-
   void MemoryStorageArea::Remove(const std::string& uuid,
                                  FileContentType type)
   {
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Fri May 16 17:19:35 2025 +0200
@@ -49,15 +49,15 @@
                         size_t size,
                         FileContentType type) ORTHANC_OVERRIDE;
 
-    virtual IMemoryBuffer* Read(const std::string& uuid,
-                                FileContentType type) ORTHANC_OVERRIDE;
-
     virtual IMemoryBuffer* ReadRange(const std::string& uuid,
                                      FileContentType type,
                                      uint64_t start /* inclusive */,
                                      uint64_t end /* exclusive */) ORTHANC_OVERRIDE;
     
-    virtual bool HasReadRange() const ORTHANC_OVERRIDE;
+    virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
 
     virtual void Remove(const std::string& uuid,
                         FileContentType type) ORTHANC_OVERRIDE;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.cpp	Fri May 16 17:19:35 2025 +0200
@@ -0,0 +1,53 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "PluginStorageAreaAdapter.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  PluginStorageAreaAdapter::PluginStorageAreaAdapter(IStorageArea* storage) :
+    storage_(storage)
+  {
+    if (storage == NULL)
+    {
+      throw OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+
+  void PluginStorageAreaAdapter::Create(std::string& customData,
+                                        const std::string& uuid,
+                                        const void* content,
+                                        size_t size,
+                                        FileContentType type,
+                                        CompressionType compression,
+                                        const DicomInstanceToStore* dicomInstance)
+  {
+    customData.clear();
+    storage_->Create(uuid, content, size, type);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h	Fri May 16 17:19:35 2025 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IStorageArea.h"
+
+
+namespace Orthanc
+{
+  class PluginStorageAreaAdapter : public IPluginStorageArea
+  {
+  private:
+    std::unique_ptr<IStorageArea> storage_;
+
+  public:
+    explicit PluginStorageAreaAdapter(IStorageArea* storage /* takes ownership */);
+
+    virtual void Create(std::string& customData,
+                        const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type,
+                        CompressionType compression,
+                        const DicomInstanceToStore* dicomInstance) ORTHANC_OVERRIDE;
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */,
+                                     const std::string& customData) ORTHANC_OVERRIDE
+    {
+      return storage_->ReadRange(uuid, type, start, end);
+    }
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type,
+                        const std::string& customData) ORTHANC_OVERRIDE
+    {
+      storage_->Remove(uuid, type);
+    }
+
+    virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
+    {
+      return storage_->HasEfficientReadRange();
+    }
+  };
+}
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Fri May 16 17:19:35 2025 +0200
@@ -275,7 +275,7 @@
   };
 
 
-  StorageAccessor::StorageAccessor(IStorageArea& area) :
+  StorageAccessor::StorageAccessor(IPluginStorageArea& area) :
     area_(area),
     cache_(NULL),
     metrics_(NULL)
@@ -283,7 +283,7 @@
   }
   
 
-  StorageAccessor::StorageAccessor(IStorageArea& area, 
+  StorageAccessor::StorageAccessor(IPluginStorageArea& area,
                                    StorageCache& cache) :
     area_(area),
     cache_(&cache),
@@ -292,7 +292,7 @@
   }
 
 
-  StorageAccessor::StorageAccessor(IStorageArea& area,
+  StorageAccessor::StorageAccessor(IPluginStorageArea& area,
                                    MetricsRegistry& metrics) :
     area_(area),
     cache_(NULL),
@@ -300,7 +300,7 @@
   {
   }
 
-  StorageAccessor::StorageAccessor(IStorageArea& area, 
+  StorageAccessor::StorageAccessor(IPluginStorageArea& area,
                                    StorageCache& cache,
                                    MetricsRegistry& metrics) :
     area_(area),
@@ -314,9 +314,10 @@
                                   size_t size,
                                   FileContentType type,
                                   CompressionType compression,
-                                  bool storeMd5)
+                                  bool storeMd5,
+                                  const DicomInstanceToStore* instance)
   {
-    std::string uuid = Toolbox::GenerateUuid();
+    const std::string uuid = Toolbox::GenerateUuid();
 
     std::string md5;
 
@@ -325,13 +326,15 @@
       Toolbox::ComputeMD5(md5, data, size);
     }
 
+    std::string customData;
+
     switch (compression)
     {
       case CompressionType_None:
       {
         {
           MetricsTimer timer(*this, METRICS_CREATE_DURATION);
-          area_.Create(uuid, data, size, type);
+          area_.Create(customData, uuid, data, size, type, compression, instance);
         }
 
         if (metrics_ != NULL)
@@ -345,7 +348,7 @@
           cacheAccessor.Add(uuid, type, data, size);
         }
 
-        return FileInfo(uuid, type, size, md5);
+        return FileInfo(uuid, type, size, md5, customData);
       }
 
       case CompressionType_ZlibWithSize:
@@ -367,11 +370,11 @@
 
           if (compressed.size() > 0)
           {
-            area_.Create(uuid, &compressed[0], compressed.size(), type);
+            area_.Create(customData, uuid, &compressed[0], compressed.size(), type, compression, instance);
           }
           else
           {
-            area_.Create(uuid, NULL, 0, type);
+            area_.Create(customData, uuid, NULL, 0, type, compression, instance);
           }
         }
 
@@ -387,7 +390,7 @@
         }
 
         return FileInfo(uuid, type, size, md5,
-                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
+                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData);
       }
 
       default:
@@ -395,16 +398,6 @@
     }
   }
 
-  FileInfo StorageAccessor::Write(const std::string &data,
-                                  FileContentType type,
-                                  CompressionType compression,
-                                  bool storeMd5)
-  {
-    return Write((data.size() == 0 ? NULL : data.c_str()),
-                 data.size(), type, compression, storeMd5);
-  }
-
-
   void StorageAccessor::Read(std::string& content,
                              const FileInfo& info)
   {
@@ -446,7 +439,7 @@
 
         {
           MetricsTimer timer(*this, METRICS_READ_DURATION);
-          buffer.reset(area_.Read(info.GetUuid(), info.GetContentType()));
+          buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
         }
 
         if (metrics_ != NULL)
@@ -467,7 +460,7 @@
         
         {
           MetricsTimer timer(*this, METRICS_READ_DURATION);
-          compressed.reset(area_.Read(info.GetUuid(), info.GetContentType()));
+          compressed.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
         }
         
         if (metrics_ != NULL)
@@ -526,7 +519,7 @@
 
     {
       MetricsTimer timer(*this, METRICS_READ_DURATION);
-      buffer.reset(area_.Read(info.GetUuid(), info.GetContentType()));
+      buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
     }
 
     if (metrics_ != NULL)
@@ -539,7 +532,8 @@
 
 
   void StorageAccessor::Remove(const std::string& fileUuid,
-                               FileContentType type)
+                               FileContentType type,
+                               const std::string& customData)
   {
     if (cache_ != NULL)
     {
@@ -548,14 +542,14 @@
 
     {
       MetricsTimer timer(*this, METRICS_REMOVE_DURATION);
-      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());
   }
 
 
@@ -616,7 +610,7 @@
 
     {
       MetricsTimer timer(*this, METRICS_READ_DURATION);
-      buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end));
+      buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end, info.GetCustomData()));
       assert(buffer->GetSize() == end);
     }
 
@@ -682,19 +676,19 @@
       if (range.HasStart() &&
           range.HasEnd())
       {
-        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1));
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1, info.GetCustomData()));
       }
       else if (range.HasStart())
       {
-        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize()));
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize(), info.GetCustomData()));
       }
       else if (range.HasEnd())
       {
-        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1));
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1, info.GetCustomData()));
       }
       else
       {
-        buffer.reset(area_.Read(info.GetUuid(), info.GetContentType()));
+        buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData()));
       }
 
       buffer->MoveToString(target);
@@ -785,4 +779,5 @@
     output.AnswerStream(transcoder);
   }
 #endif
+
 }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Fri May 16 17:19:35 2025 +0200
@@ -110,7 +110,7 @@
   private:
     class MetricsTimer;
 
-    IStorageArea&     area_;
+    IPluginStorageArea&     area_;
     StorageCache*     cache_;
     MetricsRegistry*  metrics_;
 
@@ -121,15 +121,15 @@
 #endif
 
   public:
-    explicit StorageAccessor(IStorageArea& area);
+    explicit StorageAccessor(IPluginStorageArea& area);
 
-    StorageAccessor(IStorageArea& area,
+    StorageAccessor(IPluginStorageArea& area,
                     StorageCache& cache);
 
-    StorageAccessor(IStorageArea& area,
+    StorageAccessor(IPluginStorageArea& area,
                     MetricsRegistry& metrics);
 
-    StorageAccessor(IStorageArea& area,
+    StorageAccessor(IPluginStorageArea& area,
                     StorageCache& cache,
                     MetricsRegistry& metrics);
 
@@ -137,12 +137,8 @@
                    size_t size,
                    FileContentType type,
                    CompressionType compression,
-                   bool storeMd5);
-
-    FileInfo Write(const std::string& data,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
+                   bool storeMd5,
+                   const DicomInstanceToStore* instance);
 
     void Read(std::string& content,
               const FileInfo& info);
@@ -155,7 +151,8 @@
                         uint64_t end /* exclusive */);
 
     void Remove(const std::string& fileUuid,
-                FileContentType type);
+                FileContentType type,
+                const std::string& customData);
 
     void Remove(const FileInfo& info);
 
@@ -185,6 +182,7 @@
                     const std::string& mime,
                     const std::string& contentFilename);
 #endif
+
   private:
     void ReadStartRangeInternal(std::string& target,
                                 const FileInfo& info,
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Fri May 16 17:19:35 2025 +0200
@@ -30,10 +30,9 @@
 #include <gtest/gtest.h>
 
 #include "../Sources/FileStorage/FilesystemStorage.h"
+#include "../Sources/FileStorage/PluginStorageAreaAdapter.h"
 #include "../Sources/FileStorage/StorageAccessor.h"
 #include "../Sources/FileStorage/StorageCache.h"
-#include "../Sources/HttpServer/BufferHttpSender.h"
-#include "../Sources/HttpServer/FilesystemHttpSender.h"
 #include "../Sources/Logging.h"
 #include "../Sources/OrthancException.h"
 #include "../Sources/Toolbox.h"
@@ -63,12 +62,18 @@
   s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown);
   std::string d;
   {
-    std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown));
+    std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown));
     buffer->MoveToString(d);    
   }
   ASSERT_EQ(d.size(), data.size());
   ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
   ASSERT_EQ(s.GetSize(uid), data.size());
+  {
+    std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size()));
+    std::string d2;
+    buffer2->MoveToString(d2);
+    ASSERT_EQ(d, d2);
+  }
 }
 
 TEST(FilesystemStorage, Basic2)
@@ -81,12 +86,18 @@
   s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown);
   std::string d;
   {
-    std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown));
+    std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown));
     buffer->MoveToString(d);    
   }
   ASSERT_EQ(d.size(), data.size());
   ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
   ASSERT_EQ(s.GetSize(uid), data.size());
+  {
+    std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size()));
+    std::string d2;
+    buffer2->MoveToString(d2);
+    ASSERT_EQ(d, d2);
+  }
 }
 
 TEST(FilesystemStorage, FileWithSameNameAsTopDirectory)
@@ -169,13 +180,13 @@
 
 TEST(StorageAccessor, NoCompression)
 {
-  FilesystemStorage s("UnitTestsStorage");
+  PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage"));
   StorageCache cache;
   StorageAccessor accessor(s, cache);
 
-  std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true);
-  
+  const std::string data = "Hello world";
+  FileInfo info = accessor.Write(data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, NULL);
+
   std::string r;
   accessor.Read(r, info);
 
@@ -191,13 +202,13 @@
 
 TEST(StorageAccessor, Compression)
 {
-  FilesystemStorage s("UnitTestsStorage");
+  PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage"));
   StorageCache cache;
   StorageAccessor accessor(s, cache);
 
-  std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true);
-  
+  const std::string data = "Hello world";
+  FileInfo info = accessor.Write(data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, NULL);
+
   std::string r;
   accessor.Read(r, info);
 
@@ -212,17 +223,17 @@
 
 TEST(StorageAccessor, Mix)
 {
-  FilesystemStorage s("UnitTestsStorage");
+  PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage"));
   StorageCache cache;
   StorageAccessor accessor(s, cache);
 
-  std::string r;
-  std::string compressedData = "Hello";
-  std::string uncompressedData = "HelloWorld";
+  const std::string compressedData = "Hello";
+  const 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.Write(compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, NULL);
+  FileInfo uncompressedInfo = accessor.Write(uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, NULL);
+
+  std::string r;
   accessor.Read(r, compressedInfo);
   ASSERT_EQ(compressedData, r);
 
--- a/OrthancServer/CMakeLists.txt	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/CMakeLists.txt	Fri May 16 17:19:35 2025 +0200
@@ -243,15 +243,17 @@
 #####################################################################
 
 set(ORTHANC_EMBEDDED_FILES
-  CONFIGURATION_SAMPLE            ${CMAKE_SOURCE_DIR}/Resources/Configuration.json
-  DICOM_CONFORMANCE_STATEMENT     ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
-  FONT_UBUNTU_MONO_BOLD_16        ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
-  LUA_TOOLBOX                     ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua
-  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
-  INSTALL_TRACK_ATTACHMENTS_SIZE  ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql
-  INSTALL_LABELS_TABLE            ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql
+  CONFIGURATION_SAMPLE              ${CMAKE_SOURCE_DIR}/Resources/Configuration.json
+  DICOM_CONFORMANCE_STATEMENT       ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
+  FONT_UBUNTU_MONO_BOLD_16          ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json
+  LUA_TOOLBOX                       ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua
+  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
+  INSTALL_TRACK_ATTACHMENTS_SIZE    ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql
+  INSTALL_LABELS_TABLE              ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql
+  INSTALL_REVISION_AND_CUSTOM_DATA  ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql  
+  INSTALL_KEY_VALUE_STORE_AND_QUEUES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallKeyValueStoresAndQueues.sql
   )
 
 if (STANDALONE_BUILD)
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri May 16 17:19:35 2025 +0200
@@ -83,13 +83,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);
     }
 
 
@@ -1448,6 +1450,66 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+    virtual void StoreKeyValue(const std::string& storeId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void DeleteKeyValue(const std::string& storeId,
+                                const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual bool GetKeyValue(std::string& value,
+                             const std::string& storeId,
+                             const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void ListKeys(std::list<std::string>& keys,
+                          const std::string& storeId,
+                          uint64_t since,
+                          uint64_t limit) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void EnqueueValue(const std::string& queueId,
+                              const std::string& value) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual bool DequeueValue(std::string& value,
+                              const std::string& queueId,
+                              QueueOrigin origin) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void GetQueueSize(uint64_t& size,
+                              const std::string& queueId) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual bool GetAttachment(FileInfo& attachment,
+                               int64_t& revision,
+                               const std::string& attachmentUuid) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
+
+    virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid,
+                                            const std::string& customData) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
   };
 
 
@@ -1620,7 +1682,7 @@
 
 
   void OrthancPluginDatabase::Upgrade(unsigned int targetVersion,
-                                      IStorageArea& storageArea)
+                                      IPluginStorageArea& storageArea)
   {
     VoidDatabaseListener listener;
     
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Fri May 16 17:19:35 2025 +0200
@@ -103,7 +103,7 @@
     virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE;
 
     virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea) ORTHANC_OVERRIDE;    
+                         IPluginStorageArea& storageArea) ORTHANC_OVERRIDE;
 
     virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE
     {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri May 16 17:19:35 2025 +0200
@@ -62,13 +62,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);
     }
 
 
@@ -677,7 +679,6 @@
       }
     }
 
-    
     virtual bool LookupGlobalProperty(std::string& target,
                                       GlobalProperty property,
                                       bool shared) ORTHANC_OVERRIDE
@@ -1061,6 +1062,68 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+    virtual void StoreKeyValue(const std::string& storeId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void DeleteKeyValue(const std::string& storeId,
+                                const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual bool GetKeyValue(std::string& value,
+                             const std::string& storeId,
+                             const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void ListKeys(std::list<std::string>& keys,
+                          const std::string& storeId,
+                          uint64_t since,
+                          uint64_t limit) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void EnqueueValue(const std::string& queueId,
+                              const std::string& value) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual bool DequeueValue(std::string& value,
+                              const std::string& queueId,
+                              QueueOrigin origin) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void GetQueueSize(uint64_t& size,
+                              const std::string& queueId) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual bool GetAttachment(FileInfo& attachment,
+                               int64_t& revision,
+                               const std::string& attachmentUuid) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
+
+    virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid,
+                                            const std::string& customData) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
+
+
   };
 
   
@@ -1231,7 +1294,7 @@
 
   
   void OrthancPluginDatabaseV3::Upgrade(unsigned int targetVersion,
-                                        IStorageArea& storageArea)
+                                        IPluginStorageArea& storageArea)
   {
     VoidDatabaseListener listener;
     
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Fri May 16 17:19:35 2025 +0200
@@ -76,7 +76,7 @@
     virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE;
 
     virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea) ORTHANC_OVERRIDE;    
+                         IPluginStorageArea& storageArea) ORTHANC_OVERRIDE;
 
     virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE
     {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri May 16 17:19:35 2025 +0200
@@ -108,7 +108,8 @@
                     source.uncompressed_hash(),
                     static_cast<CompressionType>(source.compression_type()),
                     source.compressed_size(),
-                    source.compressed_hash());
+                    source.compressed_hash(),
+                    source.custom_data());
   }
 
 
@@ -576,6 +577,7 @@
       request.mutable_add_attachment()->mutable_attachment()->set_compression_type(attachment.GetCompressionType());
       request.mutable_add_attachment()->mutable_attachment()->set_compressed_size(attachment.GetCompressedSize());
       request.mutable_add_attachment()->mutable_attachment()->set_compressed_hash(attachment.GetCompressedMD5());        
+      request.mutable_add_attachment()->mutable_attachment()->set_custom_data(attachment.GetCustomData());        // new in 1.12.99
       request.mutable_add_attachment()->set_revision(revision);
 
       ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request);
@@ -1016,7 +1018,21 @@
       }
     }
 
-    
+
+    virtual bool GetAttachment(FileInfo& attachment,
+                               int64_t& revision,
+                               const std::string& attachmentUuid) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+    virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid,
+                                            const std::string& customData) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+
     virtual bool LookupGlobalProperty(std::string& target,
                                       GlobalProperty property,
                                       bool shared) ORTHANC_OVERRIDE
@@ -1805,6 +1821,54 @@
         find.ExecuteExpand(response, capabilities, request, identifier);
       }
     }
+
+    virtual void StoreKeyValue(const std::string& storeId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+    virtual void DeleteKeyValue(const std::string& storeId,
+                                const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+    virtual bool GetKeyValue(std::string& value,
+                             const std::string& storeId,
+                             const std::string& key) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+    virtual void ListKeys(std::list<std::string>& keys,
+                          const std::string& storeId,
+                          uint64_t since,
+                          uint64_t limit) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+    virtual void EnqueueValue(const std::string& queueId,
+                              const std::string& value) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+    virtual bool DequeueValue(std::string& value,
+                              const std::string& queueId,
+                              QueueOrigin origin) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
+    virtual void GetQueueSize(uint64_t& size,
+                              const std::string& queueId) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // TODO_ATTACH_CUSTOM_DATA
+    }
+
   };
 
 
@@ -1895,6 +1959,8 @@
       dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency());
       dbCapabilities_.SetHasExtendedChanges(systemInfo.has_extended_changes());
       dbCapabilities_.SetHasFindSupport(systemInfo.supports_find());
+      dbCapabilities_.SetKeyValueStoresSupport(systemInfo.supports_key_value_stores());
+      dbCapabilities_.SetQueuesSupport(systemInfo.supports_queues());
     }
 
     open_ = true;
@@ -1961,7 +2027,7 @@
 
   
   void OrthancPluginDatabaseV4::Upgrade(unsigned int targetVersion,
-                                        IStorageArea& storageArea)
+                                        IPluginStorageArea& storageArea)
   {
     if (!open_)
     {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Fri May 16 17:19:35 2025 +0200
@@ -88,7 +88,7 @@
     virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE;
 
     virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea) ORTHANC_OVERRIDE;    
+                         IPluginStorageArea& storageArea) ORTHANC_OVERRIDE;
 
     virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri May 16 17:19:35 2025 +0200
@@ -39,7 +39,7 @@
 #include "../../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h"
 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h"
-#include "../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h"
+#include "../../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h"
 #include "../../../OrthancFramework/Sources/HttpServer/HttpServer.h"
 #include "../../../OrthancFramework/Sources/HttpServer/HttpToolbox.h"
 #include "../../../OrthancFramework/Sources/Images/Image.h"
@@ -79,6 +79,125 @@
 
 namespace Orthanc
 {
+  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_;
+
+    void Setup(const void* buffer,
+               size_t size)
+    {
+      buffer_.assign(reinterpret_cast<const char*>(buffer), size);
+
+      instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_));
+      instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
+    }
+
+  public:
+    DicomInstanceFromBuffer(const void* buffer,
+                            size_t size)
+    {
+      Setup(buffer, size);
+    }
+
+    explicit DicomInstanceFromBuffer(const std::string& buffer)
+    {
+      Setup(buffer.empty() ? NULL : buffer.c_str(), buffer.size());
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return *instance_;
+    };
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance
+  {
+  private:
+    std::unique_ptr<ParsedDicomFile>       parsed_;
+    std::unique_ptr<DicomInstanceToStore>  instance_;
+
+    void Setup(ParsedDicomFile* parsed)
+    {
+      parsed_.reset(parsed);
+      
+      if (parsed_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+      else
+      {
+        instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_));
+        instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
+      }
+    }
+
+  public:
+    explicit DicomInstanceFromParsed(IDicomTranscoder::DicomImage& transcoded)
+    {
+      Setup(transcoded.ReleaseAsParsedDicomFile());
+    }
+
+    explicit DicomInstanceFromParsed(ParsedDicomFile* parsed /* takes ownership */)
+    {
+      Setup(parsed);
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return *instance_;
+    };
+  };
+
+
   class OrthancPlugins::WebDavCollection : public IWebDavBucket
   {
   private:
@@ -504,6 +623,22 @@
   }
 
 
+  static void CopyStringList(OrthancPluginMemoryBuffer& target,
+                             const std::list<std::string>& list)
+  {
+    Json::Value json = Json::arrayValue;
+
+    for (std::list<std::string>::const_iterator 
+           it = list.begin(); it != list.end(); ++it)
+    {
+      json.append(*it);
+    }
+        
+    std::string s;
+    Toolbox::WriteFastJson(s, json);
+    CopyToMemoryBuffer(target, s);
+  }
+
   namespace
   {
     class MemoryBufferRaii : public boost::noncopyable
@@ -549,9 +684,48 @@
         }
       }
     };
-  
-
-    class StorageAreaBase : public IStorageArea
+
+
+    static IMemoryBuffer* GetRangeFromWhole(std::unique_ptr<MallocMemoryBuffer>& whole,
+                                            uint64_t start /* inclusive */,
+                                            uint64_t end /* exclusive */)
+    {
+      if (start > end)
+      {
+        throw OrthancException(ErrorCode_BadRange);
+      }
+      else if (start == end)
+      {
+        return new StringMemoryBuffer;  // Empty
+      }
+      else
+      {
+        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);
+        }
+      }
+    }
+
+
+    // "legacy" storage plugins don't store customData -> derive from IStorageArea
+    class StorageAreaWithoutCustomData : public IStorageArea
     {
     private:
       OrthancPluginStorageCreate create_;
@@ -564,50 +738,10 @@
         return errorDictionary_;
       }
 
-      IMemoryBuffer* RangeFromWhole(const std::string& uuid,
-                                    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));
-
-          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:
-      StorageAreaBase(OrthancPluginStorageCreate create,
-                      OrthancPluginStorageRemove remove,
-                      PluginsErrorDictionary&  errorDictionary) : 
+      StorageAreaWithoutCustomData(OrthancPluginStorageCreate create,
+                                   OrthancPluginStorageRemove remove,
+                                   PluginsErrorDictionary&  errorDictionary) :
         create_(create),
         remove_(remove),
         errorDictionary_(errorDictionary)
@@ -649,24 +783,16 @@
     };
 
 
-    class PluginStorageArea : public StorageAreaBase
+    class PluginStorageAreaV1 : public StorageAreaWithoutCustomData
     {
     private:
       OrthancPluginStorageRead   read_;
       OrthancPluginFree          free_;
       
-      void Free(void* buffer) const
-      {
-        if (buffer != NULL)
-        {
-          free_(buffer);
-        }
-      }
-
     public:
-      PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks,
-                        PluginsErrorDictionary&  errorDictionary) :
-        StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
+      PluginStorageAreaV1(const _OrthancPluginRegisterStorageArea& callbacks,
+                          PluginsErrorDictionary&  errorDictionary) :
+        StorageAreaWithoutCustomData(callbacks.create, callbacks.remove, errorDictionary),
         read_(callbacks.read),
         free_(callbacks.free)
       {
@@ -676,38 +802,30 @@
         }
       }
 
-      virtual IMemoryBuffer* Read(const std::string& uuid,
-                                  FileContentType type) ORTHANC_OVERRIDE
-      {
-        std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer);
-
-        void* buffer = NULL;
-        int64_t size = 0;
-
-        OrthancPluginErrorCode error = read_
-          (&buffer, &size, uuid.c_str(), Plugins::Convert(type));
-
-        if (error == OrthancPluginErrorCode_Success)
-        {
-          result->Assign(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 */) ORTHANC_OVERRIDE
       {
-        return RangeFromWhole(uuid, type, start, end);
-      }
-
-      virtual bool HasReadRange() const ORTHANC_OVERRIDE
+        void* buffer = NULL;
+        int64_t size = 0;
+
+        OrthancPluginErrorCode error = read_(&buffer, &size, uuid.c_str(), Plugins::Convert(type));
+
+        if (error == OrthancPluginErrorCode_Success)
+        {
+          std::unique_ptr<MallocMemoryBuffer> whole(new MallocMemoryBuffer);
+          whole->Assign(buffer, size, free_);
+          return GetRangeFromWhole(whole, start, end);
+        }
+        else
+        {
+          GetErrorDictionary().LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+      }
+
+      virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
       {
         return false;
       }
@@ -715,16 +833,16 @@
 
 
     // New in Orthanc 1.9.0
-    class PluginStorageArea2 : public StorageAreaBase
+    class PluginStorageAreaV2 : public StorageAreaWithoutCustomData
     {
     private:
       OrthancPluginStorageReadWhole  readWhole_;
       OrthancPluginStorageReadRange  readRange_;
 
     public:
-      PluginStorageArea2(const _OrthancPluginRegisterStorageArea2& callbacks,
-                         PluginsErrorDictionary&  errorDictionary) :
-        StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
+      PluginStorageAreaV2(const _OrthancPluginRegisterStorageArea2& callbacks,
+                          PluginsErrorDictionary&  errorDictionary) :
+        StorageAreaWithoutCustomData(callbacks.create, callbacks.remove, errorDictionary),
         readWhole_(callbacks.readWhole),
         readRange_(callbacks.readRange)
       {
@@ -734,29 +852,6 @@
         }
       }
 
-      virtual IMemoryBuffer* Read(const std::string& uuid,
-                                  FileContentType type) ORTHANC_OVERRIDE
-      {
-        std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer);
-
-        OrthancPluginMemoryBuffer64 buffer;
-        buffer.size = 0;
-        buffer.data = NULL;
-        
-        OrthancPluginErrorCode error = readWhole_(&buffer, uuid.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 */,
@@ -764,7 +859,23 @@
       {
         if (readRange_ == NULL)
         {
-          return RangeFromWhole(uuid, type, start, end);
+          OrthancPluginMemoryBuffer64 buffer;
+          buffer.size = 0;
+          buffer.data = NULL;
+
+          OrthancPluginErrorCode error = readWhole_(&buffer, uuid.c_str(), Plugins::Convert(type));
+
+          if (error == OrthancPluginErrorCode_Success)
+          {
+            std::unique_ptr<MallocMemoryBuffer> whole(new MallocMemoryBuffer);
+            whole->Assign(buffer.data, buffer.size, ::free);
+            return GetRangeFromWhole(whole, start, end);
+          }
+          else
+          {
+            GetErrorDictionary().LogError(error, true);
+            throw OrthancException(static_cast<ErrorCode>(error));
+          }
         }
         else
         {
@@ -802,26 +913,153 @@
         }
       }
       
-      virtual bool HasReadRange() const ORTHANC_OVERRIDE
+      virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
       {
         return (readRange_ != NULL);
       }
     };
 
 
+    // New in Orthanc 1.12.99
+    class PluginStorageAreaV3 : public IPluginStorageArea
+    {
+    private:
+      OrthancPluginStorageCreate2     create_;
+      OrthancPluginStorageReadRange2  readRange_;
+      OrthancPluginStorageRemove2     remove_;
+
+      PluginsErrorDictionary&    errorDictionary_;
+
+    protected:
+      PluginsErrorDictionary& GetErrorDictionary() const
+      {
+        return errorDictionary_;
+      }
+
+    public:
+      PluginStorageAreaV3(const _OrthancPluginRegisterStorageArea3& callbacks,
+                          PluginsErrorDictionary&  errorDictionary) :
+        create_(callbacks.create),
+        readRange_(callbacks.readRange),
+        remove_(callbacks.remove),
+        errorDictionary_(errorDictionary)
+      {
+        if (create_ == NULL ||
+            readRange_ == NULL ||
+            remove_ == NULL)
+        {
+          throw OrthancException(ErrorCode_Plugin, "Storage area plugin does not implement all the required primitives (create, remove, and readRange)");
+        }
+      }
+
+      virtual void Create(std::string& customData /* out */,
+                          const std::string& uuid,
+                          const void* content,
+                          size_t size,
+                          FileContentType type,
+                          CompressionType compression,
+                          const DicomInstanceToStore* dicomInstance /* can be NULL if not a DICOM instance */) ORTHANC_OVERRIDE
+      {
+        MemoryBufferRaii customDataBuffer;
+        OrthancPluginErrorCode error;
+
+        if (dicomInstance != NULL)
+        {
+          Orthanc::OrthancPlugins::DicomInstanceFromCallback wrapped(*dicomInstance);
+          error = create_(customDataBuffer.GetObject(), uuid.c_str(), content, size, Plugins::Convert(type), Plugins::Convert(compression),
+                          reinterpret_cast<OrthancPluginDicomInstance*>(&wrapped));
+        }
+        else
+        {
+          error = create_(customDataBuffer.GetObject(), uuid.c_str(), content, size, Plugins::Convert(type), Plugins::Convert(compression), NULL);
+        }
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+        else
+        {
+          customDataBuffer.ToString(customData);
+        }
+      }
+
+      virtual void Remove(const std::string& uuid,
+                          FileContentType type,
+                          const std::string& customData) ORTHANC_OVERRIDE
+      {
+        OrthancPluginErrorCode error = remove_(uuid.c_str(), Plugins::Convert(type),
+                                                customData.empty() ? NULL : customData.c_str(), customData.size());
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.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 (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 =
+            readRange_(&buffer, uuid.c_str(), Plugins::Convert(type), start, customData.empty() ? NULL : customData.c_str(), customData.size());
+
+          if (error == OrthancPluginErrorCode_Success)
+          {
+            return StringMemoryBuffer::CreateFromSwap(range);
+          }
+          else
+          {
+            GetErrorDictionary().LogError(error, true);
+            throw OrthancException(static_cast<ErrorCode>(error));
+          }
+        }
+      }
+
+      virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
+      {
+        return true;
+      }
+    };
+
+
     class StorageAreaFactory : public boost::noncopyable
     {
     private:
       enum Version
       {
         Version1,
-        Version2
+        Version2,
+        Version3
       };
       
       SharedLibrary&                      sharedLibrary_;
       Version                             version_;
-      _OrthancPluginRegisterStorageArea   callbacks_;
+      _OrthancPluginRegisterStorageArea   callbacks1_;
       _OrthancPluginRegisterStorageArea2  callbacks2_;
+      _OrthancPluginRegisterStorageArea3  callbacks3_;
       PluginsErrorDictionary&             errorDictionary_;
 
       static void WarnNoReadRange()
@@ -835,7 +1073,7 @@
                          PluginsErrorDictionary&  errorDictionary) :
         sharedLibrary_(sharedLibrary),
         version_(Version1),
-        callbacks_(callbacks),
+        callbacks1_(callbacks),
         errorDictionary_(errorDictionary)
       {
         WarnNoReadRange();
@@ -855,20 +1093,37 @@
         }
       }
 
+      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_;
       }
 
-      IStorageArea* Create() const
+      IPluginStorageArea* Create() const
       {
         switch (version_)
         {
           case Version1:
-            return new PluginStorageArea(callbacks_, errorDictionary_);
+            return new PluginStorageAreaAdapter(new PluginStorageAreaV1(callbacks1_, errorDictionary_));
 
           case Version2:
-            return new PluginStorageArea2(callbacks2_, errorDictionary_);
+            return new PluginStorageAreaAdapter(new PluginStorageAreaV2(callbacks2_, errorDictionary_));
+
+          case Version3:
+            return new PluginStorageAreaV3(callbacks3_, errorDictionary_);
 
           default:
             throw OrthancException(ErrorCode_InternalError);
@@ -2527,125 +2782,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_;
-
-    void Setup(const void* buffer,
-               size_t size)
-    {
-      buffer_.assign(reinterpret_cast<const char*>(buffer), size);
-
-      instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_));
-      instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
-    }
-
-  public:
-    DicomInstanceFromBuffer(const void* buffer,
-                            size_t size)
-    {
-      Setup(buffer, size);
-    }
-
-    explicit DicomInstanceFromBuffer(const std::string& buffer)
-    {
-      Setup(buffer.empty() ? NULL : buffer.c_str(), buffer.size());
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return true;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return *instance_;
-    };
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance
-  {
-  private:
-    std::unique_ptr<ParsedDicomFile>       parsed_;
-    std::unique_ptr<DicomInstanceToStore>  instance_;
-
-    void Setup(ParsedDicomFile* parsed)
-    {
-      parsed_.reset(parsed);
-      
-      if (parsed_.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_NullPointer);
-      }
-      else
-      {
-        instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_));
-        instance_->SetOrigin(DicomInstanceOrigin::FromPlugins());
-      }
-    }
-
-  public:
-    explicit DicomInstanceFromParsed(IDicomTranscoder::DicomImage& transcoded)
-    {
-      Setup(transcoded.ReleaseAsParsedDicomFile());
-    }
-
-    explicit DicomInstanceFromParsed(ParsedDicomFile* parsed /* takes ownership */)
-    {
-      Setup(parsed);
-    }
-
-    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)
@@ -3611,6 +3747,12 @@
           break;
         }
 
+        case OrthancPluginCompressionType_None:
+        {
+          CopyToMemoryBuffer(*p.target, p.source, p.size);
+          return;
+        }
+
         default:
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
@@ -4499,6 +4641,164 @@
     reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendMultipartItem(p.answer, p.answerSize, headers);
   }
 
+  // static FileInfo CreateFileInfoFromPluginAttachment(OrthancPluginAttachment* attachment)
+  // {
+  //   return FileInfo(attachment->uuid,
+  //                   Orthanc::Plugins::Convert(attachment->contentType),
+  //                   attachment->uncompressedSize,
+  //                   attachment->uncompressedHash
+  //                                        )    fileInfo()
+  // }
+
+  static FileInfo Convert(const OrthancPluginAttachment2& attachment)
+  {
+    std::string uuid, customData;
+    if (attachment.uuid != NULL)
+    {
+      uuid = attachment.uuid;
+    }
+    else
+    {
+      uuid = Toolbox::GenerateUuid();
+    }
+
+    if (attachment.customData != NULL)
+    {
+      customData = std::string(reinterpret_cast<const char*>(attachment.customData), attachment.customDataSize);
+    }
+
+    return FileInfo(uuid,
+                    Orthanc::Plugins::Convert(static_cast<OrthancPluginContentType>(attachment.contentType)),
+                    attachment.uncompressedSize,
+                    attachment.uncompressedHash,
+                    Orthanc::Plugins::Convert(static_cast<OrthancPluginCompressionType>(attachment.compressionType)),
+                    attachment.compressedSize,
+                    attachment.compressedHash,
+                    customData);
+  }
+
+  void OrthancPlugins::ApplyAdoptAttachment(const _OrthancPluginAdoptAttachment& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+    FileInfo adoptedFile = Convert(*(parameters.attachmentInfo));
+
+    if (adoptedFile.GetContentType() == FileContentType_Dicom)
+    {
+      std::unique_ptr<DicomInstanceToStore> dicom(DicomInstanceToStore::CreateFromBuffer(parameters.buffer, parameters.bufferSize));
+      dicom->SetOrigin(DicomInstanceOrigin::FromPlugins());
+
+      std::string resultPublicId;
+
+      ServerContext::StoreResult result = lock.GetContext().AdoptAttachment(resultPublicId, *dicom, StoreInstanceMode_Default, adoptedFile);
+
+      CopyToMemoryBuffer(*parameters.attachmentUuid, adoptedFile.GetUuid().size() > 0 ? adoptedFile.GetUuid().c_str() : NULL, adoptedFile.GetUuid().size());
+      CopyToMemoryBuffer(*parameters.createdResourceId, resultPublicId.size() > 0 ? resultPublicId.c_str() : NULL, resultPublicId.size());
+      *(parameters.storeStatus) = Plugins::Convert(result.GetStatus());
+    }
+  }
+
+  void OrthancPlugins::ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+    FileInfo fileInfo;
+    int64_t revision;
+    
+    if (lock.GetContext().GetIndex().GetAttachment(fileInfo, revision, parameters.attachmentUuid))
+    {
+      CopyToMemoryBuffer(*parameters.customData, fileInfo.GetCustomData().size() > 0 ? fileInfo.GetCustomData().c_str() : NULL, fileInfo.GetCustomData().size());
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+  void OrthancPlugins::ApplyUpdateAttachmentCustomData(const _OrthancPluginUpdateAttachmentCustomData& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+    FileInfo fileInfo;
+    std::string customData(parameters.customData, parameters.customDataSize);
+    
+    lock.GetContext().GetIndex().UpdateAttachmentCustomData(parameters.attachmentUuid, customData);
+  }
+
+  bool OrthancPlugins::HasKeyValueStoresSupport()
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    return lock.GetContext().GetIndex().HasKeyValueStoresSupport();
+  }
+
+  void OrthancPlugins::ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+    std::string value(parameters.value, parameters.valueSize);
+
+    lock.GetContext().GetIndex().StoreKeyValue(parameters.storeId, parameters.key, value);
+  }
+
+  void OrthancPlugins::ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    lock.GetContext().GetIndex().DeleteKeyValue(parameters.storeId, parameters.key);
+  }
+
+  void OrthancPlugins::ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    std::string value;
+
+    if (lock.GetContext().GetIndex().GetKeyValue(value, parameters.storeId, parameters.key))
+    {
+      CopyToMemoryBuffer(*parameters.value, value.size() > 0 ? value.c_str() : NULL, value.size());
+    }
+  }
+
+  void OrthancPlugins::ApplyListKeys(const _OrthancPluginListKeys& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    std::list<std::string> keys;
+
+    lock.GetContext().GetIndex().ListKeys(keys, parameters.storeId, parameters.since, parameters.limit);
+    CopyStringList(*(parameters.keys), keys);
+  }
+
+  bool OrthancPlugins::HasQueuesSupport()
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    return lock.GetContext().GetIndex().HasQueuesSupport();
+  }
+
+  void OrthancPlugins::ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+    std::string value(parameters.value, parameters.valueSize);
+
+    lock.GetContext().GetIndex().EnqueueValue(parameters.queueId, value);
+  }
+
+  void OrthancPlugins::ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    std::string value;
+
+    if (lock.GetContext().GetIndex().DequeueValue(value, parameters.queueId, Plugins::Convert(parameters.origin)))
+    {
+      CopyToMemoryBuffer(*parameters.value, value.size() > 0 ? value.c_str() : NULL, value.size());
+    }
+  }
+
+  void OrthancPlugins::ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    lock.GetContext().GetIndex().GetQueueSize(*parameters.size, parameters.queueId);
+  }
 
   void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params)
   {
@@ -5066,32 +5366,13 @@
         return true;
 
       case _OrthancPluginService_StorageAreaCreate:
-      {
-        const _OrthancPluginStorageAreaCreate& p =
-          *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters);
-        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        storage.Create(p.uuid, p.content, static_cast<size_t>(p.size), Plugins::Convert(p.type));
-        return true;
-      }
+        throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaCreate() is only available in Orthanc <= 1.12.6");
 
       case _OrthancPluginService_StorageAreaRead:
-      {
-        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)));
-        CopyToMemoryBuffer(*p.target, content->GetData(), content->GetSize());
-        return true;
-      }
+        throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaRead() is only available in Orthanc <= 1.12.6");
 
       case _OrthancPluginService_StorageAreaRemove:
-      {
-        const _OrthancPluginStorageAreaRemove& p =
-          *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters);
-        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        storage.Remove(p.uuid, Plugins::Convert(p.type));
-        return true;
-      }
+        throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaRemove() is only available in Orthanc <= 1.12.6");
 
       case _OrthancPluginService_DicomBufferToJson:
       case _OrthancPluginService_DicomInstanceToJson:
@@ -5587,6 +5868,128 @@
         return true;
       }
 
+      case _OrthancPluginService_AdoptAttachment:
+      {
+        const _OrthancPluginAdoptAttachment& p =
+          *reinterpret_cast<const _OrthancPluginAdoptAttachment*>(parameters);
+        ApplyAdoptAttachment(p);
+        return true;
+      }
+
+      case _OrthancPluginService_GetAttachmentCustomData:
+      {
+        const _OrthancPluginGetAttachmentCustomData& p =
+          *reinterpret_cast<const _OrthancPluginGetAttachmentCustomData*>(parameters);
+        ApplyGetAttachmentCustomData(p);
+        return true;
+      }
+
+      case _OrthancPluginService_UpdateAttachmentCustomData:
+      {
+        const _OrthancPluginUpdateAttachmentCustomData& p =
+          *reinterpret_cast<const _OrthancPluginUpdateAttachmentCustomData*>(parameters);
+        ApplyUpdateAttachmentCustomData(p);
+        return true;
+      }
+
+      case _OrthancPluginService_StoreKeyValue:
+      {
+        if (!HasKeyValueStoresSupport())
+        {
+          LOG(ERROR) << "The DB engine does not support Key Value Store";
+          return false;
+        }
+
+        const _OrthancPluginStoreKeyValue& p =
+          *reinterpret_cast<const _OrthancPluginStoreKeyValue*>(parameters);
+        ApplyStoreKeyValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_DeleteKeyValue:
+      {
+        if (!HasKeyValueStoresSupport())
+        {
+          LOG(ERROR) << "The DB engine does not support Key Value Store";
+          return false;
+        }
+
+        const _OrthancPluginDeleteKeyValue& p =
+          *reinterpret_cast<const _OrthancPluginDeleteKeyValue*>(parameters);
+        ApplyDeleteKeyValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_GetKeyValue:
+      {
+        if (!HasKeyValueStoresSupport())
+        {
+          LOG(ERROR) << "The DB engine does not support Key Value Store";
+          return false;
+        }
+
+        const _OrthancPluginGetKeyValue& p =
+          *reinterpret_cast<const _OrthancPluginGetKeyValue*>(parameters);
+        ApplyGetKeyValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_ListKeys:
+      {
+        if (!HasKeyValueStoresSupport())
+        {
+          LOG(ERROR) << "The DB engine does not support Key Value Store";
+          return false;
+        }
+
+        const _OrthancPluginListKeys& p =
+          *reinterpret_cast<const _OrthancPluginListKeys*>(parameters);
+        ApplyListKeys(p);
+        return true;
+      }
+
+      case _OrthancPluginService_EnqueueValue:
+      {
+        if (!HasQueuesSupport())
+        {
+          LOG(ERROR) << "The DB engine does not support Queues";
+          return false;
+        }
+
+        const _OrthancPluginEnqueueValue& p =
+          *reinterpret_cast<const _OrthancPluginEnqueueValue*>(parameters);
+        ApplyEnqueueValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_DequeueValue:
+      {
+        if (!HasQueuesSupport())
+        {
+          LOG(ERROR) << "The DB engine does not support Queues";
+          return false;
+        }
+
+        const _OrthancPluginDequeueValue& p =
+          *reinterpret_cast<const _OrthancPluginDequeueValue*>(parameters);
+        ApplyDequeueValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_GetQueueSize:
+      {
+        if (!HasQueuesSupport())
+        {
+          LOG(ERROR) << "The DB engine does not support Queues";
+          return false;
+        }
+
+        const _OrthancPluginGetQueueSize& p =
+          *reinterpret_cast<const _OrthancPluginGetQueueSize*>(parameters);
+        ApplyGetQueueSize(p);
+        return true;
+      }
+
       default:
         return false;
     }
@@ -5670,23 +6073,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);
@@ -5809,7 +6223,7 @@
 
       case _OrthancPluginService_RegisterDatabaseBackendV4:
       {
-        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end";
+        CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end (v4)";
 
         const _OrthancPluginRegisterDatabaseBackendV4& p =
           *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV4*>(parameters);
@@ -5874,7 +6288,7 @@
         VoidDatabaseListener listener;
         
         {
-          IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
+          IPluginStorageArea& storage = *reinterpret_cast<IPluginStorageArea*>(p.storageArea);
 
           std::unique_ptr<IDatabaseWrapper::ITransaction> transaction(
             pimpl_->database_->StartTransaction(TransactionType_ReadWrite, listener));
@@ -5973,7 +6387,7 @@
   }
 
 
-  IStorageArea* OrthancPlugins::CreateStorageArea()
+  IPluginStorageArea* OrthancPlugins::CreateStorageArea()
   {
     if (!HasStorageArea())
     {
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Fri May 16 17:19:35 2025 +0200
@@ -88,11 +88,14 @@
     class HttpClientChunkedAnswer;
     class HttpServerChunkedReader;
     class IDicomInstance;
-    class DicomInstanceFromCallback;
     class DicomInstanceFromBuffer;
     class DicomInstanceFromParsed;
     class WebDavCollection;
-    
+
+public:
+    class DicomInstanceFromCallback;
+
+private:
     void RegisterRestCallback(const void* parameters,
                               bool lock);
 
@@ -220,6 +223,30 @@
 
     void ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& parameters);
 
+    void ApplyAdoptAttachment(const _OrthancPluginAdoptAttachment& parameters);
+
+    void ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters);
+
+    void ApplyUpdateAttachmentCustomData(const _OrthancPluginUpdateAttachmentCustomData& parameters);
+
+    bool HasKeyValueStoresSupport();
+
+    void ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters);
+
+    void ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters);
+
+    void ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters);
+
+    void ApplyListKeys(const _OrthancPluginListKeys& parameters);
+
+    bool HasQueuesSupport();
+
+    void ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters);
+
+    void ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters);
+
+    void ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters);
+
     void ComputeHash(_OrthancPluginService service,
                      const void* parameters);
 
@@ -291,7 +318,7 @@
 
     bool HasStorageArea() const;
 
-    IStorageArea* CreateStorageArea();  // To be freed after use
+    IPluginStorageArea* CreateStorageArea();  // To be freed after use
 
     const SharedLibrary& GetStorageAreaLibrary() const;
 
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Fri May 16 17:19:35 2025 +0200
@@ -664,5 +664,116 @@
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
     }
+
+
+    OrthancPluginCompressionType Convert(CompressionType type)
+    {
+      switch (type)
+      {
+        case CompressionType_None:
+          return OrthancPluginCompressionType_None;
+
+        case CompressionType_ZlibWithSize:
+          return OrthancPluginCompressionType_ZlibWithSize;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    CompressionType Convert(OrthancPluginCompressionType type)
+    {
+      switch (type)
+      {
+        case OrthancPluginCompressionType_None:
+          return CompressionType_None;
+
+        case OrthancPluginCompressionType_ZlibWithSize:
+          return CompressionType_ZlibWithSize;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    OrthancPluginStoreStatus Convert(StoreStatus status)
+    {
+      switch (status)
+      {
+        case StoreStatus_Success:
+          return OrthancPluginStoreStatus_Success;
+
+        case StoreStatus_AlreadyStored:
+          return OrthancPluginStoreStatus_AlreadyStored;
+
+        case StoreStatus_Failure:
+          return OrthancPluginStoreStatus_Failure;
+
+        case StoreStatus_FilteredOut:
+          return OrthancPluginStoreStatus_FilteredOut;
+
+        case StoreStatus_StorageFull:
+          return OrthancPluginStoreStatus_StorageFull;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    StoreStatus Convert(OrthancPluginStoreStatus status)
+    {
+      switch (status)
+      {
+        case OrthancPluginStoreStatus_Success:
+          return StoreStatus_Success;
+
+        case OrthancPluginStoreStatus_AlreadyStored:
+          return StoreStatus_AlreadyStored;
+
+        case OrthancPluginStoreStatus_Failure:
+          return StoreStatus_Failure;
+
+        case OrthancPluginStoreStatus_FilteredOut:
+          return StoreStatus_FilteredOut;
+
+        case OrthancPluginStoreStatus_StorageFull:
+          return StoreStatus_StorageFull;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    OrthancPluginQueueOrigin Convert(QueueOrigin origin)
+    {
+      switch (origin)
+      {
+        case QueueOrigin_Front:
+          return OrthancPluginQueueOrigin_Front;
+
+        case QueueOrigin_Back:
+          return OrthancPluginQueueOrigin_Back;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    QueueOrigin Convert(OrthancPluginQueueOrigin origin)
+    {
+      switch (origin)
+      {
+        case OrthancPluginQueueOrigin_Front:
+          return QueueOrigin_Front;
+
+        case OrthancPluginQueueOrigin_Back:
+          return QueueOrigin_Back;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
   }
 }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Fri May 16 17:19:35 2025 +0200
@@ -73,6 +73,18 @@
     ResourceType Convert(OrthancPluginResourceType type);
 
     OrthancPluginConstraintType Convert(ConstraintType constraint);
+
+    OrthancPluginCompressionType Convert(CompressionType type);
+
+    CompressionType Convert(OrthancPluginCompressionType type);
+
+    OrthancPluginStoreStatus Convert(StoreStatus type);
+
+    StoreStatus Convert(OrthancPluginStoreStatus type);
+
+    OrthancPluginQueueOrigin Convert(QueueOrigin type);
+
+    QueueOrigin Convert(OrthancPluginQueueOrigin type);
   }
 }
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Fri May 16 17:19:35 2025 +0200
@@ -1361,7 +1361,7 @@
 
     return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, &params);
   }
-  
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri May 16 17:19:35 2025 +0200
@@ -16,7 +16,7 @@
  *    - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback().
  *    - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback().
  *    - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback().
- *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea2().
+ *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea3().
  *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV4().
  *    - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback().
  *    - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback().
@@ -121,7 +121,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  6
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  99
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -469,6 +469,16 @@
     _OrthancPluginService_SetMetricsIntegerValue = 43,              /* New in Orthanc 1.12.1 */
     _OrthancPluginService_SetCurrentThreadName = 44,                /* New in Orthanc 1.12.2 */
     _OrthancPluginService_LogMessage = 45,                          /* New in Orthanc 1.12.4 */
+    _OrthancPluginService_AdoptAttachment = 46,                     /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_StoreKeyValue = 47,                       /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_DeleteKeyValue = 48,                      /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_GetKeyValue = 49,                         /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_ListKeys = 50,                            /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_EnqueueValue = 51,                        /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_DequeueValue = 52,                        /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_GetQueueSize = 53,                        /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_GetAttachmentCustomData = 54,             /* New in Orthanc 1.12.99 */
+    _OrthancPluginService_UpdateAttachmentCustomData = 55,          /* New in Orthanc 1.12.99 */
 
 
     /* Registration of callbacks */
@@ -492,6 +502,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.99 */
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -562,7 +573,7 @@
     _OrthancPluginService_StorageAreaRemove = 5005,
     _OrthancPluginService_RegisterDatabaseBackendV3 = 5006,  /* New in Orthanc 1.9.2 */
     _OrthancPluginService_RegisterDatabaseBackendV4 = 5007,  /* New in Orthanc 1.12.0 */
-    
+
     /* Primitives for handling images */
     _OrthancPluginService_GetImagePixelFormat = 6000,
     _OrthancPluginService_GetImageWidth = 6001,
@@ -791,6 +802,7 @@
     OrthancPluginCompressionType_ZlibWithSize = 1,  /*!< zlib, prefixed with uncompressed size (uint64_t) */
     OrthancPluginCompressionType_Gzip = 2,          /*!< Standard gzip compression */
     OrthancPluginCompressionType_GzipWithSize = 3,  /*!< gzip, prefixed with uncompressed size (uint64_t) */
+    OrthancPluginCompressionType_None = 4,          /*!< No compression (new in Orthanc 1.12.99) */
 
     _OrthancPluginCompressionType_INTERNAL = 0x7fffffff
   } OrthancPluginCompressionType;
@@ -1130,6 +1142,33 @@
 
 
   /**
+   * The store status response to AdoptAttachment.
+   **/
+  typedef enum
+  {
+    OrthancPluginStoreStatus_Success = 0,         /*!< The file has been stored/adopted */
+    OrthancPluginStoreStatus_AlreadyStored = 1,   /*!< The file has already been stored/adopted (only if OverwriteInstances is set to false)*/
+    OrthancPluginStoreStatus_Failure = 2,         /*!< The file could not be stored/adopted */
+    OrthancPluginStoreStatus_FilteredOut = 3,     /*!< The file has been filtered out by a lua script or a plugin */
+    OrthancPluginStoreStatus_StorageFull = 4,     /*!< The storage is full (only if MaximumStorageSize/MaximumPatientCount is set and MaximumStorageMode is Reject)*/
+    
+    _OrthancPluginStoreStatus_INTERNAL = 0x7fffffff
+  } OrthancPluginStoreStatus;
+
+  /**
+   * The supported types of enqueuing
+   **/
+  typedef enum
+  {
+    OrthancPluginQueueOrigin_Front = 0,     /*!< Pop from the front of the queue */
+    OrthancPluginQueueOrigin_Back = 1,     /*!< Pop from the back of the queue */
+    
+    _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff
+  } OrthancPluginQueueOrigin;
+
+
+
+  /**
    * @brief A 32-bit memory buffer allocated by the core system of Orthanc.
    *
    * A memory buffer allocated by the core system of Orthanc. When the
@@ -1367,8 +1406,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
-   * 
+   *
    * @warning The "content" buffer *must* have been allocated using
    * the "malloc()" function of your C standard library (i.e. nor
    * "new[]", neither a pointer to a buffer). The "free()" function of
@@ -1443,6 +1481,79 @@
 
 
   /**
+   * @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 Custom, plugin-specific data associated with the attachment (out).
+   * It must be allocated by the plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it.
+   * @param uuid The UUID of the file.
+   * @param content The content of the file (might be compressed data).
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file.
+   * @param compressionType The compression algorithm used to encode `content` (the absence of compression
+   * is indicated using `OrthancPluginCompressionType_None`).
+   * @param dicomInstance The DICOM instance being stored. Equals `NULL` if not storing a DICOM instance.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate2) (
+    OrthancPluginMemoryBuffer* customData,
+    const char* uuid,
+    const void* content,
+    uint64_t size,
+    OrthancPluginContentType type,
+    OrthancPluginCompressionType compressionType,
+    const OrthancPluginDicomInstance* dicomInstance);
+
+
+
+  /**
+   * @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 of interest.
+   * @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,
+    OrthancPluginContentType type,
+    uint64_t rangeStart,
+    const void* customData,
+    uint64_t customDataSize);
+
+
+
+  /**
+   * @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,
+    OrthancPluginContentType type,
+    const void* customData,
+    uint64_t customDataSize);
+
+
+  /**
    * @brief Callback to handle the C-Find SCP requests for worklists.
    *
    * Signature of a callback function that is triggered when Orthanc
@@ -3327,7 +3438,7 @@
    * @param read The callback function to read a file from the custom storage area.
    * @param remove The callback function to remove a file from the custom storage area.
    * @ingroup Callbacks
-   * @deprecated Please use OrthancPluginRegisterStorageArea2()
+   * @deprecated New plugins should use OrthancPluginRegisterStorageArea3()
    **/
   ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea(
     OrthancPluginContext*       context,
@@ -4915,6 +5026,8 @@
    * @ingroup Callbacks
    * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiPut()" on
    * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead.
+   * @warning This function will result in a "not implemented" error on versions of the
+   * Orthanc core above 1.12.6.
    **/
   ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaCreate(
     OrthancPluginContext*       context,
@@ -4959,6 +5072,8 @@
    * @ingroup Callbacks
    * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiGet()" on
    * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead.
+   * @warning This function will result in a "not implemented" error on versions of the
+   * Orthanc core above 1.12.6.
    **/
   ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRead(
     OrthancPluginContext*       context,
@@ -4998,6 +5113,8 @@
    * @ingroup Callbacks
    * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiDelete()" on
    * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead.
+   * @warning This function will result in a "not implemented" error on versions of the
+   * Orthanc core above 1.12.6.
    **/
   ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRemove(
     OrthancPluginContext*       context,
@@ -8917,6 +9034,7 @@
    * 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
+   * @deprecated New plugins should use OrthancPluginRegisterStorageArea3()
    **/
   ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea2(
     OrthancPluginContext*          context,
@@ -9368,6 +9486,42 @@
   }
 
 
+  typedef struct
+  {
+    OrthancPluginStorageCreate2     create;
+    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,
+    OrthancPluginStorageCreate2     create,
+    OrthancPluginStorageReadRange2  readRange,
+    OrthancPluginStorageRemove2     remove)
+  {
+    _OrthancPluginRegisterStorageArea3 params;
+    params.create = create;
+    params.readRange = readRange;
+    params.remove = remove;
+    context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, &params);
+  }
+
   /**
    * @brief Signature of a callback function that is triggered when
    * the Orthanc core requests an operation from the database plugin.
@@ -9634,6 +9788,325 @@
     return context->InvokeService(context, _OrthancPluginService_SendStreamChunk, &params);
   }
 
+  typedef struct
+  {
+    const char* uuid;
+    int32_t     contentType;
+    uint64_t    uncompressedSize;
+    const char* uncompressedHash;
+    int32_t     compressionType;
+    uint64_t    compressedSize;
+    const char* compressedHash;
+    const void* customData;
+    uint64_t    customDataSize;
+  } OrthancPluginAttachment2;
+
+
+  typedef struct
+  {
+    const void*                   buffer; /* in */
+    uint64_t                      bufferSize; /* in */
+    OrthancPluginAttachment2*     attachmentInfo;  /* in, note: uuid may not be defined */
+    OrthancPluginResourceType     attachToResourceType; /* in */
+    const char*                   attachToResourceId; /* in, can be null in case the attachment is a new instance */
+    OrthancPluginMemoryBuffer*    createdResourceId; /* out, in case the attachment is actually a new instance */
+    OrthancPluginMemoryBuffer*    attachmentUuid;    /* out */
+    OrthancPluginStoreStatus*     storeStatus;       /* out */
+  } _OrthancPluginAdoptAttachment;
+  
+  /**
+   * @brief Tell Orthanc to adopt an existing attachment.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+TODO_ATTACH_CUSTOM_DATA TODO TODO
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginAdoptAttachment(
+    OrthancPluginContext*         context,
+    const void*                   buffer,
+    uint64_t                      bufferSize,
+    OrthancPluginAttachment2*     attachmentInfo,
+    OrthancPluginResourceType     attachToResourceType,
+    const char*                   attachToResourceId,
+    OrthancPluginMemoryBuffer*    createdResourceId, /* out */
+    OrthancPluginMemoryBuffer*    attachmentUuid, /* out */
+    OrthancPluginStoreStatus*     storeStatus /* out */
+  ) 
+  {
+    _OrthancPluginAdoptAttachment params;
+    params.buffer = buffer;
+    params.bufferSize = bufferSize;
+    params.attachmentInfo = attachmentInfo;
+    params.attachToResourceType = attachToResourceType;
+    params.attachToResourceId = attachToResourceId;
+    params.createdResourceId = createdResourceId;
+    params.attachmentUuid = attachmentUuid;
+    params.storeStatus = storeStatus;
+
+    return context->InvokeService(context, _OrthancPluginService_AdoptAttachment, &params);
+  }
+
+  typedef struct
+  {
+    const char*                   attachmentUuid; /* in */
+    // OrthancPluginContentType      contentType; /* in */
+    OrthancPluginMemoryBuffer*    customData;  /* out */
+  } _OrthancPluginGetAttachmentCustomData;
+
+  /**
+   * @brief Retrieve attachment customData from the Orthanc DB.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+TODO_ATTACH_CUSTOM_DATA TODO TODO
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetAttachmentCustomData(
+    OrthancPluginContext*         context,
+    const char*                   attachmentUuid, /* in */
+    // OrthancPluginContentType      contentType, /* in */
+    OrthancPluginMemoryBuffer*    customData /* out */
+  ) 
+  {
+    _OrthancPluginGetAttachmentCustomData params;
+    params.attachmentUuid = attachmentUuid;
+    // params.contentType = contentType;
+    params.customData = customData;
+
+    return context->InvokeService(context, _OrthancPluginService_GetAttachmentCustomData, &params);
+  }
+
+  typedef struct
+  {
+    const char*                   attachmentUuid; /* in */
+    const char*                   customData;  /* in */
+    int64_t                       customDataSize; /* in */
+  } _OrthancPluginUpdateAttachmentCustomData;
+
+
+  /**
+   * @brief Update attachment custom data in the Orthanc DB.  E.g if a plugin has moved an attachment.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+TODO_ATTACH_CUSTOM_DATA TODO TODO
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginUpdateAttachmentCustomData(
+    OrthancPluginContext*         context,
+    const char*                   attachmentUuid, /* in */
+    const char*                   customData,  /* in */
+    int64_t                       customDataSize /* in */
+  ) 
+  {
+    _OrthancPluginUpdateAttachmentCustomData params;
+    params.attachmentUuid = attachmentUuid;
+    params.customData = customData;
+    params.customDataSize = customDataSize;
+
+    return context->InvokeService(context, _OrthancPluginService_UpdateAttachmentCustomData, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*                   storeId;
+    const char*                   key;
+    const char*                   value;
+    uint64_t                      valueSize;
+  } _OrthancPluginStoreKeyValue;
+  
+  /**
+   * @brief Tell Orthanc to store a key-value in its store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the store
+   * @param key The key of the value to store (Note: storeId + key must be unique)
+   * @param value The value to store
+   * @param valueSize The lenght of the value to store
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStoreKeyValue(
+    OrthancPluginContext*         context,
+    const char*                   storeId, /* in */
+    const char*                   key, /* in */
+    const char*                   value, /* in */
+    uint64_t                      valueSize /* in */)
+  {
+    _OrthancPluginStoreKeyValue params;
+    params.storeId = storeId;
+    params.key = key;
+    params.value = value;
+    params.valueSize = valueSize;
+
+    return context->InvokeService(context, _OrthancPluginService_StoreKeyValue, &params);
+  }
+
+  typedef struct
+  {
+    const char*                   storeId;
+    const char*                   key;
+  } _OrthancPluginDeleteKeyValue;
+  
+  /**
+   * @brief Tell Orthanc to delete a key-value from its store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the store
+   * @param key The key of the value to store (Note: storeId + key must be unique)
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDeleteKeyValue(
+    OrthancPluginContext*         context,
+    const char*                   storeId, /* in */
+    const char*                   key /* in */)
+  {
+    _OrthancPluginDeleteKeyValue params;
+    params.storeId = storeId;
+    params.key = key;
+
+    return context->InvokeService(context, _OrthancPluginService_DeleteKeyValue, &params);
+  }
+
+  typedef struct
+  {
+    const char*                   storeId;
+    const char*                   key;
+    OrthancPluginMemoryBuffer*    value;
+  } _OrthancPluginGetKeyValue;
+  
+  /**
+   * @brief Get the value associated to this key in the key-value store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the store
+   * @param key The key of the value to retrieve from the store (Note: storeId + key must be unique)
+   * @param value The value retrieved from the store
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetKeyValue(
+    OrthancPluginContext*         context,
+    const char*                   storeId, /* in */
+    const char*                   key, /* in */
+    OrthancPluginMemoryBuffer*    value /* out */)
+  {
+    _OrthancPluginGetKeyValue params;
+    params.storeId = storeId;
+    params.key = key;
+    params.value = value;
+
+    return context->InvokeService(context, _OrthancPluginService_GetKeyValue, &params);
+  }
+
+  typedef struct
+  {
+    const char*                   storeId;
+    uint64_t                      since;
+    uint64_t                      limit;
+    OrthancPluginMemoryBuffer*    keys;
+  } _OrthancPluginListKeys;
+
+
+  /**
+   * @brief List the keys from a key-value store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the store
+   * @param since The index of the first key to return when sorted alphabetically
+   * @param limit The number of keys to return (0 for no limit)
+   * @param keys The keys serialized in a json string
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginListKeys(
+    OrthancPluginContext*         context,
+    const char*                   storeId, /* in */
+    uint64_t                      since, /* in */
+    uint64_t                      limit, /* in */
+    OrthancPluginMemoryBuffer*    keys /* out */)
+  {
+    _OrthancPluginListKeys params;
+    params.storeId = storeId;
+    params.since = since;
+    params.limit = limit;
+    params.keys = keys;
+
+    return context->InvokeService(context, _OrthancPluginService_ListKeys, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*                   queueId;
+    const char*                   value;
+    uint64_t                      valueSize;
+  } _OrthancPluginEnqueueValue;
+
+  /**
+   * @brief Tell Orthanc to store a value in a queue.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param queueId A unique identifier identifying both the plugin and the queue
+   * @param value The value to store
+   * @param valueSize The lenght of the value to store
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginEnqueueValue(
+    OrthancPluginContext*         context,
+    const char*                   queueId, /* in */
+    const char*                   value, /* in */
+    uint64_t                      valueSize /* in */)
+  {
+    _OrthancPluginEnqueueValue params;
+    params.queueId = queueId;
+    params.value = value;
+    params.valueSize = valueSize;
+
+    return context->InvokeService(context, _OrthancPluginService_EnqueueValue, &params);
+  }
+
+  typedef struct
+  {
+    const char*                   queueId;
+    OrthancPluginQueueOrigin      origin;
+    OrthancPluginMemoryBuffer*    value;
+  } _OrthancPluginDequeueValue;
+  
+  /**
+   * @brief Dequeue a value from a queue.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param queueId A unique identifier identifying both the plugin and the queue
+   * @param origin The extremity of the queue the value is dequeue from (back for LIFO or front for FIFO)
+   * @param value The value retrieved from the queue
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDequeueValue(
+    OrthancPluginContext*         context,
+    const char*                   queueId, /* in */
+    OrthancPluginQueueOrigin      origin, /* in */
+    OrthancPluginMemoryBuffer*    value /* out */)
+  {
+    _OrthancPluginDequeueValue params;
+    params.queueId = queueId;
+    params.origin = origin;
+    params.value = value;
+
+    return context->InvokeService(context, _OrthancPluginService_DequeueValue, &params);
+  }
+
+  typedef struct
+  {
+    const char*                   queueId;
+    uint64_t*                     size;
+  } _OrthancPluginGetQueueSize;
+  
+  /**
+   * @brief Get the number of elements in a queue.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param queueId A unique identifier identifying both the plugin and the queue
+   * @param size The number of elements in the queue
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetQueueSize(
+    OrthancPluginContext*         context,
+    const char*                   queueId, /* in */
+    uint64_t*                     size /* out */)
+  {
+    _OrthancPluginGetQueueSize params;
+    params.queueId = queueId;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_GetQueueSize, &params);
+  }
 
 #ifdef  __cplusplus
 }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Fri May 16 17:19:35 2025 +0200
@@ -55,6 +55,7 @@
   int32   compression_type = 5;  // opaque "CompressionType" in Orthanc
   uint64  compressed_size = 6;
   string  compressed_hash = 7;
+  string  custom_data = 8;       // added in v 1.12.99
 }
 
 enum ResourceType {
@@ -94,6 +95,11 @@
   ORDERING_CAST_FLOAT = 2;
 }
 
+enum QueueOrigin {
+  QUEUE_ORIGIN_FRONT = 0;
+  QUEUE_ORIGIN_BACK = 1;
+}
+
 message ServerIndexChange {
   int64         seq = 1;
   int32         change_type = 2;   // opaque "ChangeType" in Orthanc
@@ -166,6 +172,8 @@
     bool has_measure_latency = 7;
     bool supports_find = 8;         // New in Orthanc 1.12.5
     bool has_extended_changes = 9;  // New in Orthanc 1.12.5
+    bool supports_key_value_stores = 10;  // New in Orthanc 1.12.99
+    bool supports_queues = 11;            // New in Orthanc 1.12.99
   }
 }
 
@@ -321,6 +329,11 @@
   OPERATION_FIND = 50;                        // New in Orthanc 1.12.5
   OPERATION_GET_CHANGES_EXTENDED = 51;        // New in Orthanc 1.12.5
   OPERATION_COUNT_RESOURCES = 52;             // New in Orthanc 1.12.5
+  OPERATION_STORE_KEY_VALUE = 53;             // New in Orthanc 1.12.99
+  OPERATION_DELETE_KEY_VALUE = 54;            // New in Orthanc 1.12.99
+  OPERATION_GET_KEY_VALUE = 55;               // New in Orthanc 1.12.99
+  OPERATION_ENQUEUE_VALUE = 56;               // New in Orthanc 1.12.99
+  OPERATION_DEQUEUE_VALUE = 57;               // New in Orthanc 1.12.99
 }
 
 message Rollback {
@@ -974,6 +987,59 @@
   }
 }
 
+message StoreKeyValue {
+  message Request {
+    string plugin_id = 1;
+    string key = 2;
+    string value = 3;
+  }
+
+  message Response {
+  }
+}
+
+message DeleteKeyValue {
+  message Request {
+    string plugin_id = 1;
+    string key = 2;
+  }
+
+  message Response {
+  }
+}
+
+message GetKeyValue {
+  message Request {
+    string plugin_id = 1;
+    string key = 2;
+  }
+
+  message Response {
+    string value = 1;
+  }
+}
+
+message EnqueueValue {
+  message Request {
+    string plugin_id = 1;
+    string value = 2;
+  }
+
+  message Response {
+  }
+}
+
+message DequeueValue {
+  message Request {
+    string plugin_id = 1;
+    QueueOrigin origin = 2;
+  }
+
+  message Response {
+    string value = 1;
+  }
+}
+
 message TransactionRequest {
   sfixed64              transaction = 1;
   TransactionOperation  operation = 2;
@@ -1031,6 +1097,11 @@
   Find.Request                            find = 150;
   GetChangesExtended.Request              get_changes_extended = 151;
   Find.Request                            count_resources = 152;
+  StoreKeyValue.Request                   store_key_value = 153;
+  DeleteKeyValue.Request                  delete_key_value = 154;
+  GetKeyValue.Request                     get_key_value = 155;
+  EnqueueValue.Request                    enqueue_value = 156;
+  DequeueValue.Request                    dequeue_value = 157;
 }
 
 message TransactionResponse {
@@ -1087,6 +1158,11 @@
   repeated Find.Response                   find = 150;   // One message per found resource
   GetChangesExtended.Response              get_changes_extended = 151;
   CountResources.Response                  count_resources = 152;
+  StoreKeyValue.Response                   store_key_value = 153;
+  DeleteKeyValue.Response                  delete_key_value = 154;
+  GetKeyValue.Response                     get_key_value = 155;
+  EnqueueValue.Request                    enqueue_value = 156;
+  DequeueValue.Request                    dequeue_value = 157;
 }
 
 enum RequestType {
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Fri May 16 17:19:35 2025 +0200
@@ -4347,4 +4347,150 @@
     }
   }
 #endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 99)
+
+  KeyValueStore::KeyValueStore(const std::string& storeId)
+  : storeId_(storeId)
+  {
+  }
+
+  void KeyValueStore::Store(const std::string& key, const std::string& value)
+  {
+    OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(),
+                               storeId_.c_str(),
+                               key.c_str(),
+                               value.c_str(),
+                               value.size());
+  }
+
+  bool KeyValueStore::Get(std::string& value, const std::string& key)
+  {
+    OrthancPlugins::MemoryBuffer valueBuffer;
+    OrthancPluginErrorCode ret = OrthancPluginGetKeyValue(OrthancPlugins::GetGlobalContext(),
+                                                          storeId_.c_str(),
+                                                          key.c_str(),
+                                                          *valueBuffer);
+
+    if (ret == OrthancPluginErrorCode_Success)
+    {
+      if (!valueBuffer.IsEmpty())
+      {
+        value.assign(valueBuffer.GetData(), valueBuffer.GetSize());
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    
+    return false;
+  }
+
+  void KeyValueStore::Delete(const std::string& key)
+  {
+    OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(),
+                                storeId_.c_str(),
+                                key.c_str());
+  }
+
+  bool KeyValueStore::GetAllKeys(std::list<std::string>& keys, uint64_t since, uint64_t limit)
+  {
+    OrthancPlugins::MemoryBuffer keysListBuffer;
+    OrthancPluginErrorCode ret = OrthancPluginListKeys(OrthancPlugins::GetGlobalContext(),
+                                                       storeId_.c_str(),
+                                                       since,
+                                                       limit,
+                                                       *keysListBuffer);
+
+    if (ret == OrthancPluginErrorCode_Success)
+    {
+      Json::Value jsonKeys;
+      keysListBuffer.ToJson(jsonKeys);
+
+      for (Json::ArrayIndex i = 0; i < jsonKeys.size(); ++i)
+      {
+        keys.push_back(jsonKeys[i].asString());
+      }
+
+      // return true if all values have been read
+      return limit == 0 || (jsonKeys.size() < limit);
+    }
+    
+#if HAS_ORTHANC_EXCEPTION == 1
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Unable to list keys");
+#else
+    ORTHANC_PLUGINS_LOG_ERROR("Unable to list keys");
+    ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+#endif
+  }
+
+  Queue::Queue(const std::string& queueId)
+  : queueId_(queueId)
+  {
+  }
+
+  void Queue::PushBack(const std::string& value)
+  {
+    OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(),
+                              queueId_.c_str(),
+                              value.c_str(),
+                              value.size());
+  }
+
+  bool Queue::PopInternal(std::string& value, OrthancPluginQueueOrigin origin)
+  {
+    OrthancPlugins::MemoryBuffer valueBuffer;
+    OrthancPluginErrorCode ret = OrthancPluginDequeueValue(OrthancPlugins::GetGlobalContext(),
+                                                           queueId_.c_str(),
+                                                           origin,
+                                                           *valueBuffer);
+
+    if (ret == OrthancPluginErrorCode_Success)
+    {
+      if (!valueBuffer.IsEmpty())
+      {
+        value.assign(valueBuffer.GetData(), valueBuffer.GetSize());
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+    
+    return false;
+  }
+
+  bool Queue::PopBack(std::string& value)
+  {
+    return PopInternal(value, OrthancPluginQueueOrigin_Back);
+  }
+
+  bool Queue::PopFront(std::string& value)
+  {
+    return PopInternal(value, OrthancPluginQueueOrigin_Front);
+  }
+
+  uint64_t Queue::GetSize()
+  {
+    uint64_t size = 0;
+    OrthancPluginErrorCode ret = OrthancPluginGetQueueSize(OrthancPlugins::GetGlobalContext(),
+                                                           queueId_.c_str(),
+                                                           &size);
+    if (ret != OrthancPluginErrorCode_Success)
+    {
+#if HAS_ORTHANC_EXCEPTION == 1
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Unable get queue size");
+#else
+      ORTHANC_PLUGINS_LOG_ERROR("Unable get queue size");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+#endif
+    }
+
+    return size;
+  }
+#endif
+
 }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Fri May 16 17:19:35 2025 +0200
@@ -1618,4 +1618,45 @@
     bool GetAnswerJson(Json::Value& output) const;
   };
 #endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 99)
+
+  class KeyValueStore : public boost::noncopyable
+  {
+  private:
+    std::string storeId_;
+
+  public:
+    KeyValueStore(const std::string& storeId);
+
+    void Store(const std::string& key, const std::string& value);
+
+    bool Get(std::string& value, const std::string& key);
+
+    void Delete(const std::string& key);
+
+    bool GetAllKeys(std::list<std::string>& keys, uint64_t since, uint64_t limit);
+  };
+
+  class Queue : public boost::noncopyable
+  {
+  private:
+    std::string queueId_;
+
+    bool PopInternal(std::string& value, OrthancPluginQueueOrigin origin);
+
+  public:
+    Queue(const std::string& queueId);
+
+    void PushBack(const std::string& value);
+
+    bool PopFront(std::string& value);
+
+    bool PopBack(std::string& value);
+
+    uint64_t GetSize();
+  };
+
+#endif
+
 }
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Fri May 16 17:19:35 2025 +0200
@@ -113,7 +113,7 @@
 {
   try
   {
-    std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->Read(uuid, Convert(type)));
+    std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->ReadWhole(uuid, Convert(type)));
 
     // copy from a buffer allocated on plugin's heap into a buffer allocated on core's heap
     if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, buffer->GetSize()) != OrthancPluginErrorCode_Success)
--- a/OrthancServer/Resources/RunCppCheck.sh	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Resources/RunCppCheck.sh	Fri May 16 17:19:35 2025 +0200
@@ -32,8 +32,8 @@
 useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:91
 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99
 useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275
-assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:277
-assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026
+assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:279
+assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1028
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292
 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391
 assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3058
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri May 16 17:19:35 2025 +0200
@@ -56,6 +56,9 @@
       bool hasMeasureLatency_;
       bool hasFindSupport_;
       bool hasExtendedChanges_;
+      bool hasAttachmentCustomDataSupport_;
+      bool hasKeyValueStoresSupport_;
+      bool hasQueuesSupport_;
 
     public:
       Capabilities() :
@@ -66,7 +69,10 @@
         hasUpdateAndGetStatistics_(false),
         hasMeasureLatency_(false),
         hasFindSupport_(false),
-        hasExtendedChanges_(false)
+        hasExtendedChanges_(false),
+        hasAttachmentCustomDataSupport_(false),
+        hasKeyValueStoresSupport_(false),
+        hasQueuesSupport_(false)
       {
       }
 
@@ -100,6 +106,16 @@
         return hasLabelsSupport_;
       }
 
+      void SetAttachmentCustomDataSupport(bool value)
+      {
+        hasAttachmentCustomDataSupport_ = value;
+      }
+
+      bool HasAttachmentCustomDataSupport() const
+      {
+        return hasAttachmentCustomDataSupport_;
+      }
+      
       void SetHasExtendedChanges(bool value)
       {
         hasExtendedChanges_ = value;
@@ -149,6 +165,26 @@
       {
         return hasFindSupport_;
       }
+
+      void SetKeyValueStoresSupport(bool value)
+      {
+        hasKeyValueStoresSupport_ = value;
+      }
+
+      bool HasKeyValueStoresSupport() const
+      {
+        return hasKeyValueStoresSupport_;
+      }
+
+      void SetQueuesSupport(bool value)
+      {
+        hasQueuesSupport_ = value;
+      }
+
+      bool HasQueuesSupport() const
+      {
+        return hasQueuesSupport_;
+      }
     };
 
 
@@ -250,6 +286,13 @@
                                     int64_t id,
                                     FileContentType contentType) = 0;
 
+      virtual bool GetAttachment(FileInfo& attachment,
+                                 int64_t& revision,
+                                 const std::string& attachmentUuid) = 0;
+
+      virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid,
+                                              const std::string& customData) = 0;
+
       /**
        * If "shared" is "true", the property is shared by all the
        * Orthanc servers that access the same database. If "shared" is
@@ -390,6 +433,39 @@
                                       int64_t to,
                                       uint32_t limit,
                                       const std::set<ChangeType>& filterType) = 0;
+
+      // New in Orthanc 1.12.99
+      virtual void StoreKeyValue(const std::string& storeId,
+                                 const std::string& key,
+                                 const std::string& value) = 0;
+
+      // New in Orthanc 1.12.99
+      virtual void DeleteKeyValue(const std::string& storeId,
+                                  const std::string& key) = 0;
+
+      // New in Orthanc 1.12.99
+      virtual bool GetKeyValue(std::string& value,
+                               const std::string& storeId,
+                               const std::string& key) = 0;
+
+      // New in Orthanc 1.12.99
+      virtual void ListKeys(std::list<std::string>& keys,
+                            const std::string& storeId,
+                            uint64_t since,
+                            uint64_t limit) = 0;
+
+      // New in Orthanc 1.12.99
+      virtual void EnqueueValue(const std::string& queueId,
+                                const std::string& value) = 0;
+
+      // New in Orthanc 1.12.99
+      virtual bool DequeueValue(std::string& value,
+                                const std::string& queueId,
+                                QueueOrigin origin) = 0;
+
+      virtual void GetQueueSize(uint64_t& size,
+                                const std::string& queueId) = 0;
+
     };
 
 
@@ -456,7 +532,7 @@
     virtual unsigned int GetDatabaseVersion() = 0;
 
     virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea) = 0;
+                         IPluginStorageArea& storageArea) = 0;
 
     virtual const Capabilities GetDatabaseCapabilities() const = 0;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/InstallKeyValueStoresAndQueues.sql	Fri May 16 17:19:35 2025 +0200
@@ -0,0 +1,37 @@
+-- Orthanc - A Lightweight, RESTful DICOM Store
+-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+-- Department, University Hospital of Liege, Belgium
+-- Copyright (C) 2017-2023 Osimis S.A., Belgium
+-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
+-- Copyright (C) 2021-2025 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/>.
+
+
+CREATE TABLE KeyValueStores(
+       storeId TEXT NOT NULL,
+       key TEXT NOT NULL,
+       value TEXT NOT NULL,
+       PRIMARY KEY(storeId, key)  -- Prevents duplicates
+       );
+
+CREATE INDEX KeyValueStoresIndex ON KeyValueStores (storeId, key);
+
+CREATE TABLE Queues (
+       id INTEGER PRIMARY KEY AUTOINCREMENT,
+       queueId TEXT NOT NULL,
+       value TEXT
+);
+
+CREATE INDEX QueuesIndex ON Queues (queueId, id);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql	Fri May 16 17:19:35 2025 +0200
@@ -0,0 +1,66 @@
+-- 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 installs revision and customData without changing the Orthanc database version
+--
+
+-- Add new columns for revision
+ALTER TABLE Metadata ADD COLUMN revision INTEGER;
+ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER;
+
+-- Add new column for customData
+ALTER TABLE AttachedFiles ADD COLUMN customData TEXT;
+
+
+-- add another AttachedFileDeleted trigger 
+-- We want to keep backward compatibility and avoid changing the database version number (which would force
+-- users to upgrade the DB).  By keeping backward compatibility, we mean "allow a user to run a previous Orthanc
+-- version after it has run this update script".
+-- We must keep the signature of the initial trigger (it is impossible to have 2 triggers on the same event).
+-- We tried adding a trigger on "BEFORE DELETE" but then it is being called when running the previous Orthanc
+-- which makes it fail.
+-- But, we need the customData in the C++ function that is called when a AttachedFiles is deleted.
+-- The trick is then to save the customData in a DeletedFiles table.
+-- The SignalFileDeleted C++ function will then get the customData from this table and delete the entry.
+-- Drawback: if you downgrade Orthanc, the DeletedFiles table will remain and will be populated by the trigger
+-- but not consumed by the C++ function -> we consider this is an acceptable drawback for a few people compared 
+-- to the burden of upgrading the DB.
+
+CREATE TABLE DeletedFiles(
+       uuid TEXT NOT NULL,        -- 0
+       customData TEXT            -- 1
+);
+
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  INSERT INTO DeletedFiles VALUES(old.uuid, old.customData);
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           old.uncompressedMD5, old.compressedMD5
+                           );
+END;
+
+-- Record that this upgrade has been performed
+
+INSERT INTO GlobalProperties VALUES (7, 1);  -- GlobalProperty_SQLiteHasCustomDataAndRevision
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/Database/PrepareDatabase.sql	Fri May 16 17:19:35 2025 +0200
@@ -55,6 +55,7 @@
        id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
        type INTEGER,
        value TEXT,
+       -- revision INTEGER,      -- New in Orthanc 1.12.99 (added in InstallRevisionAndCustomData.sql)
        PRIMARY KEY(id, type)
        );
 
@@ -67,6 +68,8 @@
        compressionType INTEGER,
        uncompressedMD5 TEXT,  -- New in Orthanc 0.7.3 (database v4)
        compressedMD5 TEXT,    -- New in Orthanc 0.7.3 (database v4)
+       -- revision INTEGER,      -- New in Orthanc 1.12.99 (added in InstallRevisionAndCustomData.sql)
+       -- customData TEXT,       -- New in Orthanc 1.12.99 (added in InstallRevisionAndCustomData.sql)
        PRIMARY KEY(id, fileType)
        );              
 
@@ -129,7 +132,8 @@
   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
+                           );
 END;
 
 CREATE TRIGGER ResourceDeleted
@@ -155,6 +159,25 @@
   INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId);
 END;
 
+-- new in Orthanc 1.12.99
+CREATE TABLE KeyValueStores(
+       storeId TEXT NOT NULL,
+       key TEXT NOT NULL,
+       value TEXT NOT NULL,
+       PRIMARY KEY(storeId, key)  -- Prevents duplicates
+       );
+
+-- new in Orthanc 1.12.99
+CREATE INDEX KeyValueStoresIndex ON KeyValueStores (storeId, key);
+
+-- new in Orthanc 1.12.99
+CREATE TABLE Queues (
+       id INTEGER PRIMARY KEY AUTOINCREMENT,
+       queueId TEXT NOT NULL,
+       value TEXT
+);
+
+CREATE INDEX QueuesIndex ON Queues (queueId, id);
 
 -- Set the version of the database schema
 -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri May 16 17:19:35 2025 +0200
@@ -41,6 +41,8 @@
 #include <stdio.h>
 #include <boost/lexical_cast.hpp>
 
+static std::map<std::string, std::string> filesToDeleteCustomData;
+
 namespace Orthanc
 {  
   static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec)
@@ -410,8 +412,9 @@
                                const FileInfo& attachment,
                                int64_t revision) ORTHANC_OVERRIDE
     {
-      // TODO - REVISIONS
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5) VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+        "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5, revision, customData) "
+        "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
       s.BindInt64(0, id);
       s.BindInt(1, attachment.GetContentType());
       s.BindString(2, attachment.GetUuid());
@@ -420,10 +423,11 @@
       s.BindInt(5, attachment.GetCompressionType());
       s.BindString(6, attachment.GetUncompressedMD5());
       s.BindString(7, attachment.GetCompressedMD5());
+      s.BindInt(8, revision);
+      s.BindString(9, attachment.GetCustomData());
       s.Run();
     }
 
-
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
                                       const DatabaseDicomTagConstraints& lookup,
@@ -473,10 +477,12 @@
 #define C3_STRING_1 3
 #define C4_STRING_2 4
 #define C5_STRING_3 5
-#define C6_INT_1 6
-#define C7_INT_2 7
-#define C8_BIG_INT_1 8
-#define C9_BIG_INT_2 9
+#define C6_STRING_4 6
+#define C7_INT_1 7
+#define C8_INT_2 8
+#define C9_INT_3 9
+#define C10_BIG_INT_1 10
+#define C11_BIG_INT_2 11
 
 #define QUERY_LOOKUP 1
 #define QUERY_MAIN_DICOM_TAGS 2
@@ -588,10 +594,12 @@
              "  Lookup.publicId AS c3_string1, "
              "  NULL AS c4_string2, "
              "  NULL AS c5_string3, "
-             "  NULL AS c6_int1, "
-             "  NULL AS c7_int2, "
-             "  NULL AS c8_big_int1, "
-             "  NULL AS c9_big_int2 "
+             "  NULL AS c6_string4, "
+             "  NULL AS c7_int1, "
+             "  NULL AS c8_int2, "
+             "  NULL AS c9_int3, "
+             "  NULL AS c10_big_int1, "
+             "  NULL AS c11_big_int2 "
              "  FROM Lookup ";
 
       // need one instance info ? (part 2: execute the queries)
@@ -605,10 +613,12 @@
                "    instancePublicId AS c3_string1, "
                "    NULL AS c4_string2, "
                "    NULL AS c5_string3, "
-               "    NULL AS c6_int1, "
-               "    NULL AS c7_int2, "
-               "    instanceInternalId AS c8_big_int1, "
-               "    NULL AS c9_big_int2 "
+               "    NULL AS c6_string4, "
+               "    NULL AS c7_int1, "
+               "    NULL AS c8_int2, "
+               "    NULL AS c9_int3, "
+               "    instanceInternalId AS c10_big_int1, "
+               "    NULL AS c11_big_int2 "
                "   FROM OneInstance ";
 
         sql += "   UNION SELECT"
@@ -618,10 +628,12 @@
                "    Metadata.value AS c3_string1, "
                "    NULL AS c4_string2, "
                "    NULL AS c5_string3, "
-               "    Metadata.type AS c6_int1, "
-               "    NULL AS c7_int2, "
-               "    NULL AS c8_big_int1, "
-               "    NULL AS c9_big_int2 "
+               "    NULL AS c6_string4, "
+               "    Metadata.type AS c7_int1, "
+               "    NULL AS c8_int2, "
+               "    NULL AS c9_int3, "
+               "    NULL AS c10_big_int1, "
+               "    NULL AS c11_big_int2 "
                "   FROM OneInstance "
                "   INNER JOIN Metadata ON Metadata.id = OneInstance.instanceInternalId ";
               
@@ -632,10 +644,12 @@
                "    uuid AS c3_string1, "
                "    uncompressedMD5 AS c4_string2, "
                "    compressedMD5 AS c5_string3, "
-               "    fileType AS c6_int1, "
-               "    compressionType AS c7_int2, "
-               "    compressedSize AS c8_big_int1, "
-               "    uncompressedSize AS c9_big_int2 "
+               "    customData AS c6_string4, "
+               "    fileType AS c7_int1, "
+               "    compressionType AS c8_int2, "
+               "    revision AS c9_int3, "
+               "    compressedSize AS c10_big_int1, "
+               "    uncompressedSize AS c11_big_int2 "
                "   FROM OneInstance "
                "   INNER JOIN AttachedFiles ON AttachedFiles.id = OneInstance.instanceInternalId ";
 
@@ -651,10 +665,12 @@
                "  value AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  tagGroup AS c6_int1, "
-               "  tagElement AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  tagGroup AS c7_int1, "
+               "  tagElement AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId ";
       }
@@ -669,10 +685,12 @@
                "  value AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  type AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  type AS c7_int1, "
+               "  revision AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN Metadata ON Metadata.id = Lookup.internalId ";
       }
@@ -687,10 +705,12 @@
                "  uuid AS c3_string1, "
                "  uncompressedMD5 AS c4_string2, "
                "  compressedMD5 AS c5_string3, "
-               "  fileType AS c6_int1, "
-               "  compressionType AS c7_int2, "
-               "  compressedSize AS c8_big_int1, "
-               "  uncompressedSize AS c9_big_int2 "
+               "  customData AS c6_string4, "
+               "  fileType AS c7_int1, "
+               "  compressionType AS c8_int2, "
+               "  revision AS c9_int3, "
+               "  compressedSize AS c10_big_int1, "
+               "  uncompressedSize AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId ";
       }
@@ -706,10 +726,12 @@
                "  label AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  NULL AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  NULL AS c7_int1, "
+               "  NULL AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN Labels ON Labels.id = Lookup.internalId ";
       }
@@ -726,10 +748,12 @@
                  "  value AS c3_string1, "
                  "  NULL AS c4_string2, "
                  "  NULL AS c5_string3, "
-                 "  tagGroup AS c6_int1, "
-                 "  tagElement AS c7_int2, "
-                 "  NULL AS c8_big_int1, "
-                 "  NULL AS c9_big_int2 "
+                 "  NULL AS c6_string4, "
+                 "  tagGroup AS c7_int1, "
+                 "  tagElement AS c8_int2, "
+                 "  NULL AS c9_int3, "
+                 "  NULL AS c10_big_int1, "
+                 "  NULL AS c11_big_int2 "
                  "FROM Lookup "
                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                  "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId ";
@@ -745,10 +769,12 @@
                  "  value AS c3_string1, "
                  "  NULL AS c4_string2, "
                  "  NULL AS c5_string3, "
-                 "  type AS c6_int1, "
-                 "  NULL AS c7_int2, "
-                 "  NULL AS c8_big_int1, "
-                 "  NULL AS c9_big_int2 "
+                 "  NULL AS c6_string4, "
+                 "  type AS c7_int1, "
+                 "  revision AS c8_int2, "
+                 "  NULL AS c9_int3, "
+                 "  NULL AS c10_big_int1, "
+                 "  NULL AS c11_big_int2 "
                  "FROM Lookup "
                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                  "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId ";        
@@ -766,10 +792,12 @@
                   "  value AS c3_string1, "
                   "  NULL AS c4_string2, "
                   "  NULL AS c5_string3, "
-                  "  tagGroup AS c6_int1, "
-                  "  tagElement AS c7_int2, "
-                  "  NULL AS c8_big_int1, "
-                  "  NULL AS c9_big_int2 "
+                  "  NULL AS c6_string4, "
+                  "  tagGroup AS c7_int1, "
+                  "  tagElement AS c8_int2, "
+                  "  NULL AS c9_int3, "
+                  "  NULL AS c10_big_int1, "
+                  "  NULL AS c11_big_int2 "
                   "FROM Lookup "
                   "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                   "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -786,10 +814,12 @@
                   "  value AS c3_string1, "
                   "  NULL AS c4_string2, "
                   "  NULL AS c5_string3, "
-                  "  type AS c6_int1, "
-                  "  NULL AS c7_int2, "
-                  "  NULL AS c8_big_int1, "
-                  "  NULL AS c9_big_int2 "
+                  "  NULL AS c6_string4, "
+                  "  type AS c7_int1, "
+                  "  revision AS c8_int2, "
+                  "  NULL AS c9_int3, "
+                  "  NULL AS c10_big_int1, "
+                  "  NULL AS c11_big_int2 "
                   "FROM Lookup "
                   "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                   "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -808,10 +838,12 @@
                "  value AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  tagGroup AS c6_int1, "
-               "  tagElement AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  tagGroup AS c7_int1, "
+               "  tagElement AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                "  INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))); 
@@ -827,10 +859,12 @@
                 "  value AS c3_string1, "
                 "  NULL AS c4_string2, "
                 "  NULL AS c5_string3, "
-                "  tagGroup AS c6_int1, "
-                "  tagElement AS c7_int2, "
-                "  NULL AS c8_big_int1, "
-                "  NULL AS c9_big_int2 "
+                "  NULL AS c6_string4, "
+                "  tagGroup AS c7_int1, "
+                "  tagElement AS c8_int2, "
+                "  NULL AS c9_int3, "
+                "  NULL AS c10_big_int1, "
+                "  NULL AS c11_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -847,10 +881,12 @@
                "  parentLevel.publicId AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  NULL AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  NULL AS c7_int1, "
+               "  NULL AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId "
                "  INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId ";
@@ -866,10 +902,12 @@
                 "  value AS c3_string1, "
                 "  NULL AS c4_string2, "
                 "  NULL AS c5_string3, "
-                "  type AS c6_int1, "
-                "  NULL AS c7_int2, "
-                "  NULL AS c8_big_int1, "
-                "  NULL AS c9_big_int2 "
+                "  NULL AS c6_string4, "
+                "  type AS c7_int1, "
+                "  revision AS c8_int2, "
+                "  NULL AS c9_int3, "
+                "  NULL AS c10_big_int1, "
+                "  NULL AS c11_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))) + ") ";
@@ -885,10 +923,12 @@
                 "  value AS c3_string1, "
                 "  NULL AS c4_string2, "
                 "  NULL AS c5_string3, "
-                "  type AS c6_int1, "
-                "  NULL AS c7_int2, "
-                "  NULL AS c8_big_int1, "
-                "  NULL AS c9_big_int2 "
+                "  NULL AS c6_string4, "
+                "  type AS c7_int1, "
+                "  revision AS c8_int2, "
+                "  NULL AS c9_int3, "
+                "  NULL AS c10_big_int1, "
+                "  NULL AS c11_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -907,10 +947,12 @@
                "  childLevel.publicId AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  NULL AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  NULL AS c7_int1, "
+               "  NULL AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId ";
       }
@@ -926,10 +968,12 @@
                "  NULL AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  COUNT(*) AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  COUNT(*) AS c7_int1, "
+               "  NULL AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId GROUP BY Lookup.internalId ";
       }
@@ -945,10 +989,12 @@
               "  grandChildLevel.publicId AS c3_string1, "
               "  NULL AS c4_string2, "
               "  NULL AS c5_string3, "
-              "  NULL AS c6_int1, "
-              "  NULL AS c7_int2, "
-              "  NULL AS c8_big_int1, "
-              "  NULL AS c9_big_int2 "
+              "  NULL AS c6_string4, "
+              "  NULL AS c7_int1, "
+              "  NULL AS c8_int2, "
+              "  NULL AS c9_int3, "
+              "  NULL AS c10_big_int1, "
+              "  NULL AS c11_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId ";
@@ -964,10 +1010,12 @@
               "  NULL AS c3_string1, "
               "  NULL AS c4_string2, "
               "  NULL AS c5_string3, "
-               "  COUNT(*) AS c6_int1, "
-              "  NULL AS c7_int2, "
-              "  NULL AS c8_big_int1, "
-              "  NULL AS c9_big_int2 "
+              "  NULL AS c6_string4, "
+              "  COUNT(*) AS c7_int1, "
+              "  NULL AS c8_int2, "
+              "  NULL AS c9_int3, "
+              "  NULL AS c10_big_int1, "
+              "  NULL AS c11_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId GROUP BY Lookup.internalId ";
@@ -983,10 +1031,12 @@
               "  grandGrandChildLevel.publicId AS c3_string1, "
               "  NULL AS c4_string2, "
               "  NULL AS c5_string3, "
-              "  NULL AS c6_int1, "
-              "  NULL AS c7_int2, "
-              "  NULL AS c8_big_int1, "
-              "  NULL AS c9_big_int2 "
+              "  NULL AS c6_string4, "
+              "  NULL AS c7_int1, "
+              "  NULL AS c8_int2, "
+              "  NULL AS c9_int3, "
+              "  NULL AS c10_big_int1, "
+              "  NULL AS c11_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
@@ -1002,10 +1052,12 @@
               "  NULL AS c3_string1, "
               "  NULL AS c4_string2, "
               "  NULL AS c5_string3, "
-               "  COUNT(*) AS c6_int1, "
-              "  NULL AS c7_int2, "
-              "  NULL AS c8_big_int1, "
-              "  NULL AS c9_big_int2 "
+              "  NULL AS c6_string4, "
+              "  COUNT(*) AS c7_int1, "
+              "  NULL AS c8_int2, "
+              "  NULL AS c9_int3, "
+              "  NULL AS c10_big_int1, "
+              "  NULL AS c11_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
@@ -1043,19 +1095,19 @@
           case QUERY_ATTACHMENTS:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
-            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)),
-                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2),
-                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
-                          s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3));
-            res.AddAttachment(file, 0 /* TODO - REVISIONS */);
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)),
+                          s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C8_INT_2)),
+                          s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3), s.ColumnString(C6_STRING_4));
+            res.AddAttachment(file, s.ColumnInt(C9_INT_3));
           }; break;
 
           case QUERY_MAIN_DICOM_TAGS:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddStringDicomTag(requestLevel, 
-                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
-                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C8_INT_2)),
                                   s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1063,8 +1115,8 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 1), 
-                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
-                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C8_INT_2)),
                                   s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1072,8 +1124,8 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 2), 
-                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
-                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C8_INT_2)),
                                   s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1081,7 +1133,7 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 1), 
-                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))),
+                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))),
                                              s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1089,7 +1141,7 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 2), 
-                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))),
+                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))),
                                              s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1097,31 +1149,31 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddMetadata(static_cast<ResourceType>(requestLevel), 
-                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
-                            s.ColumnString(C3_STRING_1), 0 /* no support for revision */);
+                            static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
+                            s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2));
           }; break;
 
           case QUERY_PARENT_METADATA:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddMetadata(static_cast<ResourceType>(requestLevel - 1), 
-                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
-                            s.ColumnString(C3_STRING_1), 0 /* no support for revision */);
+                            static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
+                            s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2));
           }; break;
 
           case QUERY_GRAND_PARENT_METADATA:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddMetadata(static_cast<ResourceType>(requestLevel - 2), 
-                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
-                            s.ColumnString(C3_STRING_1), 0 /* no support for revision */);
+                            static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
+                            s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2));
           }; break;
 
           case QUERY_CHILDREN_METADATA:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 1), 
-                                         static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                                         static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
                                          s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1129,7 +1181,7 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 2), 
-                                         static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                                         static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
                                          s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1170,21 +1222,21 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 1),
-                                 static_cast<uint64_t>(s.ColumnInt64(C6_INT_1)));
+                                 static_cast<uint64_t>(s.ColumnInt64(C7_INT_1)));
           }; break;
 
           case QUERY_GRAND_CHILDREN_COUNT:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 2),
-                                 static_cast<uint64_t>(s.ColumnInt64(C6_INT_1)));
+                                 static_cast<uint64_t>(s.ColumnInt64(C7_INT_1)));
           }; break;
 
           case QUERY_GRAND_GRAND_CHILDREN_COUNT:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 3),
-                                 static_cast<uint64_t>(s.ColumnInt64(C6_INT_1)));
+                                 static_cast<uint64_t>(s.ColumnInt64(C7_INT_1)));
           }; break;
 
           case QUERY_ONE_INSTANCE_IDENTIFIER:
@@ -1196,16 +1248,16 @@
           case QUERY_ONE_INSTANCE_METADATA:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
-            res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), s.ColumnString(C3_STRING_1));
+            res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1));
           }; break;
 
           case QUERY_ONE_INSTANCE_ATTACHMENTS:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
-            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)),
-                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2),
-                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
-                          s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3));
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)),
+                          s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C8_INT_2)),
+                          s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3), s.ColumnString(C6_STRING_4));
             res.AddOneInstanceAttachment(file);
           }; break;
 
@@ -1312,6 +1364,28 @@
       }
     }
 
+    void DeleteDeletedFile(const std::string& uuid)
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DeletedFiles WHERE uuid=?");
+      s.BindString(0, uuid);
+      s.Run();
+    }
+
+    void GetDeletedFileCustomData(std::string& customData, const std::string& uuid)
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT customData FROM DeletedFiles WHERE uuid=?");
+      s.BindString(0, uuid);
+    
+      if (s.Step())
+      { 
+        customData = s.ColumnString(0);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+    }
 
     virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
                                 int64_t id) ORTHANC_OVERRIDE
@@ -1687,7 +1761,7 @@
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE, 
                           "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
-                          "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
+                          "uncompressedMD5, compressedMD5, revision, customData FROM AttachedFiles WHERE id=? AND fileType=?");
       s.BindInt64(0, id);
       s.BindInt(1, contentType);
 
@@ -1703,12 +1777,50 @@
                               s.ColumnString(4),
                               static_cast<CompressionType>(s.ColumnInt(2)),
                               s.ColumnInt64(3),
-                              s.ColumnString(5));
-        revision = 0;   // TODO - REVISIONS
+                              s.ColumnString(5),
+                              s.ColumnString(7));
+        revision = s.ColumnInt(6);
         return true;
       }
     }
 
+    virtual bool GetAttachment(FileInfo& attachment,
+                               int64_t& revision,
+                               const std::string& attachmentUuid) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
+                          "uncompressedMD5, compressedMD5, revision, customData, fileType FROM AttachedFiles WHERE uuid=?");
+      s.BindString(0, attachmentUuid);
+
+      if (!s.Step())
+      {
+        return false;
+      }
+      else
+      {
+        attachment = FileInfo(s.ColumnString(0),
+                              static_cast<FileContentType>(s.ColumnInt(8)),
+                              s.ColumnInt64(1),
+                              s.ColumnString(4),
+                              static_cast<CompressionType>(s.ColumnInt(2)),
+                              s.ColumnInt64(3),
+                              s.ColumnString(5),
+                              s.ColumnString(7));
+        revision = s.ColumnInt(6);
+        return true;
+      }
+    }
+
+    virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid,
+                                            const std::string& customData) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "UPDATE AttachedFiles SET customData=? WHERE uuid=?");
+      s.BindString(0, customData);
+      s.BindString(1, attachmentUuid);
+      s.Run();
+    }
 
     virtual bool LookupGlobalProperty(std::string& target,
                                       GlobalProperty property,
@@ -1739,7 +1851,7 @@
                                 MetadataType type) ORTHANC_OVERRIDE
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                          "SELECT value FROM Metadata WHERE id=? AND type=?");
+                          "SELECT value, revision FROM Metadata WHERE id=? AND type=?");
       s.BindInt64(0, id);
       s.BindInt(1, type);
 
@@ -1750,7 +1862,7 @@
       else
       {
         target = s.ColumnString(0);
-        revision = 0;   // TODO - REVISIONS
+        revision = s.ColumnInt(1);
         return true;
       }
     }
@@ -1922,11 +2034,11 @@
                              const std::string& value,
                              int64_t revision) ORTHANC_OVERRIDE
     {
-      // TODO - REVISIONS
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata (id, type, value) VALUES(?, ?, ?)");
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata (id, type, value, revision) VALUES(?, ?, ?, ?)");
       s.BindInt64(0, id);
       s.BindInt(1, type);
       s.BindString(2, value);
+      s.BindInt(3, revision);
       s.Run();
     }
 
@@ -2027,6 +2139,128 @@
         target.insert(s.ColumnString(0));
       }
     }
+
+    virtual void StoreKeyValue(const std::string& storeId,
+                               const std::string& key,
+                               const std::string& value) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO KeyValueStores (storeId, key, value) VALUES(?, ?, ?)");
+      s.BindString(0, storeId);
+      s.BindString(1, key);
+      s.BindString(2, value);
+      s.Run();
+    }
+
+    virtual void DeleteKeyValue(const std::string& storeId,
+                                const std::string& key) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM KeyValueStores WHERE storeId = ? AND key = ?");
+      s.BindString(0, storeId);
+      s.BindString(1, key);
+      s.Run();
+    }
+
+    virtual bool GetKeyValue(std::string& value,
+                             const std::string& storeId,
+                             const std::string& key) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT value FROM KeyValueStores WHERE storeId=? AND key=?");
+      s.BindString(0, storeId);
+      s.BindString(1, key);
+
+      if (!s.Step())
+      {
+        // No value found
+        return false;
+      }
+      else
+      {
+        value = s.ColumnString(0);
+        return true;
+      }    
+    }
+
+    // New in Orthanc 1.12.99
+    virtual void ListKeys(std::list<std::string>& keys,
+                          const std::string& storeId,
+                          uint64_t since,
+                          uint64_t limit) ORTHANC_OVERRIDE
+    {
+      LookupFormatter formatter;
+
+      std::string sql = "SELECT key FROM KeyValueStores WHERE storeId=? ORDER BY key ASC " + formatter.FormatLimits(since, limit);
+      SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql);
+      s.BindString(0, storeId);
+
+      while (s.Step())
+      {
+        keys.push_back(s.ColumnString(0));
+      }
+    }
+
+
+    // New in Orthanc 1.12.99
+    virtual void EnqueueValue(const std::string& queueId,
+                              const std::string& value) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "INSERT INTO Queues (queueId, value) VALUES (?, ?)");
+      s.BindString(0, queueId);
+      s.BindString(1, value);
+      s.Run();
+    }
+
+    // New in Orthanc 1.12.99
+    virtual bool DequeueValue(std::string& value,
+                              const std::string& queueId,
+                              QueueOrigin origin) ORTHANC_OVERRIDE
+    {
+      int64_t rowId;
+      std::unique_ptr<SQLite::Statement> s;
+
+      switch (origin)
+      {
+        case QueueOrigin_Front:
+          s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id ASC LIMIT 1"));
+          break;
+        case QueueOrigin_Back:
+          s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id DESC LIMIT 1"));
+          break;
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      s->BindString(0, queueId);
+      if (!s->Step())
+      {
+        // No value found
+        return false;
+      }
+      else
+      {
+        rowId = s->ColumnInt64(0);
+        value = s->ColumnString(1);
+
+        SQLite::Statement s2(db_, SQLITE_FROM_HERE, 
+                            "DELETE FROM Queues WHERE id = ?");
+        s2.BindInt64(0, rowId);
+        s2.Run();
+
+        return true;
+      }    
+    }
+
+    // New in Orthanc 1.12.99
+    virtual void GetQueueSize(uint64_t& size,
+                              const std::string& queueId) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT COUNT(*) FROM Queues WHERE queueId=?");
+      s.BindString(0, queueId);
+      s.Step();
+      size = s.ColumnInt64(0);
+    }
+
   };
 
 
@@ -2055,6 +2289,11 @@
     {
       if (sqlite_.activeTransaction_ != NULL)
       {
+        std::string id = context.GetStringValue(0);
+
+        std::string customData;
+        sqlite_.activeTransaction_->GetDeletedFileCustomData(customData, id);
+
         std::string uncompressedMD5, compressedMD5;
 
         if (!context.IsNullValue(5))
@@ -2073,9 +2312,11 @@
                       uncompressedMD5,
                       static_cast<CompressionType>(context.GetIntValue(3)),
                       static_cast<uint64_t>(context.GetInt64Value(4)),
-                      compressedMD5);
+                      compressedMD5,
+                      customData);
 
         sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info);
+        sqlite_.activeTransaction_->DeleteDeletedFile(id);
       }
     }
   };
@@ -2214,11 +2455,13 @@
     signalRemainingAncestor_(NULL),
     version_(0)
   {
-    // TODO: implement revisions in SQLite
+    dbCapabilities_.SetRevisionsSupport(true);
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
     dbCapabilities_.SetHasExtendedChanges(true);
     dbCapabilities_.SetHasFindSupport(HasIntegratedFind());
+    dbCapabilities_.SetKeyValueStoresSupport(true);
+    dbCapabilities_.SetQueuesSupport(true);
     db_.Open(path);
   }
 
@@ -2228,11 +2471,13 @@
     signalRemainingAncestor_(NULL),
     version_(0)
   {
-    // TODO: implement revisions in SQLite
+    dbCapabilities_.SetRevisionsSupport(true);
     dbCapabilities_.SetFlushToDisk(true);
     dbCapabilities_.SetLabelsSupport(true);
     dbCapabilities_.SetHasExtendedChanges(true);
     dbCapabilities_.SetHasFindSupport(HasIntegratedFind());
+    dbCapabilities_.SetKeyValueStoresSupport(true);
+    dbCapabilities_.SetQueuesSupport(true);
     db_.OpenInMemory();
   }
 
@@ -2330,6 +2575,24 @@
           ServerResources::GetFileResource(query, ServerResources::INSTALL_LABELS_TABLE);
           db_.Execute(query);
         }
+
+        // New in Orthanc 1.12.99
+        if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasCustomDataAndRevision, true /* unused in SQLite */) 
+            || tmp != "1")
+        {
+          LOG(INFO) << "Upgrading SQLite schema to support revision and customData";
+          std::string query;
+          ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA);
+          db_.Execute(query);
+        }
+
+        if (!db_.DoesTableExist("KeyValueStores"))
+        {
+          LOG(INFO) << "Installing the \"KeyValueStores\" and \"Queues\" tables";
+          std::string query;
+          ServerResources::GetFileResource(query, ServerResources::INSTALL_KEY_VALUE_STORE_AND_QUEUES);
+          db_.Execute(query);
+        }
       }
 
       transaction->Commit(0);
@@ -2358,11 +2621,11 @@
 
 
   void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion,
-                                      IStorageArea& storageArea)
+                                      IPluginStorageArea& storageArea)
   {
     boost::mutex::scoped_lock lock(mutex_);
 
-    if (targetVersion != 6)
+    if (targetVersion != 7)
     {
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
     }
@@ -2372,7 +2635,8 @@
     if (version_ != 3 &&
         version_ != 4 &&
         version_ != 5 &&
-        version_ != 6)
+        version_ != 6 &&
+        version_ != 7)
     {
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
     }
@@ -2413,12 +2677,30 @@
       
       version_ = 6;
     }
+
   }
 
+  // class RaiiTransactionLogger
+  // {
+  //   TransactionType type_;
+  //   public:
+  //     RaiiTransactionLogger(TransactionType type)
+  //     : type_(type)
+  //     {
+  //       LOG(INFO) << "IN  " << (type_ == TransactionType_ReadOnly ? "RO" : "RW");
+  //     }
+  //     ~RaiiTransactionLogger()
+  //     {
+  //     LOG(INFO) << "OUT " << (type_ == TransactionType_ReadOnly ? "RO" : "RW");
+  //     }
+
+  // };
 
   IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction(TransactionType type,
                                                                           IDatabaseListener& listener)
   {
+    // RaiiTransactionLogger logger(type);
+
     switch (type)
     {
       case TransactionType_ReadOnly:
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Fri May 16 17:19:35 2025 +0200
@@ -88,7 +88,7 @@
     }
 
     virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea) ORTHANC_OVERRIDE;
+                         IPluginStorageArea& storageArea) ORTHANC_OVERRIDE;
 
     virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE
     {
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri May 16 17:19:35 2025 +0200
@@ -3200,6 +3200,18 @@
     return db_.GetDatabaseCapabilities().HasFindSupport();
   }
 
+  bool StatelessDatabaseOperations::HasKeyValueStoresSupport()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasKeyValueStoresSupport();
+  }
+
+  bool StatelessDatabaseOperations::HasQueuesSupport()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasQueuesSupport();
+  }
+
   void StatelessDatabaseOperations::ExecuteCount(uint64_t& count,
                                                  const FindRequest& request)
   {
@@ -3320,4 +3332,258 @@
       }
     }
   }
+
+  void StatelessDatabaseOperations::StoreKeyValue(const std::string& storeId,
+                                                  const std::string& key,
+                                                  const std::string& value)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& storeId_;
+      const std::string& key_;
+      const std::string& value_;
+
+    public:
+      Operations(const std::string& storeId,
+                 const std::string& key,
+                 const std::string& value) :
+        storeId_(storeId),
+        key_(key),
+        value_(value)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.StoreKeyValue(storeId_, key_, value_);
+      }
+    };
+
+    Operations operations(storeId, key, value);
+    Apply(operations);
+  }
+
+  void StatelessDatabaseOperations::DeleteKeyValue(const std::string& storeId,
+                                                   const std::string& key)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& storeId_;
+      const std::string& key_;
+
+    public:
+      Operations(const std::string& storeId,
+                 const std::string& key) :
+        storeId_(storeId),
+        key_(key)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.DeleteKeyValue(storeId_, key_);
+      }
+    };
+
+    Operations operations(storeId, key);
+    Apply(operations);
+  }
+
+  bool StatelessDatabaseOperations::GetKeyValue(std::string& value,
+                                                const std::string& storeId,
+                                                const std::string& key)
+  {
+    class Operations : public ReadOnlyOperationsT3<std::string&, const std::string&, const std::string& >
+    {
+      bool found_;
+    public:
+      Operations():
+        found_(false)
+      {}
+
+      bool HasFound()
+      {
+        return found_;
+      }
+
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        found_ = transaction.GetKeyValue(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, value, storeId, key);
+
+    return operations.HasFound();
+  }
+
+  void StatelessDatabaseOperations::ListKeys(std::list<std::string>& keys,
+                                             const std::string& storeId,
+                                             uint64_t since,
+                                             uint64_t limit)
+  {
+    class Operations : public ReadOnlyOperationsT4<std::list<std::string>&, const std::string&, uint64_t, uint64_t>
+    {
+    public:
+      Operations()
+      {}
+
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ListKeys(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, keys, storeId, since, limit);
+  }
+
+  void StatelessDatabaseOperations::EnqueueValue(const std::string& queueId,
+                                                 const std::string& value)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& queueId_;
+      const std::string& value_;
+
+    public:
+      Operations(const std::string& queueId,
+                 const std::string& value) :
+        queueId_(queueId),
+        value_(value)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.EnqueueValue(queueId_, value_);
+      }
+    };
+
+    Operations operations(queueId, value);
+    Apply(operations);
+  }
+
+  bool StatelessDatabaseOperations::DequeueValue(std::string& value,
+                                                 const std::string& queueId,
+                                                 QueueOrigin origin)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& queueId_;
+      std::string& value_;
+      QueueOrigin origin_;
+      bool found_;
+
+    public:
+      Operations(std::string& value,
+                 const std::string& queueId,
+                 QueueOrigin origin) :
+        queueId_(queueId),
+        value_(value),
+        origin_(origin),
+        found_(false)
+      {
+      }
+
+      bool HasFound()
+      {
+        return found_;
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        found_ = transaction.DequeueValue(value_, queueId_, origin_);
+      }
+    };
+
+    Operations operations(value, queueId, origin);
+    Apply(operations);
+
+    return operations.HasFound();
+  }
+
+  void StatelessDatabaseOperations::GetQueueSize(uint64_t& size,
+                                                 const std::string& queueId)
+  {
+    class Operations : public ReadOnlyOperationsT2<uint64_t&, const std::string& >
+    {
+    public:
+
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.GetQueueSize(tuple.get<0>(), tuple.get<1>());
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, size, queueId);
+  }
+
+
+  bool StatelessDatabaseOperations::GetAttachment(FileInfo& attachment,
+                                                  int64_t& revision,
+                                                  const std::string& attachmentUuid)
+  {
+    class Operations : public ReadOnlyOperationsT3<FileInfo&, int64_t&, const std::string& >
+    {
+      bool found_;
+    public:
+      Operations():
+        found_(false)
+      {}
+
+      bool HasFound()
+      {
+        return found_;
+      }
+
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        found_ = transaction.GetAttachment(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, attachment, revision, attachmentUuid);
+
+    return operations.HasFound();
+  }
+
+  void StatelessDatabaseOperations::UpdateAttachmentCustomData(const std::string& attachmentUuid,
+                                                               const std::string& customData)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& attachmentUuid_;
+      const std::string& customData_;
+
+    public:
+      Operations(const std::string& attachmentUuid,
+                 const std::string& customData) :
+        attachmentUuid_(attachmentUuid),
+        customData_(customData)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.UpdateAttachmentCustomData(attachmentUuid_, customData_);
+      }
+    };
+
+    Operations operations(attachmentUuid, customData);
+    Apply(operations);
+  }
+
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri May 16 17:19:35 2025 +0200
@@ -226,6 +226,13 @@
         return transaction_.LookupAttachment(attachment, revision, id, contentType);
       }
       
+      bool GetAttachment(FileInfo& attachment,
+                         int64_t& revision,
+                         const std::string& attachmentUuid)
+      {
+        return transaction_.GetAttachment(attachment, revision, attachmentUuid);
+      }
+
       bool LookupGlobalProperty(std::string& target,
                                 GlobalProperty property,
                                 bool shared)
@@ -293,6 +300,27 @@
       {
         transaction_.ExecuteExpand(response, capabilities, request, identifier);
       }
+
+      bool GetKeyValue(std::string& value,
+                       const std::string& storeId,
+                       const std::string& key)
+      {
+        return transaction_.GetKeyValue(value, storeId, key);
+      }
+
+      void GetQueueSize(uint64_t& size,
+                        const std::string& queueId)
+      {
+        return transaction_.GetQueueSize(size, queueId);
+      }
+
+      void ListKeys(std::list<std::string>& keys,
+                    const std::string& storeId,
+                    uint64_t since,
+                    uint64_t limit)
+      {
+        return transaction_.ListKeys(keys, storeId, since, limit);
+      }
     };
 
 
@@ -428,6 +456,39 @@
       {
         transaction_.RemoveLabel(id, label);
       }
+
+      void StoreKeyValue(const std::string& storeId,
+                         const std::string& key,
+                         const std::string& value)
+      {
+        transaction_.StoreKeyValue(storeId, key, value);
+      }
+
+      void DeleteKeyValue(const std::string& storeId,
+                          const std::string& key)
+      {
+        transaction_.DeleteKeyValue(storeId, key);
+      }
+
+      void EnqueueValue(const std::string& queueId,
+                        const std::string& value)
+      {
+        transaction_.EnqueueValue(queueId, value);
+      }
+
+      bool DequeueValue(std::string& value,
+                        const std::string& queueId,
+                        QueueOrigin origin)
+      {
+        return transaction_.DequeueValue(value, queueId, origin);
+      }
+
+      void UpdateAttachmentCustomData(const std::string& attachmentUuid,
+                                      const std::string& customData)
+      {
+        return transaction_.UpdateAttachmentCustomData(attachmentUuid, customData);
+      }
+
     };
 
 
@@ -523,6 +584,13 @@
                              /* out */ uint64_t& countSeries, 
                              /* out */ uint64_t& countInstances);
 
+    bool GetAttachment(FileInfo& attachment,
+                       int64_t& revision,
+                       const std::string& attachmentUuid);
+
+    void UpdateAttachmentCustomData(const std::string& attachmentUuid,
+                                    const std::string& customData);
+
     bool LookupAttachment(FileInfo& attachment,
                           int64_t& revision,
                           ResourceType level,
@@ -544,6 +612,10 @@
     bool HasExtendedChanges();
 
     bool HasFindSupport();
+
+    bool HasKeyValueStoresSupport();
+
+    bool HasQueuesSupport();
     
     void GetExportedResources(Json::Value& target,
                               int64_t since,
@@ -724,5 +796,31 @@
 
     void ExecuteCount(uint64_t& count,
                       const FindRequest& request);
+
+    void StoreKeyValue(const std::string& storeId,
+                       const std::string& key,
+                       const std::string& value);
+
+    void DeleteKeyValue(const std::string& storeId,
+                        const std::string& key);
+
+    bool GetKeyValue(std::string& value,
+                     const std::string& storeId,
+                     const std::string& key);
+
+    void ListKeys(std::list<std::string>& keys,
+                  const std::string& storeId,
+                  uint64_t since,
+                  uint64_t limit);
+
+    void EnqueueValue(const std::string& queueId,
+                      const std::string& value);
+
+    bool DequeueValue(std::string& value,
+                      const std::string& queueId,
+                      QueueOrigin origin);
+    
+    void GetQueueSize(uint64_t& size,
+                      const std::string& queueId);
   };
 }
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Fri May 16 17:19:35 2025 +0200
@@ -56,6 +56,7 @@
 
 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h"
+#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h"
 #include "../../OrthancFramework/Sources/HttpClient.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/OrthancException.h"
@@ -482,19 +483,6 @@
         }
       }
 
-      virtual IMemoryBuffer* Read(const std::string& uuid,
-                                  FileContentType type) ORTHANC_OVERRIDE
-      {
-        if (type != FileContentType_Dicom)
-        {
-          return storage_.Read(uuid, type);
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-      }
-
       virtual IMemoryBuffer* ReadRange(const std::string& uuid,
                                        FileContentType type,
                                        uint64_t start /* inclusive */,
@@ -510,9 +498,9 @@
         }
       }
 
-      virtual bool HasReadRange() const ORTHANC_OVERRIDE
+      virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
       {
-        return storage_.HasReadRange();
+        return storage_.HasEfficientReadRange();
       }
 
       virtual void Remove(const std::string& uuid,
@@ -527,7 +515,7 @@
   }
 
 
-  static IStorageArea* CreateFilesystemStorage()
+  static IPluginStorageArea* CreateFilesystemStorage()
   {
     static const char* const SYNC_STORAGE_AREA = "SyncStorageArea";
     static const char* const STORE_DICOM = "StoreDicom";
@@ -547,12 +535,12 @@
 
     if (lock.GetConfiguration().GetBooleanParameter(STORE_DICOM, true))
     {
-      return new FilesystemStorage(storageDirectory.string(), fsyncOnWrite);
+      return new PluginStorageAreaAdapter(new FilesystemStorage(storageDirectory.string(), fsyncOnWrite));
     }
     else
     {
       LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode";
-      return new FilesystemStorageWithoutDicom(storageDirectory.string(), fsyncOnWrite);
+      return new PluginStorageAreaAdapter(new FilesystemStorageWithoutDicom(storageDirectory.string(), fsyncOnWrite));
     }
   }
 
@@ -563,7 +551,7 @@
   }
 
 
-  IStorageArea* CreateStorageArea()
+  IPluginStorageArea* CreateStorageArea()
   {
     return CreateFilesystemStorage();
   }
--- a/OrthancServer/Sources/OrthancInitialization.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.h	Fri May 16 17:19:35 2025 +0200
@@ -35,7 +35,7 @@
 
   IDatabaseWrapper* CreateDatabaseWrapper();
 
-  IStorageArea* CreateStorageArea();
+  IPluginStorageArea* CreateStorageArea();
 
   void SetGlobalVerbosity(Verbosity verbosity);
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri May 16 17:19:35 2025 +0200
@@ -2666,7 +2666,7 @@
       }
 
       int64_t newRevision;
-      context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(),
+      context.AddAttachment(newRevision, publicId, level, StringToContentType(name), call.GetBodyData(),
                             call.GetBodySize(), hasOldRevision, oldRevision, oldMD5);
 
       SetBufferContentETag(call.GetOutput(), newRevision, call.GetBodyData(), call.GetBodySize());  // New in Orthanc 1.9.2
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Fri May 16 17:19:35 2025 +0200
@@ -95,6 +95,8 @@
     static const char* const HAS_LABELS = "HasLabels";
     static const char* const CAPABILITIES = "Capabilities";
     static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges";
+    static const char* const HAS_KEY_VALUE_STORES = "HasKeyValueStores";
+    static const char* const HAS_QUEUES = "HasQueues";
     static const char* const HAS_EXTENDED_FIND = "HasExtendedFind";
     static const char* const READ_ONLY = "ReadOnly";
 
@@ -211,6 +213,8 @@
     result[CAPABILITIES] = Json::objectValue;
     result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges();
     result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport();
+    result[CAPABILITIES][HAS_KEY_VALUE_STORES] = OrthancRestApi::GetIndex(call).HasKeyValueStoresSupport();
+    result[CAPABILITIES][HAS_QUEUES] = OrthancRestApi::GetIndex(call).HasQueuesSupport();
     
     call.GetOutput().AnswerJson(result);
   }
--- a/OrthancServer/Sources/ServerContext.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Fri May 16 17:19:35 2025 +0200
@@ -356,7 +356,7 @@
 
 
   ServerContext::ServerContext(IDatabaseWrapper& database,
-                               IStorageArea& area,
+                               IPluginStorageArea& area,
                                bool unitTesting,
                                size_t maxCompletedJobs,
                                bool readOnly,
@@ -593,10 +593,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);
   }
 
 
@@ -605,6 +606,37 @@
                                                                   StoreInstanceMode mode,
                                                                   bool isReconstruct)
   {
+    FileInfo adoptedFileNotUsed;
+
+    return StoreAfterTranscoding(resultPublicId,
+                                 dicom,
+                                 mode,
+                                 isReconstruct,
+                                 false,
+                                 adoptedFileNotUsed);
+  }
+
+  ServerContext::StoreResult ServerContext::AdoptAttachment(std::string& resultPublicId,
+                                                            DicomInstanceToStore& dicom,
+                                                            StoreInstanceMode mode,
+                                                            const FileInfo& adoptedFile)
+  {
+    return StoreAfterTranscoding(resultPublicId,
+                                 dicom,
+                                 mode,
+                                 false,
+                                 true,
+                                 adoptedFile);
+  }
+
+
+  ServerContext::StoreResult ServerContext::StoreAfterTranscoding(std::string& resultPublicId,
+                                                                  DicomInstanceToStore& dicom,
+                                                                  StoreInstanceMode mode,
+                                                                  bool isReconstruct,
+                                                                  bool isAdoption,
+                                                                  const FileInfo& adoptedFile)
+  {
     bool overwrite;
     switch (mode)
     {
@@ -707,19 +739,25 @@
       // 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_);
+      ServerIndex::Attachments attachments;
+      FileInfo dicomInfo;
 
-      ServerIndex::Attachments attachments;
-      attachments.push_back(dicomInfo);
+      if (!isAdoption)
+      {
+        dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom, compression, storeMD5_, &dicom);
+        attachments.push_back(dicomInfo);
+      }
+      else
+      {
+        attachments.push_back(adoptedFile);
+      }
 
       FileInfo dicomUntilPixelData;
       if (hasPixelDataOffset &&
-          (!area_.HasReadRange() ||
+          (!area_.HasEfficientReadRange() ||
            compressionEnabled_))
       {
-        dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, 
-                                             FileContentType_DicomUntilPixelData, compression, storeMD5_);
+        dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL);
         attachments.push_back(dicomUntilPixelData);
       }
 
@@ -764,7 +802,10 @@
             
       if (result.GetStatus() != StoreStatus_Success)
       {
-        accessor.Remove(dicomInfo);
+        if (!isAdoption)
+        {
+          accessor.Remove(dicomInfo);
+        }
 
         if (dicomUntilPixelData.IsValid())
         {
@@ -778,7 +819,14 @@
         switch (result.GetStatus())
         {
           case StoreStatus_Success:
-            LOG(INFO) << "New instance stored (" << resultPublicId << ")";
+            if (isAdoption)
+            {
+              LOG(INFO) << "New instance adopted (" << resultPublicId << ")";
+            }
+            else
+            {
+              LOG(INFO) << "New instance stored (" << resultPublicId << ")";
+            }
             break;
 
           case StoreStatus_AlreadyStored:
@@ -1018,8 +1066,7 @@
     StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     accessor.Read(content, attachment);
 
-    FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(),
-                                       content.size(), attachmentType, compression, storeMD5_);
+    FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(), content.size(), attachmentType, compression, storeMD5_, NULL);
 
     try
     {
@@ -1201,7 +1248,7 @@
 
 
       if (hasPixelDataOffset &&
-          area_.HasReadRange() &&
+          area_.HasEfficientReadRange() &&
           LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments) &&
           attachment.GetCompressionType() == CompressionType_None)
       {
@@ -1279,13 +1326,13 @@
             index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset,
                                      boost::lexical_cast<std::string>(pixelDataOffset));
 
-            if (!area_.HasReadRange() ||
+            if (!area_.HasEfficientReadRange() ||
                 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 */);
             }
           }
         }
@@ -1354,7 +1401,7 @@
       return true;
     }
 
-    if (!area_.HasReadRange())
+    if (!area_.HasEfficientReadRange())
     {
       return false;
     }
@@ -1513,6 +1560,7 @@
 
   bool ServerContext::AddAttachment(int64_t& newRevision,
                                     const std::string& resourceId,
+                                    ResourceType resourceType,
                                     FileContentType attachmentType,
                                     const void* data,
                                     size_t size,
@@ -1526,7 +1574,10 @@
     CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
     StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-    FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
+
+    assert(attachmentType != FileContentType_Dicom && attachmentType != FileContentType_DicomUntilPixelData); // this method can not be used to store instances
+
+    FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_, NULL);
 
     try
     {
--- a/OrthancServer/Sources/ServerContext.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Fri May 16 17:19:35 2025 +0200
@@ -43,7 +43,7 @@
 namespace Orthanc
 {
   class DicomInstanceToStore;
-  class IStorageArea;
+  class IPluginStorageArea;
   class JobsEngine;
   class MetricsRegistry;
   class OrthancPlugins;
@@ -193,7 +193,7 @@
     virtual void SignalJobFailure(const std::string& jobId) ORTHANC_OVERRIDE;
 
     ServerIndex index_;
-    IStorageArea& area_;
+    IPluginStorageArea& area_;
     StorageCache storageCache_;
 
     bool compressionEnabled_;
@@ -269,9 +269,17 @@
                                       StoreInstanceMode mode,
                                       bool isReconstruct);
 
+    StoreResult StoreAfterTranscoding(std::string& resultPublicId,
+                                      DicomInstanceToStore& dicom,
+                                      StoreInstanceMode mode,
+                                      bool isReconstruct,
+                                      bool isAdoption,
+                                      const FileInfo& adoptedFile);
+
     // 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
@@ -305,7 +313,7 @@
     };
 
     ServerContext(IDatabaseWrapper& database,
-                  IStorageArea& area,
+                  IPluginStorageArea& area,
                   bool unitTesting,
                   size_t maxCompletedJobs,
                   bool readOnly,
@@ -344,6 +352,7 @@
 
     bool AddAttachment(int64_t& newRevision,
                        const std::string& resourceId,
+                       ResourceType resourceType,
                        FileContentType attachmentType,
                        const void* data,
                        size_t size,
@@ -355,6 +364,11 @@
                       DicomInstanceToStore& dicom,
                       StoreInstanceMode mode);
 
+    StoreResult AdoptAttachment(std::string& resultPublicId,
+                                DicomInstanceToStore& dicom,
+                                StoreInstanceMode mode,
+                                const FileInfo& adoptedFile);
+
     StoreResult TranscodeAndStore(std::string& resultPublicId,
                                   DicomInstanceToStore* dicom,
                                   StoreInstanceMode mode,
--- a/OrthancServer/Sources/ServerEnumerations.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Fri May 16 17:19:35 2025 +0200
@@ -171,6 +171,7 @@
     GlobalProperty_AnonymizationSequence = 3,
     GlobalProperty_JobsRegistry = 5,
     GlobalProperty_GetTotalSizeIsFast = 6,      // New in Orthanc 1.5.2
+    GlobalProperty_SQLiteHasCustomDataAndRevision = 7,     // New in Orthanc 1.12.99
     GlobalProperty_Modalities = 20,             // New in Orthanc 1.5.0
     GlobalProperty_Peers = 21,                  // New in Orthanc 1.5.0
 
@@ -258,6 +259,11 @@
     Warnings_007_MissingRequestedTagsNotReadFromDisk       // new in Orthanc 1.12.5
   };
 
+  enum QueueOrigin
+  {
+    QueueOrigin_Front,
+    QueueOrigin_Back
+  };
 
   void InitializeServerEnumerations();
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Fri May 16 17:19:35 2025 +0200
@@ -45,12 +45,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())
       {
       }
@@ -60,6 +62,11 @@
         return uuid_;
       }
 
+      const std::string& GetCustomData() const
+      {
+        return customData_;
+      }
+
       FileContentType GetContentType() const 
       {
         return type_;
@@ -93,7 +100,7 @@
       {
         try
         {
-          context_.RemoveFile(it->GetUuid(), it->GetContentType());
+          context_.RemoveFile(it->GetUuid(), it->GetContentType(), it->GetCustomData());
         }
         catch (OrthancException& e)
         {
--- a/OrthancServer/Sources/ServerToolbox.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Fri May 16 17:19:35 2025 +0200
@@ -96,7 +96,7 @@
 
 
     void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction,
-                                  IStorageArea& storageArea,
+                                  IPluginStorageArea& storageArea,
                                   ResourceType level)
     {
       // WARNING: The database should be locked with a transaction!
--- a/OrthancServer/Sources/ServerToolbox.h	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/ServerToolbox.h	Fri May 16 17:19:35 2025 +0200
@@ -32,7 +32,7 @@
 namespace Orthanc
 {
   class ServerContext;
-  class IStorageArea;
+  class IPluginStorageArea;
 
   namespace ServerToolbox
   {
@@ -42,7 +42,7 @@
                               ResourceType type);
 
     void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction,
-                                  IStorageArea& storageArea,
+                                  IPluginStorageArea& storageArea,
                                   ResourceType level);
 
     void LoadIdentifiers(const DicomTag*& tags,
--- a/OrthancServer/Sources/main.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/Sources/main.cpp	Fri May 16 17:19:35 2025 +0200
@@ -30,6 +30,7 @@
 #include "../../OrthancFramework/Sources/DicomNetworking/DicomServer.h"
 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
+#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h"
 #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h"
 #include "../../OrthancFramework/Sources/HttpServer/HttpServer.h"
 #include "../../OrthancFramework/Sources/Logging.h"
@@ -1426,7 +1427,7 @@
 
 
 static void UpgradeDatabase(IDatabaseWrapper& database,
-                            IStorageArea& storageArea)
+                            IPluginStorageArea& storageArea)
 {
   // Upgrade the schema of the database, if needed
   unsigned int currentVersion = database.GetDatabaseVersion();
@@ -1529,7 +1530,7 @@
 
 
 static bool ConfigureServerContext(IDatabaseWrapper& database,
-                                   IStorageArea& storageArea,
+                                   IPluginStorageArea& storageArea,
                                    OrthancPlugins *plugins,
                                    bool loadJobsFromDatabase)
 {
@@ -1667,7 +1668,7 @@
 
 
 static bool ConfigureDatabase(IDatabaseWrapper& database,
-                              IStorageArea& storageArea,
+                              IPluginStorageArea& storageArea,
                               OrthancPlugins *plugins,
                               bool upgradeDatabase,
                               bool loadJobsFromDatabase)
@@ -1746,7 +1747,7 @@
                              bool loadJobsFromDatabase)
 {
   std::unique_ptr<IDatabaseWrapper>  databasePtr;
-  std::unique_ptr<IStorageArea>  storage;
+  std::unique_ptr<IPluginStorageArea>  storage;
 
 #if ORTHANC_ENABLE_PLUGINS == 1
   std::string databaseServerIdentifier;
@@ -1997,7 +1998,7 @@
         {
           SQLiteDatabaseWrapper inMemoryDatabase;
           inMemoryDatabase.Open();
-          MemoryStorageArea inMemoryStorage;
+          PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea);
           ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
           restApi.GenerateOpenApiDocumentation(openapi);
@@ -2048,7 +2049,7 @@
         {
           SQLiteDatabaseWrapper inMemoryDatabase;
           inMemoryDatabase.Open();
-          MemoryStorageArea inMemoryStorage;
+          PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea);
           ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
           restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html");
--- a/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Fri May 16 17:19:35 2025 +0200
@@ -26,9 +26,8 @@
 
 #include "../../OrthancFramework/Sources/Compatibility.h"
 #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
+#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h"
 #include "../../OrthancFramework/Sources/Logging.h"
-#include "../../OrthancFramework/Sources/SerializationToolbox.h"
 
 #include "../Sources/Database/SQLiteDatabaseWrapper.h"
 #include "../Sources/ServerContext.h"
@@ -39,7 +38,7 @@
 {
   const std::string path = "UnitTestsStorage";
 
-  MemoryStorageArea storage;
+  PluginStorageAreaAdapter storage(new MemoryStorageArea);
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
   ServerContext context(db, storage, true /* running unit tests */, 10, false, 1);
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Fri May 16 17:19:35 2025 +0200
@@ -27,6 +27,7 @@
 #include "../../OrthancFramework/Sources/Compatibility.h"
 #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h"
 #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
+#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h"
 #include "../../OrthancFramework/Sources/Images/Image.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 
@@ -296,11 +297,10 @@
   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);
   
   transaction_->GetAllMetadata(md, a[4]);
@@ -339,17 +339,17 @@
 
   int64_t revision;
   ASSERT_TRUE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_RemoteAet));
-  ASSERT_EQ(0, revision);   // "0" instead of "42" because of (*)
+  ASSERT_EQ(42, revision);
   ASSERT_FALSE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_Instance_IndexInSeries));
-  ASSERT_EQ(0, revision);
+  ASSERT_EQ(42, revision);
   ASSERT_EQ("PINNACLE", s);
 
   std::string u;
   ASSERT_TRUE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_RemoteAet));
-  ASSERT_EQ(0, revision);
+  ASSERT_EQ(42, revision);
   ASSERT_EQ("PINNACLE", u);
   ASSERT_FALSE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_Instance_IndexInSeries));
-  ASSERT_EQ(0, revision);
+  ASSERT_EQ(42, revision);
 
   ASSERT_TRUE(transaction_->LookupGlobalProperty(s, GlobalProperty_FlushSleep, true));
   ASSERT_FALSE(transaction_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42), true));
@@ -357,7 +357,7 @@
 
   FileInfo att;
   ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson));
-  ASSERT_EQ(0, revision);  // "0" instead of "42" because of (*)
+  ASSERT_EQ(42, revision);
   ASSERT_EQ("my json file", att.GetUuid());
   ASSERT_EQ(21u, att.GetCompressedSize());
   ASSERT_EQ("md5", att.GetUncompressedMD5());
@@ -366,7 +366,7 @@
   ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType());
 
   ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom));
-  ASSERT_EQ(0, revision);  // "0" instead of "42" because of (*)
+  ASSERT_EQ(44, revision);
   ASSERT_EQ("world", att.GetUuid());
   ASSERT_EQ(44u, att.GetCompressedSize());
   ASSERT_EQ("md5", att.GetUncompressedMD5());
@@ -402,7 +402,7 @@
 
   CheckTableRecordCount(0, "Resources");
   CheckTableRecordCount(0, "AttachedFiles");
-  CheckTableRecordCount(3, "GlobalProperties");
+  CheckTableRecordCount(4, "GlobalProperties");
 
   std::string tmp;
   ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true));
@@ -478,7 +478,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]));
   }
 
@@ -539,7 +539,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]));
   }
 
@@ -618,7 +618,7 @@
   const std::string path = "UnitTestsStorage";
 
   SystemToolbox::RemoveFile(path + "/index");
-  FilesystemStorage storage(path);
+  PluginStorageAreaAdapter storage(new FilesystemStorage(path));
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
   ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
@@ -700,7 +700,7 @@
   const std::string path = "UnitTestsStorage";
 
   SystemToolbox::RemoveFile(path + "/index");
-  FilesystemStorage storage(path);
+  PluginStorageAreaAdapter storage(new FilesystemStorage(path));
   SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
   ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
@@ -782,7 +782,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);
@@ -817,7 +817,7 @@
   {
     bool overwrite = (i == 0);
 
-    MemoryStorageArea storage;
+    PluginStorageAreaAdapter storage(new MemoryStorageArea);
     SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
     ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
@@ -982,7 +982,7 @@
   {
     const bool compression = (i == 0);
     
-    MemoryStorageArea storage;
+    PluginStorageAreaAdapter storage(new MemoryStorageArea);
     SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
     ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */);
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Fri May 16 17:15:54 2025 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Fri May 16 17:19:35 2025 +0200
@@ -26,6 +26,7 @@
 
 #include "../../OrthancFramework/Sources/Compatibility.h"
 #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
+#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h"
 #include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/SerializationToolbox.h"
@@ -528,12 +529,13 @@
   class OrthancJobsSerialization : public testing::Test
   {
   private:
-    MemoryStorageArea              storage_;
-    SQLiteDatabaseWrapper          db_;   // The SQLite DB is in memory
-    std::unique_ptr<ServerContext>   context_;
+    PluginStorageAreaAdapter        storage_;
+    SQLiteDatabaseWrapper           db_;   // The SQLite DB is in memory
+    std::unique_ptr<ServerContext>  context_;
 
   public:
-    OrthancJobsSerialization()
+    OrthancJobsSerialization() :
+      storage_(new MemoryStorageArea)
     {
       db_.Open();
       context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */));
--- a/TODO	Fri May 16 17:15:54 2025 +0200
+++ b/TODO	Fri May 16 17:19:35 2025 +0200
@@ -1,11 +1,3 @@
-current work on C-Get SCU:
-- for the negotiation, limit SOPClassUID to the ones listed in a C-Find response or to a list provided in the Rest API ?
-- SetupPresentationContexts
-- handle progress
-- handle cancellation when the job is cancelled ?
-
-
-
 =======================
 === Orthanc Roadmap ===
 =======================