changeset 6214:e64c3ae969e4 sql-opti

merged default -> sql-opti
author Alain Mazy <am@orthanc.team>
date Fri, 27 Jun 2025 15:00:33 +0200
parents 0290cf80dd93 (current diff) 3a974dbf4740 (diff)
children 02c3f861b6e6
files OrthancServer/Sources/ServerEnumerations.h
diffstat 89 files changed, 6587 insertions(+), 1308 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Fri Jun 27 14:59:41 2025 +0200
+++ b/.hgignore	Fri Jun 27 15:00:33 2025 +0200
@@ -15,3 +15,4 @@
 .project
 Resources/Testing/Issue32/Java/bin
 Resources/Testing/Issue32/Java/target
+build/
--- a/CITATION.cff	Fri Jun 27 14:59:41 2025 +0200
+++ b/CITATION.cff	Fri Jun 27 15:00:33 2025 +0200
@@ -10,5 +10,5 @@
 doi: "10.1007/s10278-018-0082-y"
 license: "GPL-3.0-or-later"
 repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
-version: 1.12.7
-date-released: 2025-04-07
+version: 1.12.8
+date-released: 2025-06-13
--- a/NEWS	Fri Jun 27 14:59:41 2025 +0200
+++ b/NEWS	Fri Jun 27 15:00:33 2025 +0200
@@ -1,34 +1,87 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* Lua: new "SetStableStatus" function.
+
+
+Plugin SDK
+----------
+
+* Added new function OrthancPluginSetStableStatus to e.g force the 
+  stabilization of a study from a plugin.
+
+
+Plugins
+-------
+
+* Housekeeper plugin:
+  - new "ForceReconstructFiles": If "Force" is set to true, forces 
+    the "ReconstructFiles" option when reconstructing resources even 
+    if the plugin did not detect any changes in the configuration that 
+    should trigger a Reconstruct.
+
+
+
+Version 1.12.8 (2025-06-13)
+===========================
+
+General
+-------
+
+* The default SQLite database engine now supports metadata and attachment revisions.
+
+REST API
+--------
+
+* API version upgraded to 29
+* If the database backend provides the "HasExtendedFind" primitive, the
+  value "IsProtected" can be included in the "ResponseContent" field of
+  "/tools/find" to request the "IsProtected" status of patient resources.
+
+Plugin SDK
+----------
+
+* Added new functions (available to all plugins) to access key-value
+  stores and queues stored as a part of the Orthanc database.
+* New SDK to create storage area plugins (V3) that associate custom data with
+  attachments. The built-in SQLite database engine supports such custom data.
+* New SDK to handle custom data for attachments, key-value stores, and queues
+  by custom database backends (cf. "OrthancDatabasePlugin.proto").
+* Added OrthancPluginAdoptDicomInstance() to adopt DICOM instances stored elsewhere
+  than in the storage area (to be used by "orthanc-advanced-storage" plugin).
+
+Plugins
+-------
+
+* New sample plugins: "CppSkeleton" and "AdoptDicomInstance"
+* Housekeeper plugin:
+  - If "LimitMainDicomTagsReconstructLevel" was set, files were not transcoded
+    if they had to. The "LimitMainDicomTagsReconstructLevel" configuration is now
+    ignored when a full processing is required.
+* Delayed Deletion plugin:
+  - Added an index in the delayed-deletion SQLite external DB to speed up delayed
+    deletions. This new index will only apply to new databases. If you want to speed
+    up an existing installation, run "CREATE INDEX PendingIndex ON Pending(uuid)"
+    manually in the plugin SQLite DB. With this patch, we observed a 100 fold
+    performance improvement when the "Pending" table contains 1-2 millions files.
+    Contribution by Yurii (George) from ivtech.dev.
+
 Maintenance
 -----------
 
-* In verbose logs, added the elapsed time spent in each HTTP call.
-* Housekeeper plugin: 
-  - If "LimitMainDicomTagsReconstructLevel" was set, files were not transcoded if they had to.
-    The "LimitMainDicomTagsReconstructLevel" configuration is now ignored when a full processing
-    is required.
-* Delayed Deletion plugin:
-  - Added an index in the delayed-deletion SQLite external DB to speed up delayed deletions.
-    This new index will only apply to new databases.  If you wish to speed up an existing installation,
-    run "CREATE INDEX PendingIndex ON Pending(uuid)" manually in the plugin SQLite DB.
-    Patch provided by Yurii (George) from ivtech.dev.
-    With this patch, we observed a 100 fold performance improvement when the 
-    "Pending" table contains 1-2 millions files.
-      
+* In verbose logs, the elapsed time spent in each HTTP call is now reported.
+* Fix computation of MD5 hashes for memory buffers whose size is larger than 2^31 bytes.
 * Configuration options "RejectSopClasses" and "RejectedSopClasses" are taken as synonyms.
   In Orthanc 1.12.6 and 1.12.7, "RejectSopClasses" was used instead of the expected
   "RejectedSopClasses" spelling.
-
-
-REST API
---------
-
-* If the index database provides the "HasExtendedFind" primitive, the "ResponseContent" option in
-  "/tools/find" now allows to specify "IsProtected" to retrieve the "IsProtected" status of a
-  patient resource.
-
+* Fix the re-encoding of DICOM files larger than 4GB.
+* Improved translations of HTTP error codes when a plugin calls the core REST API.
+  In particular, a plugin could receive an error OrthancPluginErrorCode_UnknownResource (code 17)
+  when the underlying REST handler was actually returning an HTTP error 415. The plugin will
+  now receive an error OrthancPluginErrorCode_UnsupportedMediaType (code 3000).
 
 
 Version 1.12.7 (2025-04-07)
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Fri Jun 27 15:00:33 2025 +0200
@@ -171,6 +171,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "0e971f32f4f3e4951e0f3b5de49a3da6")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.7")
         set(ORTHANC_FRAMEWORK_MD5 "f27c27d7a7a694dab1fd7f0a99d9715a")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.8")
+        set(ORTHANC_FRAMEWORK_MD5 "eb1c719234338e8277b80d3453563e9f")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
@@ -515,7 +517,6 @@
   include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
-  set(EMBED_RESOURCES_PYTHON ${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py)
 
   if (ORTHANC_FRAMEWORK_USE_SHARED)
     list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Jun 27 15:00:33 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/Resources/CMake/OrthancFrameworkParameters.cmake	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Fri Jun 27 15:00:33 2025 +0200
@@ -39,7 +39,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "28")
+set(ORTHANC_API_VERSION "29")
 
 
 #####################################################################
--- a/OrthancFramework/Sources/ChunkedBuffer.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/ChunkedBuffer.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -25,6 +25,8 @@
 #include "PrecompiledHeaders.h"
 #include "ChunkedBuffer.h"
 
+#include "OrthancException.h"
+
 #include <cassert>
 #include <string.h>
 
@@ -54,7 +56,16 @@
     else
     {
       assert(chunkData != NULL);
-      chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize));
+
+      try
+      {
+        chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize));
+      }
+      catch (...)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
       numBytes_ += chunkSize;
     }
   }
@@ -172,24 +183,59 @@
   void ChunkedBuffer::Flatten(std::string& result)
   {
     FlushPendingBuffer();
-    result.resize(numBytes_);
 
-    size_t pos = 0;
-    for (Chunks::iterator it = chunks_.begin(); 
-         it != chunks_.end(); ++it)
+    if (chunks_.empty())
     {
-      assert(*it != NULL);
-
-      size_t s = (*it)->size();
-      if (s != 0)
+      if (numBytes_ != 0)
       {
-        memcpy(&result[pos], (*it)->c_str(), s);
-        pos += s;
+        throw OrthancException(ErrorCode_InternalError);
       }
 
-      delete *it;
+      result.clear();
+    }
+    else if (chunks_.size() == 1)
+    {
+      // Avoid reallocating a buffer if there is a single chunk
+      assert(chunks_.front() != NULL);
+      if (chunks_.front()->size() != numBytes_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        chunks_.front()->swap(result);
+        delete chunks_.front();
+      }
+    }
+    else
+    {
+      try
+      {
+        result.resize(numBytes_);
+      }
+      catch (...)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      size_t pos = 0;
+      for (Chunks::iterator it = chunks_.begin();
+           it != chunks_.end(); ++it)
+      {
+        assert(*it != NULL);
+
+        size_t s = (*it)->size();
+        if (s != 0)
+        {
+          memcpy(&result[pos], (*it)->c_str(), s);
+          pos += s;
+        }
+
+        delete *it;
+      }
     }
 
+    // Reset the data structure
     chunks_.clear();
     numBytes_ = 0;
   }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -31,7 +31,6 @@
 
 #include "../Compatibility.h"
 #include "../Endianness.h"
-#include "../Logging.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
 #include "DicomArray.h"
@@ -44,6 +43,7 @@
 #if !defined(__EMSCRIPTEN__)
 // Multithreading is not supported in WebAssembly
 #  include <boost/thread/shared_mutex.hpp>
+#  include <boost/thread/lock_types.hpp>  // For boost::unique_lock<> and boost::shared_lock<>
 #endif
 
 namespace Orthanc
@@ -1220,7 +1220,7 @@
   }
 
 
-  void DicomMap::LogMissingTagsForStore() const
+  std::string DicomMap::FormatMissingTagsForStore() const
   {
     std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
     
@@ -1244,14 +1244,14 @@
       sopInstanceUid = ValueAsString(*this, DICOM_TAG_SOP_INSTANCE_UID);
     }
 
-    LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
+    return FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
   }
 
   
-  void DicomMap::LogMissingTagsForStore(const std::string& patientId,
-                                        const std::string& studyInstanceUid,
-                                        const std::string& seriesInstanceUid,
-                                        const std::string& sopInstanceUid)
+  std::string DicomMap::FormatMissingTagsForStore(const std::string& patientId,
+                                                  const std::string& studyInstanceUid,
+                                                  const std::string& seriesInstanceUid,
+                                                  const std::string& sopInstanceUid)
   {
     std::string s, t;
 
@@ -1309,11 +1309,11 @@
 
     if (t.size() == 0)
     {
-      LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)";
+      return "Store has failed because all the required tags (" + s + ") are missing (is it a DICOMDIR file?)";
     }
     else
     {
-      LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t;
+      return "Store has failed because required tags (" + s + ") are missing for the following instance: " + t;
     }
   }
 
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Fri Jun 27 15:00:33 2025 +0200
@@ -171,12 +171,12 @@
                                           const void* dicom,
                                           size_t size);
 
-    void LogMissingTagsForStore() const;
+    std::string FormatMissingTagsForStore() const;
 
-    static void LogMissingTagsForStore(const std::string& patientId,
-                                       const std::string& studyInstanceUid,
-                                       const std::string& seriesInstanceUid,
-                                       const std::string& sopInstanceUid);
+    static std::string FormatMissingTagsForStore(const std::string& patientId,
+                                                 const std::string& studyInstanceUid,
+                                                 const std::string& seriesInstanceUid,
+                                                 const std::string& sopInstanceUid);
 
     bool LookupStringValue(std::string& result,
                            const DicomTag& tag,
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -192,7 +192,7 @@
 
                   if (e.GetErrorCode() == ErrorCode_InexistentTag)
                   {
-                    FromDcmtkBridge::LogMissingTagsForStore(**imageDataSet);
+                    LOG(ERROR) << FromDcmtkBridge::FormatMissingTagsForStore(**imageDataSet);
                   }
                   else
                   {
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -38,6 +38,7 @@
 
 #include "FromDcmtkBridge.h"
 #include "ToDcmtkBridge.h"
+#include "../ChunkedBuffer.h"
 #include "../Compatibility.h"
 #include "../Logging.h"
 #include "../Toolbox.h"
@@ -57,7 +58,6 @@
 
 #include <dcmtk/dcmdata/dcdeftag.h>
 #include <dcmtk/dcmdata/dcdicent.h>
-#include <dcmtk/dcmdata/dcdict.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmdata/dcistrmb.h>
 #include <dcmtk/dcmdata/dcostrmb.h>
@@ -126,6 +126,38 @@
 
 namespace Orthanc
 {
+  FromDcmtkBridge::DictionaryWriterLock::DictionaryWriterLock() :
+    dictionary_(dcmDataDict.wrlock())
+  {
+  }
+
+
+  FromDcmtkBridge::DictionaryWriterLock::~DictionaryWriterLock()
+  {
+#if DCMTK_VERSION_NUMBER >= 364
+    dcmDataDict.wrunlock();
+#else
+    dcmDataDict.unlock();
+#endif
+  }
+
+
+  FromDcmtkBridge::DictionaryReaderLock::DictionaryReaderLock() :
+    dictionary_(dcmDataDict.rdlock())
+  {
+  }
+
+
+  FromDcmtkBridge::DictionaryReaderLock::~DictionaryReaderLock()
+  {
+#if DCMTK_VERSION_NUMBER >= 364
+    dcmDataDict.rdunlock();
+#else
+    dcmDataDict.unlock();
+#endif
+  }
+
+
   static bool IsBinaryTag(const DcmTag& key)
   {
     return (key.isUnknownVR() ||
@@ -167,37 +199,73 @@
 
   namespace
   {
-    class DictionaryLocker : public boost::noncopyable
+    class ChunkedBufferStream : public DcmOutputStream
     {
     private:
-      DcmDataDictionary& dictionary_;
+      class Consumer : public DcmConsumer
+      {
+      private:
+        ChunkedBuffer  buffer_;
+
+      public:
+        void Flatten(std::string& buffer)
+        {
+          buffer_.Flatten(buffer);
+        }
+
+        OFBool good() const ORTHANC_OVERRIDE
+        {
+          return true;
+        }
+
+        OFCondition status() const ORTHANC_OVERRIDE
+        {
+          return EC_Normal;
+        }
+
+        OFBool isFlushed() const ORTHANC_OVERRIDE
+        {
+          return true;
+        }
+
+        offile_off_t avail() const ORTHANC_OVERRIDE
+        {
+          // since we cannot report "unlimited", let's claim that we can still write 10MB.
+          // Note that offile_off_t is a signed type.
+          return 10 * 1024 * 1024;
+        }
+
+        offile_off_t write(const void *buf,
+                           offile_off_t buflen) ORTHANC_OVERRIDE
+        {
+          buffer_.AddChunk(buf, buflen);
+          return buflen;
+        }
+
+        void flush() ORTHANC_OVERRIDE
+        {
+          // Nothing to flush
+        }
+      };
+
+      Consumer consumer_;
 
     public:
-      DictionaryLocker() : dictionary_(dcmDataDict.wrlock())
+      ChunkedBufferStream() :
+        DcmOutputStream(&consumer_)
       {
       }
 
-      ~DictionaryLocker()
+      void Flatten(std::string& buffer)
       {
-#if DCMTK_VERSION_NUMBER >= 364
-        dcmDataDict.wrunlock();
-#else
-        dcmDataDict.unlock();
-#endif
-      }
-
-      DcmDataDictionary& operator*()
-      {
-        return dictionary_;
-      }
-
-      DcmDataDictionary* operator->()
-      {
-        return &dictionary_;
+        consumer_.Flatten(buffer);
       }
     };
-
-    
+  }
+
+
+  namespace
+  {
     ORTHANC_FORCE_INLINE
     static std::string FloatToString(float v)
     {
@@ -296,9 +364,9 @@
     
 #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
     {
-      DictionaryLocker locker;
-
-      locker->clear();
+      DictionaryWriterLock lock;
+
+      lock.GetDictionary().clear();
 
       CLOG(INFO, DICOM) << "Loading the embedded dictionaries";
       /**
@@ -306,14 +374,14 @@
        * command "strace storescu 2>&1 |grep dic" shows that DICONDE
        * dictionary is not loaded by storescu.
        **/
-      //LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_DICONDE);
-
-      LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_DICOM);
+      //LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICONDE);
+
+      LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICOM);
 
       if (loadPrivateDictionary)
       {
         CLOG(INFO, DICOM) << "Loading the embedded dictionary of private tags";
-        LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_PRIVATE);
+        LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_PRIVATE);
       }
       else
       {
@@ -373,16 +441,16 @@
 
   void FromDcmtkBridge::LoadExternalDictionaries(const std::vector<std::string>& dictionaries)
   {
-    DictionaryLocker locker;
+    DictionaryWriterLock lock;
 
     CLOG(INFO, DICOM) << "Clearing the DICOM dictionary";
-    locker->clear();
+    lock.GetDictionary().clear();
 
     for (size_t i = 0; i < dictionaries.size(); i++)
     {
       LOG(WARNING) << "Loading external DICOM dictionary: \"" << dictionaries[i] << "\"";
         
-      if (!locker->loadDictionary(dictionaries[i].c_str()))
+      if (!lock.GetDictionary().loadDictionary(dictionaries[i].c_str()))
       {
         throw OrthancException(ErrorCode_InexistentFile);
       }
@@ -475,10 +543,10 @@
     entry->setElementRangeRestriction(DcmDictRange_Unspecified);
 
     {
-      DictionaryLocker locker;
-
-      if (locker->findEntry(DcmTagKey(tag.GetGroup(), tag.GetElement()),
-                            privateCreator.empty() ? NULL : privateCreator.c_str()))
+      DictionaryWriterLock lock;
+
+      if (lock.GetDictionary().findEntry(DcmTagKey(tag.GetGroup(), tag.GetElement()),
+                                         privateCreator.empty() ? NULL : privateCreator.c_str()))
       {
         throw OrthancException(ErrorCode_AlreadyExistingTag,
                                "Cannot register twice the tag (" + tag.Format() +
@@ -486,7 +554,7 @@
       }
       else
       {
-        locker->addEntry(entry.release());
+        lock.GetDictionary().addEntry(entry.release());
       }
     }
   }
@@ -663,10 +731,11 @@
        * syntax (cf. DICOM CP 246).
        * ftp://medical.nema.org/medical/dicom/final/cp246_ft.pdf
        **/
-      DictionaryLocker locker;
-      
-      const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(), 
-                                                    element.getTag().getPrivateCreator());
+      DictionaryReaderLock lock;
+
+      // The "entry" value is only valid while "lock" is active
+      const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(),
+                                                                 element.getTag().getPrivateCreator());
       if (entry != NULL && 
           entry->getVR().isaString())
       {
@@ -1111,8 +1180,8 @@
 
       if (!(flags & DicomToJsonFlags_IncludeUnknownTags))
       {
-        DictionaryLocker locker;
-        if (locker->findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL)
+        DictionaryReaderLock lock;
+        if (lock.GetDictionary().findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL)
         {
           continue;
         }
@@ -1572,7 +1641,12 @@
   }
 
 
-  
+#if 0
+  /**
+   * This was the implementation in Orthanc <= 1.12.7. This version
+   * uses "DcmFileFormat::calcElementLength()", which cannot handle
+   * DICOM files whose size cannot be represented on 32 bits.
+   **/
   static bool SaveToMemoryBufferInternal(std::string& buffer,
                                          DcmFileFormat& dicom,
                                          E_TransferSyntax xfer,
@@ -1619,6 +1693,46 @@
       return false;
     }
   }
+#endif
+
+
+#if 1
+  /**
+   * This is the cleaner implementation used in Orthanc >= 1.12.8,
+   * which allows to write DICOM files larger than 4GB.
+   **/
+  static bool SaveToMemoryBufferInternal(std::string& buffer,
+                                         DcmFileFormat& dicom,
+                                         E_TransferSyntax xfer,
+                                         std::string& errorMessage)
+  {
+    ChunkedBufferStream ob;
+
+    // Fill the (chunked) memory buffer with the meta-header and the dataset
+    dicom.transferInit();
+    OFCondition c = dicom.write(ob, xfer, /*opt_sequenceType*/ EET_ExplicitLength, NULL,
+                                /*opt_groupLength*/ EGL_recalcGL,
+                                /*opt_paddingType*/ EPD_noChange,
+                                /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0,
+                                EWM_updateMeta /* creates new SOP instance UID on lossy */);
+    dicom.transferEnd();
+
+    if (c.good())
+    {
+      ob.flush();
+      ob.Flatten(buffer);
+      return true;
+    }
+    else
+    {
+      // Error
+      buffer.clear();
+      errorMessage = std::string(c.text());
+      return false;
+    }
+  }
+#endif
+
 
   bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
                                            DcmDataset& dataSet)
@@ -2646,10 +2760,11 @@
     if (evr == EVR_UN)
     {
       // New in Orthanc 1.9.5
-      DictionaryLocker locker;
-      
-      const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(),
-                                                    element.getTag().getPrivateCreator());
+      FromDcmtkBridge::DictionaryReaderLock lock;
+
+      // The "entry" value is only valid while "lock" is active
+      const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(),
+                                                                 element.getTag().getPrivateCreator());
 
       if (entry != NULL)
       {
@@ -3162,7 +3277,7 @@
   }
 
 
-  void FromDcmtkBridge::LogMissingTagsForStore(DcmDataset& dicom)
+  std::string FromDcmtkBridge::FormatMissingTagsForStore(DcmDataset& dicom)
   {
     std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
 
@@ -3194,7 +3309,7 @@
       sopInstanceUid.assign(c);
     }
     
-    DicomMap::LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
+    return DicomMap::FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
   }
 
 
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Fri Jun 27 15:00:33 2025 +0200
@@ -30,9 +30,10 @@
 #include "../DicomFormat/DicomPath.h"
 
 #include <dcmtk/dcmdata/dcdatset.h>
+#include <dcmtk/dcmdata/dcdict.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmdata/dcmetinf.h>
 #include <dcmtk/dcmdata/dcpixseq.h>
-#include <dcmtk/dcmdata/dcfilefo.h>
 #include <json/value.h>
 
 #if ORTHANC_ENABLE_DCMTK != 1
@@ -86,6 +87,40 @@
     };
     
 
+    class ORTHANC_PUBLIC DictionaryWriterLock : public boost::noncopyable
+    {
+    private:
+      DcmDataDictionary& dictionary_;
+
+    public:
+      DictionaryWriterLock();
+
+      ~DictionaryWriterLock();
+
+      DcmDataDictionary& GetDictionary()
+      {
+        return dictionary_;
+      }
+    };
+
+
+    class ORTHANC_PUBLIC DictionaryReaderLock : public boost::noncopyable
+    {
+    private:
+      const DcmDataDictionary& dictionary_;
+
+    public:
+      DictionaryReaderLock();
+
+      ~DictionaryReaderLock();
+
+      const DcmDataDictionary& GetDictionary() const
+      {
+        return dictionary_;
+      }
+    };
+
+
   private:
     FromDcmtkBridge();  // Pure static class
 
@@ -280,7 +315,7 @@
     static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
                                             DcmDataset& dicom);
 
-    static void LogMissingTagsForStore(DcmDataset& dicom);
+    static std::string FormatMissingTagsForStore(DcmDataset& dicom);
 
     static void RemovePath(DcmDataset& dataset,
                            const DicomPath& path);
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -169,4 +169,56 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
   }
+
+  void FileInfo::SetCustomData(const void* data,
+                               size_t size)
+  {
+    if (valid_)
+    {
+      customData_.assign(reinterpret_cast<const char*>(data), size);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FileInfo::SetCustomData(const std::string& data)
+  {
+    if (valid_)
+    {
+      customData_ = data;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FileInfo::SwapCustomData(std::string& data)
+  {
+    if (valid_)
+    {
+      customData_.swap(data);
+    }
+    else
+    {
+      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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.h	Fri Jun 27 15:00:33 2025 +0200
@@ -42,6 +42,7 @@
     CompressionType  compressionType_;
     uint64_t         compressedSize_;
     std::string      compressedMD5_;
+    std::string      customData_;
 
   public:
     FileInfo();
@@ -80,5 +81,14 @@
     const std::string& GetCompressedMD5() const;
 
     const std::string& GetUncompressedMD5() const;
+
+    void SetCustomData(const void* data,
+                       size_t size);
+
+    void SetCustomData(const std::string& data);
+
+    void SwapCustomData(std::string& data);
+
+    const std::string& GetCustomData() const;
   };
 }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Fri Jun 27 15:00:33 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);
@@ -354,7 +348,7 @@
                                const std::string& uuid,
                                FileContentType type)
   {
-    std::unique_ptr<IMemoryBuffer> buffer(Read(uuid, type));
+    std::unique_ptr<IMemoryBuffer> buffer(ReadWhole(uuid, type));
     buffer->MoveToString(content);
   }
 #endif
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp	Fri Jun 27 15:00:33 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 */,
@@ -149,12 +124,6 @@
   }
 
 
-  bool MemoryStorageArea::HasReadRange() const
-  {
-    return true;
-  }
-
-
   void MemoryStorageArea::Remove(const std::string& uuid,
                                  FileContentType type)
   {
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Fri Jun 27 15:00:33 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 Jun 27 15:00:33 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 Jun 27 15:00:33 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 ORTHANC_PUBLIC 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Fri Jun 27 15:00:33 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),
@@ -310,13 +310,15 @@
   }
 
 
-  FileInfo StorageAccessor::Write(const void* data,
-                                  size_t size,
-                                  FileContentType type,
-                                  CompressionType compression,
-                                  bool storeMd5)
+  void StorageAccessor::Write(FileInfo& info,
+                              const void* data,
+                              size_t size,
+                              FileContentType type,
+                              CompressionType compression,
+                              bool storeMd5,
+                              const DicomInstanceToStore* instance)
   {
-    std::string uuid = Toolbox::GenerateUuid();
+    const std::string uuid = Toolbox::GenerateUuid();
 
     std::string md5;
 
@@ -325,13 +327,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 +349,9 @@
           cacheAccessor.Add(uuid, type, data, size);
         }
 
-        return FileInfo(uuid, type, size, md5);
+        info = FileInfo(uuid, type, size, md5);
+        info.SetCustomData(customData);
+        return;
       }
 
       case CompressionType_ZlibWithSize:
@@ -367,11 +373,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);
           }
         }
 
@@ -386,8 +392,10 @@
           cacheAccessor.Add(uuid, type, data, size);    // always add uncompressed data to cache
         }
 
-        return FileInfo(uuid, type, size, md5,
+        info = FileInfo(uuid, type, size, md5,
                         CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
+        info.SetCustomData(customData);
+        return;
       }
 
       default:
@@ -395,16 +403,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 +444,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 +465,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 +524,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 +537,8 @@
 
 
   void StorageAccessor::Remove(const std::string& fileUuid,
-                               FileContentType type)
+                               FileContentType type,
+                               const std::string& customData)
   {
     if (cache_ != NULL)
     {
@@ -548,14 +547,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 +615,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 +681,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 +784,5 @@
     output.AnswerStream(transcoder);
   }
 #endif
+
 }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Fri Jun 27 15:00:33 2025 +0200
@@ -110,7 +110,7 @@
   private:
     class MetricsTimer;
 
-    IStorageArea&     area_;
+    IPluginStorageArea&     area_;
     StorageCache*     cache_;
     MetricsRegistry*  metrics_;
 
@@ -121,28 +121,25 @@
 #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);
 
-    FileInfo Write(const void* data,
-                   size_t size,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
-
-    FileInfo Write(const std::string& data,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
+    void Write(FileInfo& info /* out */,
+               const void* data,
+               size_t size,
+               FileContentType type,
+               CompressionType compression,
+               bool storeMd5,
+               const DicomInstanceToStore* instance);
 
     void Read(std::string& content,
               const FileInfo& info);
@@ -155,7 +152,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 +183,7 @@
                     const std::string& mime,
                     const std::string& contentFilename);
 #endif
+
   private:
     void ReadStartRangeInternal(std::string& target,
                                 const FileInfo& info,
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -51,7 +51,10 @@
     if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, 
                        httpHeaders, getArguments, NULL /* no body for GET */, 0))
     {
-      stream.GetBody(answerBody);
+      if (stream.GetStatus() == HttpStatus_200_Ok)
+      {
+        stream.GetBody(answerBody);
+      }
 
       if (answerHeaders != NULL)
       {
--- a/OrthancFramework/Sources/SQLite/Statement.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/SQLite/Statement.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -236,10 +236,22 @@
       BindString(col, UTF16ToUTF8(value));
       }*/
 
-    void Statement::BindBlob(int col, const void* val, int val_len) 
+    void Statement::BindBlob(int col, const void* val, size_t val_len)
     {
-      CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, val_len, SQLITE_TRANSIENT),
-              ErrorCode_BadParameterType);
+      if (static_cast<size_t>(static_cast<int>(val_len)) != val_len)
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteBindOutOfRange);
+      }
+      else
+      {
+        CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, static_cast<int>(val_len), SQLITE_TRANSIENT),
+                ErrorCode_BadParameterType);
+      }
+    }
+
+    void Statement::BindBlob(int col, const std::string& value)
+    {
+      BindBlob(col, value.empty() ? NULL : value.c_str(), value.size());
     }
 
 
--- a/OrthancFramework/Sources/SQLite/Statement.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/SQLite/Statement.h	Fri Jun 27 15:00:33 2025 +0200
@@ -130,7 +130,8 @@
       void BindCString(int col, const char* val);
       void BindString(int col, const std::string& val);
       //void BindString16(int col, const string16& value);
-      void BindBlob(int col, const void* value, int value_len);
+      void BindBlob(int col, const void* value, size_t value_len);
+      void BindBlob(int col, const std::string& value);
 
 
       // Retrieving ----------------------------------------------------------------
--- a/OrthancFramework/Sources/SystemToolbox.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/SystemToolbox.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -454,6 +454,64 @@
     }
   }
 
+#if ORTHANC_ENABLE_MD5 == 1
+  void SystemToolbox::ComputeStreamMD5(std::string& result,
+                                       std::istream& inputStream)
+  {
+    Toolbox::MD5Context context;
+
+    const size_t bufferSize = 1024;
+    char buffer[bufferSize];
+
+    while (inputStream.good())
+    {
+      inputStream.read(buffer, bufferSize);
+      std::streamsize bytesRead = inputStream.gcount();
+
+      if (bytesRead > 0)
+      {
+        context.Append(buffer, bytesRead);
+      }
+    }
+
+    context.Export(result);
+  }
+
+
+  void SystemToolbox::ComputeFileMD5(std::string& result,
+                                     const std::string& path)
+  {
+    boost::filesystem::ifstream fileStream;
+    fileStream.open(path, std::ifstream::in | std::ifstream::binary);
+
+    if (!fileStream.good())
+    {
+      throw OrthancException(ErrorCode_InexistentFile, "File not found: " + path);
+    }
+
+    ComputeStreamMD5(result, fileStream);
+  }
+
+
+  bool SystemToolbox::CompareFilesMD5(const std::string& path1,
+                                      const std::string& path2)
+  {
+    if (GetFileSize(path1) != GetFileSize(path2))
+    {
+      return false;
+    }
+    else
+    {
+      std::string path1md5, path2md5;
+    
+      ComputeFileMD5(path1md5, path1);
+      ComputeFileMD5(path2md5, path2);
+
+      return path1md5 == path2md5;
+    }
+  }
+#endif
+
 
   void SystemToolbox::MakeDirectory(const std::string& path)
   {
--- a/OrthancFramework/Sources/SystemToolbox.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/SystemToolbox.h	Fri Jun 27 15:00:33 2025 +0200
@@ -30,6 +30,10 @@
 #  error The macro ORTHANC_SANDBOXED must be defined
 #endif
 
+#if !defined(ORTHANC_ENABLE_MD5)
+#  error The macro ORTHANC_ENABLE_MD5 must be defined
+#endif
+
 #if ORTHANC_SANDBOXED == 1
 #  error The namespace SystemToolbox cannot be used in sandboxed environments
 #endif
@@ -83,6 +87,18 @@
 
     static uint64_t GetFileSize(const std::string& path);
 
+#if ORTHANC_ENABLE_MD5 == 1
+    static void ComputeStreamMD5(std::string& result,
+                                 std::istream& stream);
+
+    static void ComputeFileMD5(std::string& result,
+                               const std::string& path);
+
+    // returns true if file have the same MD5
+    static bool CompareFilesMD5(const std::string& path1,
+                                const std::string& path2); 
+#endif
+
     static void MakeDirectory(const std::string& path);
 
     static bool IsExistingFile(const std::string& path);
--- a/OrthancFramework/Sources/Toolbox.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -64,6 +64,7 @@
 #include <boost/algorithm/string/join.hpp>
 #include <boost/lexical_cast.hpp>
 #include <boost/regex.hpp>
+#include <cassert>
 
 #if BOOST_VERSION >= 106600
 #  include <boost/uuid/detail/sha1.hpp>
@@ -207,6 +208,112 @@
 
 namespace Orthanc
 {
+#if ORTHANC_ENABLE_MD5 == 1
+  static char GetHexadecimalCharacter(uint8_t value)
+  {
+    assert(value < 16);
+
+    if (value < 10)
+    {
+      return value + '0';
+    }
+    else
+    {
+      return (value - 10) + 'a';
+    }
+  }
+
+
+  struct Toolbox::MD5Context::PImpl
+  {
+    md5_state_s  state_;
+    bool         done_;
+
+    PImpl() :
+      done_(false)
+    {
+      md5_init(&state_);
+    }
+  };
+
+
+  Toolbox::MD5Context::MD5Context() :
+    pimpl_(new PImpl)
+  {
+  }
+
+
+  void Toolbox::MD5Context::Append(const void* data,
+                                   size_t size)
+  {
+    static const size_t MAX_SIZE = 128 * 1024 * 1024;
+
+    if (pimpl_->done_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    const uint8_t *p = reinterpret_cast<const uint8_t*>(data);
+
+    while (size > 0)
+    {
+      /**
+       * The built-in implementation of MD5 requires that "size" can
+       * be casted to "int", so we feed it by chunks of maximum
+       * 128MB. This fixes an incorrect behavior in Orthanc <= 1.12.7.
+       **/
+
+      int chunkSize;
+      if (size > MAX_SIZE)
+      {
+        chunkSize = static_cast<int>(MAX_SIZE);
+      }
+      else
+      {
+        chunkSize = static_cast<int>(size);
+      }
+
+      md5_append(&pimpl_->state_, reinterpret_cast<const md5_byte_t*>(p), chunkSize);
+
+      p += chunkSize;
+
+      assert(static_cast<size_t>(chunkSize) <= size);
+      size -= chunkSize;
+    }
+  }
+
+
+  void Toolbox::MD5Context::Append(const std::string& source)
+  {
+    if (source.size() > 0)
+    {
+      Append(source.c_str(), source.size());
+    }
+  }
+
+
+  void Toolbox::MD5Context::Export(std::string& target)
+  {
+    if (pimpl_->done_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    pimpl_->done_ = true;
+
+    md5_byte_t actualHash[16];
+    md5_finish(&pimpl_->state_, actualHash);
+
+    target.resize(32);
+    for (unsigned int i = 0; i < 16; i++)
+    {
+      target[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16));
+      target[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16));
+    }
+  }
+#endif  /* ORTHANC_ENABLE_MD5 */
+
+
   void Toolbox::LinesIterator::FindEndOfLine()
   {
     lineEnd_ = lineStart_;
@@ -444,21 +551,6 @@
 
 
 #if ORTHANC_ENABLE_MD5 == 1
-  static char GetHexadecimalCharacter(uint8_t value)
-  {
-    assert(value < 16);
-
-    if (value < 10)
-    {
-      return value + '0';
-    }
-    else
-    {
-      return (value - 10) + 'a';
-    }
-  }
-
-
   void Toolbox::ComputeMD5(std::string& result,
                            const std::string& data)
   {
@@ -477,25 +569,9 @@
                            const void* data,
                            size_t size)
   {
-    md5_state_s state;
-    md5_init(&state);
-
-    if (size > 0)
-    {
-      md5_append(&state, 
-                 reinterpret_cast<const md5_byte_t*>(data), 
-                 static_cast<int>(size));
-    }
-
-    md5_byte_t actualHash[16];
-    md5_finish(&state, actualHash);
-
-    result.resize(32);
-    for (unsigned int i = 0; i < 16; i++)
-    {
-      result[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16));
-      result[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16));
-    }
+    MD5Context context;
+    context.Append(data, size);
+    context.Export(result);
   }
 
   void Toolbox::ComputeMD5(std::string& result,
--- a/OrthancFramework/Sources/Toolbox.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/Sources/Toolbox.h	Fri Jun 27 15:00:33 2025 +0200
@@ -82,6 +82,25 @@
   class ORTHANC_PUBLIC Toolbox
   {
   public:
+#if ORTHANC_ENABLE_MD5 == 1
+    class ORTHANC_PUBLIC MD5Context : public boost::noncopyable
+    {
+    private:
+      class PImpl;
+      boost::shared_ptr<PImpl> pimpl_;
+
+    public:
+      MD5Context();
+
+      void Append(const void* data,
+                  size_t size);
+
+      void Append(const std::string& source);
+
+      void Export(std::string& target);
+    };
+#endif
+
     class ORTHANC_PUBLIC LinesIterator : public boost::noncopyable
     {
     private:
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Fri Jun 27 15:00:33 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,14 @@
 
 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(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, NULL);
+
   std::string r;
   accessor.Read(r, info);
 
@@ -191,13 +203,14 @@
 
 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(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, NULL);
+
   std::string r;
   accessor.Read(r, info);
 
@@ -212,20 +225,22 @@
 
 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(compressedInfo, compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, NULL);
+
+  std::string r;
   accessor.Read(r, compressedInfo);
   ASSERT_EQ(compressedData, r);
 
+  FileInfo uncompressedInfo;
+  accessor.Write(uncompressedInfo, uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, NULL);
   accessor.Read(r, uncompressedInfo);
   ASSERT_EQ(uncompressedData, r);
   ASSERT_NE(compressedData, r);
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -397,6 +397,25 @@
 
   Toolbox::ComputeMD5(s, set);
   ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); // set md5 same as string with the values sorted
+
+  {
+    Toolbox::MD5Context context;
+    context.Append("");
+    context.Append(NULL, 0);
+    context.Append("Hello");
+    context.Export(s);
+    ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s);
+    ASSERT_THROW(context.Append("World"), OrthancException);
+    ASSERT_THROW(context.Export(s), OrthancException);
+  }
+
+#if ORTHANC_SANDBOXED != 1
+  {
+    std::istringstream iss(std::string("aaabbbccc"));
+    SystemToolbox::ComputeStreamMD5(s, iss);
+    ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s);
+  }
+#endif
 }
 
 TEST(Toolbox, ComputeSHA1)
@@ -1591,6 +1610,47 @@
 #endif
 
 
+#if ORTHANC_SANDBOXED != 1 && ORTHANC_ENABLE_MD5 == 1
+TEST(Toolbox, FileMD5)
+{
+  {
+    TemporaryFile tmp1, tmp2;
+    std::string s = "aaabbbccc";
+
+    SystemToolbox::WriteFile(s, tmp1.GetPath());
+    SystemToolbox::WriteFile(s, tmp2.GetPath());
+
+    std::string md5;
+    SystemToolbox::ComputeFileMD5(md5, tmp1.GetPath());
+
+    ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", md5);
+    ASSERT_TRUE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath()));
+  }
+
+  { // different sizes
+    TemporaryFile tmp1, tmp2;
+    std::string s1 = "aaabbbccc";
+    std::string s2 = "aaabbbcccd";
+
+    SystemToolbox::WriteFile(s1, tmp1.GetPath());
+    SystemToolbox::WriteFile(s2, tmp2.GetPath());
+
+    ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath()));
+  }
+
+  { // same sizes, different contents
+    TemporaryFile tmp1, tmp2;
+    std::string s1 = "aaabbbccc";
+    std::string s2 = "aaabbbccd";
+
+    SystemToolbox::WriteFile(s1, tmp1.GetPath());
+    SystemToolbox::WriteFile(s2, tmp2.GetPath());
+
+    ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath()));
+  }
+}
+#endif
+
 #if ORTHANC_SANDBOXED != 1
 TEST(Toolbox, GetMacAddressess)
 {
--- a/OrthancServer/CMakeLists.txt	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/CMakeLists.txt	Fri Jun 27 15:00:33 2025 +0200
@@ -203,6 +203,8 @@
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV3.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV4.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPlugins.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginMemoryBuffer32.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginMemoryBuffer64.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsEnumerations.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsErrorDictionary.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsJob.cpp
@@ -243,15 +245,18 @@
 #####################################################################
 
 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_DELETED_FILES             ${CMAKE_SOURCE_DIR}/Sources/Database/InstallDeletedFiles.sql
+  INSTALL_KEY_VALUE_STORES_AND_QUEUES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallKeyValueStoresAndQueues.sql
   )
 
 if (STANDALONE_BUILD)
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -1448,6 +1448,69 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+    virtual void StoreKeyValue(const std::string& storeId,
+                               const std::string& key,
+                               const void* value,
+                               size_t valueSize) 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 ListKeysValues(std::list<std::string>& keys,
+                                std::list<std::string>& values,
+                                const std::string& storeId,
+                                bool first,
+                                const std::string& from,
+                                uint64_t limit) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void EnqueueValue(const std::string& queueId,
+                              const void* value,
+                              size_t valueSize) 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 uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void GetAttachmentCustomData(std::string& customData,
+                                         const std::string& attachmentUuid) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
+
+    virtual void SetAttachmentCustomData(const std::string& attachmentUuid,
+                                         const void* customData,
+                                         size_t customDataSize) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
   };
 
 
@@ -1620,7 +1683,7 @@
 
 
   void OrthancPluginDatabase::Upgrade(unsigned int targetVersion,
-                                      IStorageArea& storageArea)
+                                      IPluginStorageArea& storageArea)
   {
     VoidDatabaseListener listener;
     
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -677,7 +677,6 @@
       }
     }
 
-    
     virtual bool LookupGlobalProperty(std::string& target,
                                       GlobalProperty property,
                                       bool shared) ORTHANC_OVERRIDE
@@ -1061,6 +1060,69 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+    virtual void StoreKeyValue(const std::string& storeId,
+                               const std::string& key,
+                               const void* value,
+                               size_t valueSize) 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 ListKeysValues(std::list<std::string>& keys,
+                                std::list<std::string>& values,
+                                const std::string& storeId,
+                                bool first,
+                                const std::string& from,
+                                uint64_t limit) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void EnqueueValue(const std::string& queueId,
+                              const void* value,
+                              size_t valueSize) 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 uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Not supported
+    }
+
+    virtual void GetAttachmentCustomData(std::string& customData,
+                                         const std::string& attachmentUuid) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
+
+    virtual void SetAttachmentCustomData(const std::string& attachmentUuid,
+                                         const void* customData,
+                                         size_t customDataSize) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
   };
 
   
@@ -1231,7 +1293,7 @@
 
   
   void OrthancPluginDatabaseV3::Upgrade(unsigned int targetVersion,
-                                        IStorageArea& storageArea)
+                                        IPluginStorageArea& storageArea)
   {
     VoidDatabaseListener listener;
     
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -41,7 +41,7 @@
 #include "OrthancDatabasePlugin.pb.h"  // Auto-generated file
 
 #include <cassert>
-
+#include <limits>
 
 namespace Orthanc
 {
@@ -100,15 +100,17 @@
   }
 
     
-  static FileInfo Convert(const DatabasePluginMessages::FileInfo& source)
+  static void Convert(FileInfo& info,
+                      const DatabasePluginMessages::FileInfo& source)
   {
-    return FileInfo(source.uuid(),
+    info = FileInfo(source.uuid(),
                     static_cast<FileContentType>(source.content_type()),
                     source.uncompressed_size(),
                     source.uncompressed_hash(),
                     static_cast<CompressionType>(source.compression_type()),
                     source.compressed_size(),
                     source.compressed_hash());
+    info.SetCustomData(source.custom_data());
   }
 
 
@@ -576,6 +578,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.8
       request.mutable_add_attachment()->set_revision(revision);
 
       ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request);
@@ -604,7 +607,9 @@
       DatabasePluginMessages::TransactionResponse response;
       ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DELETE_ATTACHMENT, request);
 
-      listener_.SignalAttachmentDeleted(Convert(response.delete_attachment().deleted_attachment()));
+      FileInfo info;
+      Convert(info, response.delete_attachment().deleted_attachment());
+      listener_.SignalAttachmentDeleted(info);
     }
 
     
@@ -629,7 +634,9 @@
 
       for (int i = 0; i < response.delete_resource().deleted_attachments().size(); i++)
       {
-        listener_.SignalAttachmentDeleted(Convert(response.delete_resource().deleted_attachments(i)));
+        FileInfo info;
+        Convert(info, response.delete_resource().deleted_attachments(i));
+        listener_.SignalAttachmentDeleted(info);
       }
 
       for (int i = 0; i < response.delete_resource().deleted_resources().size(); i++)
@@ -1006,7 +1013,7 @@
 
       if (response.lookup_attachment().found())
       {
-        attachment = Convert(response.lookup_attachment().attachment());
+        Convert(attachment, response.lookup_attachment().attachment());
         revision = response.lookup_attachment().revision();
         return true;
       }
@@ -1016,7 +1023,48 @@
       }
     }
 
-    
+
+    virtual void GetAttachmentCustomData(std::string& customData,
+                                         const std::string& attachmentUuid) ORTHANC_OVERRIDE
+    {
+      if (database_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_get_attachment_custom_data()->set_uuid(attachmentUuid);
+
+        DatabasePluginMessages::TransactionResponse response;
+        ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ATTACHMENT_CUSTOM_DATA, request);
+
+        customData = response.get_attachment_custom_data().custom_data();
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual void SetAttachmentCustomData(const std::string& attachmentUuid,
+                                         const void* customData,
+                                         size_t customDataSize) ORTHANC_OVERRIDE
+    {
+      if (database_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_set_attachment_custom_data()->set_uuid(attachmentUuid);
+        request.mutable_set_attachment_custom_data()->set_custom_data(customData, customDataSize);
+
+        DatabasePluginMessages::TransactionResponse response;
+        ExecuteTransaction(response, DatabasePluginMessages::OPERATION_SET_ATTACHMENT_CUSTOM_DATA, request);
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+
     virtual bool LookupGlobalProperty(std::string& target,
                                       GlobalProperty property,
                                       bool shared) ORTHANC_OVERRIDE
@@ -1686,7 +1734,9 @@
 
           for (int j = 0; j < source.attachments().size(); j++)
           {
-            target->AddAttachment(Convert(source.attachments(j)), source.attachments_revisions(j));
+            FileInfo info;
+            Convert(info, source.attachments(j));
+            target->AddAttachment(info, source.attachments_revisions(j));
           }
 
           Convert(*target, ResourceType_Patient, source.patient_content());
@@ -1748,7 +1798,8 @@
 
             for (int j = 0; j < source.one_instance_attachments().size(); j++)
             {
-              FileInfo info(Convert(source.one_instance_attachments(j)));
+              FileInfo info;
+              Convert(info, source.one_instance_attachments(j));
               if (attachments.find(info.GetContentType()) == attachments.end())
               {
                 attachments[info.GetContentType()] = info;
@@ -1805,6 +1856,201 @@
         find.ExecuteExpand(response, capabilities, request, identifier);
       }
     }
+
+    virtual void StoreKeyValue(const std::string& storeId,
+                               const std::string& key,
+                               const void* value,
+                               size_t valueSize) ORTHANC_OVERRIDE
+    {
+      // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32"
+      // https://protobuf.dev/programming-guides/proto3/
+      if (valueSize > std::numeric_limits<uint32_t>::max())
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_store_key_value()->set_store_id(storeId);
+        request.mutable_store_key_value()->set_key(key);
+        request.mutable_store_key_value()->set_value(value, valueSize);
+
+        ExecuteTransaction(DatabasePluginMessages::OPERATION_STORE_KEY_VALUE, request);
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual void DeleteKeyValue(const std::string& storeId,
+                                const std::string& key) ORTHANC_OVERRIDE
+    {
+      if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_delete_key_value()->set_store_id(storeId);
+        request.mutable_delete_key_value()->set_key(key);
+
+        ExecuteTransaction(DatabasePluginMessages::OPERATION_DELETE_KEY_VALUE, request);
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual bool GetKeyValue(std::string& value,
+                             const std::string& storeId,
+                             const std::string& key) ORTHANC_OVERRIDE
+    {
+      if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_get_key_value()->set_store_id(storeId);
+        request.mutable_get_key_value()->set_key(key);
+
+        DatabasePluginMessages::TransactionResponse response;
+        ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_KEY_VALUE, request);
+
+        if (response.get_key_value().found())
+        {
+          value = response.get_key_value().value();
+          return true;
+        }
+        else
+        {
+          return false;
+        }
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual void ListKeysValues(std::list<std::string>& keys,
+                                std::list<std::string>& values,
+                                const std::string& storeId,
+                                bool fromFirst,
+                                const std::string& fromKey,
+                                uint64_t limit) ORTHANC_OVERRIDE
+    {
+      if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_list_keys_values()->set_store_id(storeId);
+        request.mutable_list_keys_values()->set_from_first(fromFirst);
+        request.mutable_list_keys_values()->set_from_key(fromKey);
+        request.mutable_list_keys_values()->set_limit(limit);
+
+        DatabasePluginMessages::TransactionResponse response;
+        ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LIST_KEY_VALUES, request);
+
+        for (int i = 0; i < response.list_keys_values().keys_values_size(); ++i)
+        {
+          keys.push_back(response.list_keys_values().keys_values(i).key());
+          values.push_back(response.list_keys_values().keys_values(i).value());
+        }
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual void EnqueueValue(const std::string& queueId,
+                              const void* value,
+                              size_t valueSize) ORTHANC_OVERRIDE
+    {
+      // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32"
+      // https://protobuf.dev/programming-guides/proto3/
+      if (valueSize > std::numeric_limits<uint32_t>::max())
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      if (database_.GetDatabaseCapabilities().HasQueuesSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_enqueue_value()->set_queue_id(queueId);
+        request.mutable_enqueue_value()->set_value(value, valueSize);
+
+        ExecuteTransaction(DatabasePluginMessages::OPERATION_ENQUEUE_VALUE, request);
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual bool DequeueValue(std::string& value,
+                              const std::string& queueId,
+                              QueueOrigin origin) ORTHANC_OVERRIDE
+    {
+      if (database_.GetDatabaseCapabilities().HasQueuesSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_dequeue_value()->set_queue_id(queueId);
+
+        switch (origin)
+        {
+          case QueueOrigin_Back:
+            request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_BACK);
+            break;
+
+          case QueueOrigin_Front:
+            request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_FRONT);
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+
+        DatabasePluginMessages::TransactionResponse response;
+        ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DEQUEUE_VALUE, request);
+
+        if (response.dequeue_value().found())
+        {
+          value = response.dequeue_value().value();
+          return true;
+        }
+        else
+        {
+          return false;
+        }
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE
+    {
+      if (database_.GetDatabaseCapabilities().HasQueuesSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_get_queue_size()->set_queue_id(queueId);
+
+        DatabasePluginMessages::TransactionResponse response;
+        ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_QUEUE_SIZE, request);
+
+        return response.get_queue_size().size();
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
   };
 
 
@@ -1895,6 +2141,9 @@
       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());
+      dbCapabilities_.SetAttachmentCustomDataSupport(systemInfo.has_attachment_custom_data());
     }
 
     open_ = true;
@@ -1961,7 +2210,7 @@
 
   
   void OrthancPluginDatabaseV4::Upgrade(unsigned int targetVersion,
-                                        IStorageArea& storageArea)
+                                        IPluginStorageArea& storageArea)
   {
     if (!open_)
     {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri Jun 27 15:00:33 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"
@@ -54,7 +54,6 @@
 #include "../../../OrthancFramework/Sources/MetricsRegistry.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
-#include "../../../OrthancFramework/Sources/StringMemoryBuffer.h"
 #include "../../../OrthancFramework/Sources/Toolbox.h"
 #include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/OrthancConfiguration.h"
@@ -65,12 +64,12 @@
 #include "OrthancPluginDatabase.h"
 #include "OrthancPluginDatabaseV3.h"
 #include "OrthancPluginDatabaseV4.h"
+#include "PluginMemoryBuffer32.h"
 #include "PluginsEnumerations.h"
 #include "PluginsJob.h"
 
 #include <boost/math/special_functions/round.hpp>
 #include <boost/regex.hpp>
-#include <dcmtk/dcmdata/dcdict.h>
 #include <dcmtk/dcmdata/dcdicent.h>
 #include <dcmtk/dcmnet/dimse.h>
 
@@ -79,6 +78,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:
@@ -417,78 +535,45 @@
   };
   
 
-  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
+  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer* target,
                                  const void* data,
                                  size_t size)
   {
-    if (static_cast<uint32_t>(size) != size)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT);
-    }
-
-    target.size = size;
-
-    if (size == 0)
-    {
-      target.data = NULL;
-    }
-    else
-    {
-      target.data = malloc(size);
-      if (target.data != NULL)
-      {
-        memcpy(target.data, data, size);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-      }
-    }
+    PluginMemoryBuffer32 buffer;
+    buffer.Assign(data, size);
+    buffer.Release(target);
   }
 
 
-  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
+  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer* target,
                                  const std::string& str)
   {
-    if (str.size() == 0)
-    {
-      target.size = 0;
-      target.data = NULL;
-    }
-    else
-    {
-      CopyToMemoryBuffer(target, str.c_str(), str.size());
-    }
+    PluginMemoryBuffer32 buffer;
+    buffer.Assign(str);
+    buffer.Release(target);
   }
 
 
   static char* CopyString(const std::string& str)
   {
-    if (static_cast<uint32_t>(str.size()) != str.size())
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT);
-    }
-
     char *result = reinterpret_cast<char*>(malloc(str.size() + 1));
     if (result == NULL)
     {
       throw OrthancException(ErrorCode_NotEnoughMemory);
     }
 
-    if (str.size() == 0)
-    {
-      result[0] = '\0';
-    }
-    else
-    {
-      memcpy(result, &str[0], str.size() + 1);
-    }
+    if (!str.empty())
+    {
+      memcpy(result, str.c_str(), str.size());
+    }
+
+    result[str.size()] = '\0';  // Add the null terminator of the string
 
     return result;
   }
 
 
-  static void CopyDictionary(OrthancPluginMemoryBuffer& target,
+  static void CopyDictionary(PluginMemoryBuffer32& target,
                              const std::map<std::string, std::string>& dictionary)
   {
     Json::Value json = Json::objectValue;
@@ -499,59 +584,49 @@
       json[it->first] = it->second;
     }
         
-    std::string s = json.toStyledString();
-    CopyToMemoryBuffer(target, s);
+    target.Assign(json.toStyledString());
   }
 
 
   namespace
   {
-    class MemoryBufferRaii : public boost::noncopyable
-    {
-    private:
-      OrthancPluginMemoryBuffer  buffer_;
-
-    public:
-      MemoryBufferRaii()
-      {
-        buffer_.size = 0;
-        buffer_.data = NULL;
-      }
-
-      ~MemoryBufferRaii()
-      {
-        if (buffer_.size != 0)
+    static IMemoryBuffer* GetRangeFromWhole(std::unique_ptr<IMemoryBuffer>& whole,
+                                            uint64_t start /* inclusive */,
+                                            uint64_t end /* exclusive */)
+    {
+      if (start > end)
+      {
+        throw OrthancException(ErrorCode_BadRange);
+      }
+      else if (start == end)
+      {
+        return new PluginMemoryBuffer64;  // Empty
+      }
+      else
+      {
+        if (start == 0 &&
+            end == whole->GetSize())
         {
-          free(buffer_.data);
+          return whole.release();
         }
-      }
-
-      OrthancPluginMemoryBuffer* GetObject()
-      {
-        return &buffer_;
-      }
-
-      void ToString(std::string& target) const
-      {
-        if ((buffer_.data == NULL && buffer_.size != 0) ||
-            (buffer_.data != NULL && buffer_.size == 0))
+        else if (end > whole->GetSize())
         {
-          throw OrthancException(ErrorCode_Plugin);
+          throw OrthancException(ErrorCode_BadRange);
         }
         else
         {
-          target.resize(buffer_.size);
-        
-          if (buffer_.size != 0)
-          {
-            memcpy(&target[0], buffer_.data, buffer_.size);
-          }
+          std::unique_ptr<PluginMemoryBuffer64> range(new PluginMemoryBuffer64);
+          range->Assign(reinterpret_cast<const char*>(whole->GetData()) + start, end - start);
+          assert(range->GetSize() > 0);
+
+          return range.release();
         }
       }
-    };
-  
-
-    class StorageAreaBase : public IStorageArea
+    }
+
+
+    // "legacy" storage plugins don't store customData -> derive from IStorageArea
+    class StorageAreaWithoutCustomData : public IStorageArea
     {
     private:
       OrthancPluginStorageCreate create_;
@@ -564,50 +639,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 +684,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,21 +703,25 @@
         }
       }
 
-      virtual IMemoryBuffer* Read(const std::string& uuid,
-                                  FileContentType type) ORTHANC_OVERRIDE
-      {
-        std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer);
+      virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                       FileContentType type,
+                                       uint64_t start /* inclusive */,
+                                       uint64_t end /* exclusive */) ORTHANC_OVERRIDE
+      {
+        std::unique_ptr<IMemoryBuffer> whole(new MallocMemoryBuffer);
 
         void* buffer = NULL;
         int64_t size = 0;
 
-        OrthancPluginErrorCode error = read_
-          (&buffer, &size, uuid.c_str(), Plugins::Convert(type));
+        OrthancPluginErrorCode error = read_(&buffer, &size, uuid.c_str(), Plugins::Convert(type));
 
         if (error == OrthancPluginErrorCode_Success)
         {
-          result->Assign(buffer, size, free_);
-          return result.release();
+          // Beware that the buffer must be unallocated by the "free_" function provided by the plugin,
+          // so we cannot use "PluginMemoryBuffer64"
+          dynamic_cast<MallocMemoryBuffer&>(*whole).Assign(buffer, size, free_);
+
+          return GetRangeFromWhole(whole, start, end);
         }
         else
         {
@@ -699,15 +730,7 @@
         }
       }
 
-      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
+      virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
       {
         return false;
       }
@@ -715,16 +738,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 +757,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,34 +764,44 @@
       {
         if (readRange_ == NULL)
         {
-          return RangeFromWhole(uuid, type, start, end);
+          std::unique_ptr<IMemoryBuffer> whole(new PluginMemoryBuffer64);
+
+          OrthancPluginErrorCode error = readWhole_(dynamic_cast<PluginMemoryBuffer64&>(*whole).GetObject(),
+                                                    uuid.c_str(), Plugins::Convert(type));
+
+          if (error == OrthancPluginErrorCode_Success)
+          {
+            return GetRangeFromWhole(whole, start, end);
+          }
+          else
+          {
+            GetErrorDictionary().LogError(error, true);
+            throw OrthancException(static_cast<ErrorCode>(error));
+          }
         }
         else
         {
+          std::unique_ptr<PluginMemoryBuffer64> buffer(new PluginMemoryBuffer64);
+
           if (start > end)
           {
             throw OrthancException(ErrorCode_BadRange);
           }
           else if (start == end)
           {
-            return new StringMemoryBuffer;
+            return buffer.release();
           }
           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());
+            buffer->Resize(end - start);
+            assert(buffer->GetSize() > 0);
 
             OrthancPluginErrorCode error =
-              readRange_(&buffer, uuid.c_str(), Plugins::Convert(type), start);
+              readRange_(buffer->GetObject(), uuid.c_str(), Plugins::Convert(type), start);
 
             if (error == OrthancPluginErrorCode_Success)
             {
-              return StringMemoryBuffer::CreateFromSwap(range);
+              return buffer.release();
             }
             else
             {
@@ -802,26 +812,148 @@
         }
       }
       
-      virtual bool HasReadRange() const ORTHANC_OVERRIDE
+      virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
       {
         return (readRange_ != NULL);
       }
     };
 
 
+    // New in Orthanc 1.12.8
+    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
+      {
+        PluginMemoryBuffer32 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.MoveToString(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 PluginMemoryBuffer64;
+        }
+        else
+        {
+          std::unique_ptr<PluginMemoryBuffer64> buffer(new PluginMemoryBuffer64);
+          buffer->Resize(end - start);
+          assert(buffer->GetSize() > 0);
+
+          OrthancPluginErrorCode error =
+            readRange_(buffer->GetObject(), uuid.c_str(), Plugins::Convert(type), start, customData.empty() ? NULL : customData.c_str(), customData.size());
+
+          if (error == OrthancPluginErrorCode_Success)
+          {
+            return buffer.release();
+          }
+          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 +967,7 @@
                          PluginsErrorDictionary&  errorDictionary) :
         sharedLibrary_(sharedLibrary),
         version_(Version1),
-        callbacks_(callbacks),
+        callbacks1_(callbacks),
         errorDictionary_(errorDictionary)
       {
         WarnNoReadRange();
@@ -855,20 +987,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);
@@ -1612,7 +1761,8 @@
     std::unique_ptr<OrthancPluginDatabaseV4>  databaseV4_;  // New in Orthanc 1.12.0
     PluginsErrorDictionary  dictionary_;
     std::string databaseServerIdentifier_;   // New in Orthanc 1.9.2
-    unsigned int maxDatabaseRetries_;   // New in Orthanc 1.9.2
+    unsigned int maxDatabaseRetries_;        // New in Orthanc 1.9.2
+    bool hasStorageAreaCustomData_;          // New in Orthanc 1.12.8
 
     explicit PImpl(const std::string& databaseServerIdentifier) : 
       contextRefCount_(0),
@@ -1623,7 +1773,8 @@
       argc_(1),
       argv_(NULL),
       databaseServerIdentifier_(databaseServerIdentifier),
-      maxDatabaseRetries_(0)
+      maxDatabaseRetries_(0),
+      hasStorageAreaCustomData_(false)
     {
       memset(&moveCallbacks_, 0, sizeof(moveCallbacks_));
     }
@@ -1715,7 +1866,7 @@
       }
     }
 
-    void GetDicomQuery(OrthancPluginMemoryBuffer& target) const
+    void GetDicomQuery(OrthancPluginMemoryBuffer* target) const
     {
       if (currentQuery_ == NULL)
       {
@@ -1724,7 +1875,7 @@
 
       std::string dicom;
       currentQuery_->SaveToMemoryBuffer(dicom);
-      CopyToMemoryBuffer(target, dicom.c_str(), dicom.size());
+      CopyToMemoryBuffer(target, dicom);
     }
 
     bool IsMatch(const void* dicom,
@@ -2109,16 +2260,16 @@
         sizeof(int32_t) != sizeof(OrthancPluginContentType) ||
         sizeof(int32_t) != sizeof(OrthancPluginResourceType) ||
         sizeof(int32_t) != sizeof(OrthancPluginChangeType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginCompressionType) ||
         sizeof(int32_t) != sizeof(OrthancPluginImageFormat) ||
-        sizeof(int32_t) != sizeof(OrthancPluginCompressionType) ||
         sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) ||
         sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) ||
-        sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) ||
         sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
         sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
         sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginJobStopReason) ||
         sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
         sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
@@ -2127,6 +2278,14 @@
         sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) ||
         sizeof(int32_t) != sizeof(OrthancPluginLogLevel) ||
         sizeof(int32_t) != sizeof(OrthancPluginLogCategory) ||
+        sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin) ||
+
+        // From OrthancCDatabasePlugin.h
+        sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType) ||
+
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) ||
@@ -2527,125 +2686,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)
@@ -2735,32 +2775,22 @@
   }
 
 
-  OrthancPluginReceivedInstanceAction OrthancPlugins::ApplyReceivedInstanceCallbacks(
-    MallocMemoryBuffer& modified,
-    const void* receivedDicom,
-    size_t receivedDicomSize,
-    RequestOrigin origin)
+  OrthancPluginReceivedInstanceAction OrthancPlugins::ApplyReceivedInstanceCallbacks(PluginMemoryBuffer64& modified,
+                                                                                     const void* receivedDicom,
+                                                                                     size_t receivedDicomSize,
+                                                                                     RequestOrigin origin)
   {
     boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
 
+    modified.Clear();
+
     if (pimpl_->receivedInstanceCallback_ == NULL)
     {
       return OrthancPluginReceivedInstanceAction_KeepAsIs;
     }
     else
     {
-      OrthancPluginReceivedInstanceAction action;
-      
-      {
-        OrthancPluginMemoryBuffer64 buffer;
-        buffer.size = 0;
-        buffer.data = NULL;
-
-        action = (*pimpl_->receivedInstanceCallback_) (&buffer, receivedDicom, receivedDicomSize, Plugins::Convert(origin));
-        modified.Assign(buffer.data, buffer.size, ::free);
-      }
-
-      return action;
+      return (*pimpl_->receivedInstanceCallback_) (modified.GetObject(), receivedDicom, receivedDicomSize, Plugins::Convert(origin));
     }
   }
 
@@ -3224,7 +3254,7 @@
       lock.GetContext().ReadDicom(dicom, p.instanceId);
     }
 
-    CopyToMemoryBuffer(*p.target, dicom);
+    CopyToMemoryBuffer(p.target, dicom);
   }
 
   static void ThrowOnHttpError(HttpStatus httpStatus)
@@ -3242,6 +3272,10 @@
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
+    else if (intHttpStatus == 415)
+    {
+      throw OrthancException(ErrorCode_UnsupportedMediaType);
+    }
     else
     {
       throw OrthancException(ErrorCode_BadRequest);
@@ -3270,7 +3304,7 @@
     std::string result;
 
     ThrowOnHttpError(IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, httpHeaders));
-    CopyToMemoryBuffer(*p.target, result);
+    CopyToMemoryBuffer(p.target, result);
   }
 
 
@@ -3301,7 +3335,7 @@
     std::string result;
 
     ThrowOnHttpError(IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, headers));
-    CopyToMemoryBuffer(*p.target, result);
+    CopyToMemoryBuffer(p.target, result);
   }
 
 
@@ -3331,7 +3365,7 @@
                                  p.body, p.bodySize, httpHeaders) :
         IHttpHandler::SimplePut(result, NULL, *handler, RequestOrigin_Plugins, p.uri,
                                 p.body, p.bodySize, httpHeaders)));
-    CopyToMemoryBuffer(*p.target, result);
+    CopyToMemoryBuffer(p.target, result);
   }
 
 
@@ -3611,6 +3645,12 @@
           break;
         }
 
+        case OrthancPluginCompressionType_None:
+        {
+          CopyToMemoryBuffer(p.target, p.source, p.size);
+          return;
+        }
+
         default:
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
@@ -3625,7 +3665,7 @@
       }
     }
 
-    CopyToMemoryBuffer(*p.target, result);
+    CopyToMemoryBuffer(p.target, result);
   }
 
 
@@ -3686,7 +3726,7 @@
         MimeType mime;
         std::string frame;
         instance.GetParsedDicomFile().GetRawFrame(frame, mime, p.frameIndex);
-        CopyToMemoryBuffer(*p.targetBuffer, frame);
+        CopyToMemoryBuffer(p.targetBuffer, frame);
         return;
       }
         
@@ -3716,7 +3756,7 @@
 
         p.targetBuffer->data = NULL;
         p.targetBuffer->size = 0;
-        CopyToMemoryBuffer(*p.targetBuffer, instance.GetBufferData(), instance.GetBufferSize());
+        CopyToMemoryBuffer(p.targetBuffer, instance.GetBufferData(), instance.GetBufferSize());
         return;
       }
 
@@ -3825,7 +3865,7 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    CopyToMemoryBuffer(*p.target, compressed.size() > 0 ? compressed.c_str() : NULL, compressed.size());
+    CopyToMemoryBuffer(p.target, compressed);
   }
 
 
@@ -3922,29 +3962,29 @@
     }
 
     // Copy the HTTP headers of the answer, if the plugin requested them
+    PluginMemoryBuffer32 tmpHeaders;
     if (answerHeaders != NULL)
     {
-      CopyDictionary(*answerHeaders, headers);
+      CopyDictionary(tmpHeaders, headers);
     }
 
     // Copy the body of the answer if it makes sense
-    if (client.GetMethod() != HttpMethod_Delete)
-    {
-      try
-      {
-        if (answerBody != NULL)
-        {
-          CopyToMemoryBuffer(*answerBody, body);
-        }
-      }
-      catch (OrthancException&)
-      {
-        if (answerHeaders != NULL)
-        {
-          free(answerHeaders->data);
-        }
-        throw;
-      }
+    PluginMemoryBuffer32 tmpBody;
+    if (client.GetMethod() != HttpMethod_Delete &&
+        answerBody != NULL)
+    {
+      tmpBody.Assign(body);
+    }
+
+    // All the memory has been allocated at this point, so we can safely release the buffers
+    if (answerHeaders != NULL)
+    {
+      tmpHeaders.Release(answerHeaders);
+    }
+
+    if (answerBody != NULL)
+    {
+      tmpBody.Release(answerBody);
     }
   }
 
@@ -4143,25 +4183,28 @@
 
     *p.httpStatus = static_cast<uint16_t>(status);
 
+    PluginMemoryBuffer32 tmpHeaders;
     if (p.answerHeaders != NULL)
     {
-      CopyDictionary(*p.answerHeaders, answerHeaders);
-    }
-
-    try
-    {
-      if (p.answerBody != NULL)
-      {
-        CopyToMemoryBuffer(*p.answerBody, answerBody);
-      }
-    }
-    catch (OrthancException&)
-    {
-      if (p.answerHeaders != NULL)
-      {
-        free(p.answerHeaders->data);
-      }
-      throw;
+      CopyDictionary(tmpHeaders, answerHeaders);
+    }
+
+    PluginMemoryBuffer32 tmpBody;
+    if (p.method != OrthancPluginHttpMethod_Delete &&
+        p.answerBody != NULL)
+    {
+      tmpBody.Assign(answerBody);
+    }
+
+    // All the memory has been allocated at this point, so we can safely release the buffers
+    if (p.answerHeaders != NULL)
+    {
+      tmpHeaders.Release(p.answerHeaders);
+    }
+
+    if (p.answerBody != NULL)
+    {
+      tmpBody.Release(p.answerBody);
     }
   }
 
@@ -4228,29 +4271,29 @@
     }
 
     // Copy the HTTP headers of the answer, if the plugin requested them
+    PluginMemoryBuffer32 tmpHeaders;
     if (p.answerHeaders != NULL)
     {
-      CopyDictionary(*p.answerHeaders, headers);
+      CopyDictionary(tmpHeaders, headers);
     }
 
     // Copy the body of the answer if it makes sense
-    if (p.method != OrthancPluginHttpMethod_Delete)
-    {
-      try
-      {
-        if (p.answerBody != NULL)
-        {
-          CopyToMemoryBuffer(*p.answerBody, body);
-        }
-      }
-      catch (OrthancException&)
-      {
-        if (p.answerHeaders != NULL)
-        {
-          free(p.answerHeaders->data);
-        }
-        throw;
-      }
+    PluginMemoryBuffer32 tmpBody;
+    if (p.method != OrthancPluginHttpMethod_Delete &&
+        p.answerBody != NULL)
+    {
+      tmpBody.Assign(body);
+    }
+
+    // All the memory has been allocated at this point, so we can safely release the buffers
+    if (p.answerHeaders != NULL)
+    {
+      tmpHeaders.Release(p.answerHeaders);
+    }
+
+    if (p.answerBody != NULL)
+    {
+      tmpBody.Release(p.answerBody);
     }
   }
 
@@ -4391,7 +4434,7 @@
       file->SaveToMemoryBuffer(dicom);
     }
 
-    CopyToMemoryBuffer(*parameters.target, dicom);
+    CopyToMemoryBuffer(parameters.target, dicom);
   }
 
 
@@ -4499,6 +4542,184 @@
     reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendMultipartItem(p.answer, p.answerSize, headers);
   }
 
+  void OrthancPlugins::ApplyAdoptDicomInstance(const _OrthancPluginAdoptDicomInstance& parameters)
+  {
+    if (!pimpl_->hasStorageAreaCustomData_)
+    {
+      LOG(WARNING) << "The adoption of a DICOM instance should only be used in combination with a custom "
+                   << "storage area registered using OrthancPluginRegisterStorageArea3()";
+    }
+
+    std::string md5;
+    Toolbox::ComputeMD5(md5, parameters.dicom, parameters.dicomSize);
+
+    std::unique_ptr<DicomInstanceToStore> dicom(DicomInstanceToStore::CreateFromBuffer(parameters.dicom, parameters.dicomSize));
+    dicom->SetOrigin(DicomInstanceOrigin::FromPlugins());
+
+    const std::string attachmentUuid = Toolbox::GenerateUuid();
+
+    FileInfo adoptedFile(attachmentUuid, FileContentType_Dicom, parameters.dicomSize, md5);
+    adoptedFile.SetCustomData(parameters.customData, parameters.customDataSize);
+
+    std::string instanceId;
+    ServerContext::StoreResult result;
+
+    {
+      PImpl::ServerContextReference lock(*pimpl_);
+      result = lock.GetContext().AdoptDicomInstance(instanceId, *dicom, StoreInstanceMode_Default, adoptedFile);
+    }
+
+    CopyToMemoryBuffer(parameters.attachmentUuid, attachmentUuid);
+    CopyToMemoryBuffer(parameters.instanceId, instanceId);
+    *(parameters.storeStatus) = Plugins::Convert(result.GetStatus());
+  }
+
+  static void CheckAttachmentCustomDataSupport(ServerContext& context)
+  {
+    if (!context.GetIndex().HasAttachmentCustomDataSupport())
+    {
+      throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support custom data for attachments");
+    }
+  }
+
+  void OrthancPlugins::ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    CheckAttachmentCustomDataSupport(lock.GetContext());
+
+    std::string customData;
+    lock.GetContext().GetIndex().GetAttachmentCustomData(customData, parameters.attachmentUuid);
+
+    CopyToMemoryBuffer(parameters.customData, customData);
+  }
+
+  void OrthancPlugins::ApplySetAttachmentCustomData(const _OrthancPluginSetAttachmentCustomData& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    CheckAttachmentCustomDataSupport(lock.GetContext());
+
+    lock.GetContext().GetIndex().SetAttachmentCustomData(parameters.attachmentUuid, parameters.customData, parameters.customDataSize);
+  }
+
+  static void CheckKeyValueStoresSupport(ServerContext& context)
+  {
+    if (!context.GetIndex().HasKeyValueStoresSupport())
+    {
+      throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support key-value stores");
+    }
+  }
+
+  void OrthancPlugins::ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    CheckKeyValueStoresSupport(lock.GetContext());
+
+    lock.GetContext().GetIndex().StoreKeyValue(parameters.storeId, parameters.key, parameters.value, parameters.valueSize);
+  }
+
+  void OrthancPlugins::ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    CheckKeyValueStoresSupport(lock.GetContext());
+
+    lock.GetContext().GetIndex().DeleteKeyValue(parameters.storeId, parameters.key);
+  }
+
+  void OrthancPlugins::ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    CheckKeyValueStoresSupport(lock.GetContext());
+
+    std::string value;
+
+    if (lock.GetContext().GetIndex().GetKeyValue(value, parameters.storeId, parameters.key))
+    {
+      CopyToMemoryBuffer(parameters.target, value);
+      *parameters.found = true;
+    }
+    else
+    {
+      *parameters.found = false;
+    }
+  }
+
+  void OrthancPlugins::ApplyCreateKeysValuesIterator(const _OrthancPluginCreateKeysValuesIterator& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    CheckKeyValueStoresSupport(lock.GetContext());
+
+    *parameters.target = reinterpret_cast<OrthancPluginKeysValuesIterator*>(
+      new StatelessDatabaseOperations::KeysValuesIterator(lock.GetContext().GetIndex(), parameters.storeId));
+  }
+
+  static void CheckQueuesSupport(ServerContext& context)
+  {
+    if (!context.GetIndex().HasQueuesSupport())
+    {
+      throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support queues");
+    }
+  }
+
+  void OrthancPlugins::ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    CheckQueuesSupport(lock.GetContext());
+
+    lock.GetContext().GetIndex().EnqueueValue(parameters.queueId, parameters.value, parameters.valueSize);
+  }
+
+  void OrthancPlugins::ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    CheckQueuesSupport(lock.GetContext());
+
+    std::string value;
+
+    if (lock.GetContext().GetIndex().DequeueValue(value, parameters.queueId, Plugins::Convert(parameters.origin)))
+    {
+      CopyToMemoryBuffer(parameters.target, value);
+      *parameters.found = true;
+    }
+    else
+    {
+      *parameters.found = false;
+    }
+  }
+
+  void OrthancPlugins::ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    CheckQueuesSupport(lock.GetContext());
+
+    *parameters.size = lock.GetContext().GetIndex().GetQueueSize(parameters.queueId);
+  }
+
+  void OrthancPlugins::ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+    bool statusHasChanged = false;
+
+    lock.GetContext().GetIndex().SetStableStatus(statusHasChanged,
+                                                 parameters.resourceId,
+                                                 (parameters.stableStatus == OrthancPluginStableStatus_Stable));
+    if (statusHasChanged)
+    {
+      *(parameters.statusHasChanged) = 1;
+    }
+    else
+    {
+      *(parameters.statusHasChanged) = 0;
+    }
+  }
 
   void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params)
   {
@@ -4624,35 +4845,6 @@
   }
 
 
-  namespace
-  {
-    class DictionaryReadLocker
-    {
-    private:
-      const DcmDataDictionary& dictionary_;
-
-    public:
-      DictionaryReadLocker() : dictionary_(dcmDataDict.rdlock())
-      {
-      }
-
-      ~DictionaryReadLocker()
-      {
-#if DCMTK_VERSION_NUMBER >= 364
-        dcmDataDict.rdunlock();
-#else
-        dcmDataDict.unlock();
-#endif
-      }
-
-      const DcmDataDictionary* operator->()
-      {
-        return &dictionary_;
-      }
-    };
-  }
-
-
   void OrthancPlugins::ApplyLookupDictionary(const void* parameters)
   {
     const _OrthancPluginLookupDictionary& p =
@@ -4661,38 +4853,41 @@
     DicomTag tag(FromDcmtkBridge::ParseTag(p.name));
     DcmTagKey tag2(tag.GetGroup(), tag.GetElement());
 
-    DictionaryReadLocker locker;
-    const DcmDictEntry* entry = NULL;
-
-    if (tag.IsPrivate())
-    {
-      // Fix issue 168 (Plugins can't read private tags from the
-      // configuration file)
-      // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=168
-      std::string privateCreator;
-      {
-        OrthancConfiguration::ReaderLock lock;
-        privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator();
-      }
-
-      entry = locker->findEntry(tag2, privateCreator.c_str());
-    }
-    else
-    {
-      entry = locker->findEntry(tag2, NULL);
-    }
-
-    if (entry == NULL)
-    {
-      throw OrthancException(ErrorCode_UnknownDicomTag, p.name);
-    }
-    else
-    {
-      p.target->group = entry->getKey().getGroup();
-      p.target->element = entry->getKey().getElement();
-      p.target->vr = Plugins::Convert(FromDcmtkBridge::Convert(entry->getEVR()));
-      p.target->minMultiplicity = static_cast<uint32_t>(entry->getVMMin());
-      p.target->maxMultiplicity = (entry->getVMMax() == DcmVariableVM ? 0 : static_cast<uint32_t>(entry->getVMMax()));
+    {
+      FromDcmtkBridge::DictionaryReaderLock lock;
+
+      const DcmDictEntry* entry = NULL;  // This value is only valid while "lock" is active
+
+      if (tag.IsPrivate())
+      {
+        // Fix issue 168 (Plugins can't read private tags from the
+        // configuration file)
+        // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=168
+        std::string privateCreator;
+        {
+          OrthancConfiguration::ReaderLock configurationLock;
+          privateCreator = configurationLock.GetConfiguration().GetDefaultPrivateCreator();
+        }
+
+        entry = lock.GetDictionary().findEntry(tag2, privateCreator.c_str());
+      }
+      else
+      {
+        entry = lock.GetDictionary().findEntry(tag2, NULL);
+      }
+
+      if (entry == NULL)
+      {
+        throw OrthancException(ErrorCode_UnknownDicomTag, p.name);
+      }
+      else
+      {
+        p.target->group = entry->getKey().getGroup();
+        p.target->element = entry->getKey().getElement();
+        p.target->vr = Plugins::Convert(FromDcmtkBridge::Convert(entry->getEVR()));
+        p.target->minMultiplicity = static_cast<uint32_t>(entry->getVMMin());
+        p.target->maxMultiplicity = (entry->getVMMax() == DcmVariableVM ? 0 : static_cast<uint32_t>(entry->getVMMax()));
+      }
     }
   }
 
@@ -4948,7 +5143,7 @@
 
         std::string content;
         SystemToolbox::ReadFile(content, p.path);
-        CopyToMemoryBuffer(*p.target, content.size() > 0 ? content.c_str() : NULL, content.size());
+        CopyToMemoryBuffer(p.target, content);
 
         return true;
       }
@@ -5066,32 +5261,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:
@@ -5143,7 +5319,7 @@
       {
         const _OrthancPluginWorklistQueryOperation& p =
           *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters);
-        reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(*p.target);
+        reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(p.target);
         return true;
       }
 
@@ -5526,20 +5702,10 @@
         const _OrthancPluginCreateMemoryBuffer& p =
           *reinterpret_cast<const _OrthancPluginCreateMemoryBuffer*>(parameters);
 
-        p.target->data = NULL;
-        p.target->size = 0;
-        
-        if (p.size != 0)
-        {
-          p.target->data = malloc(p.size);
-          if (p.target->data == NULL)
-          {
-            throw OrthancException(ErrorCode_NotEnoughMemory);
-          }
-
-          p.target->size = p.size;
-        }          
-        
+        PluginMemoryBuffer32 buffer;
+        buffer.Resize(p.size);
+        buffer.Release(p.target);
+
         return true;
       }
 
@@ -5548,19 +5714,9 @@
         const _OrthancPluginCreateMemoryBuffer64& p =
           *reinterpret_cast<const _OrthancPluginCreateMemoryBuffer64*>(parameters);
 
-        p.target->data = NULL;
-        p.target->size = 0;
-        
-        if (p.size != 0)
-        {
-          p.target->data = malloc(p.size);
-          if (p.target->data == NULL)
-          {
-            throw OrthancException(ErrorCode_NotEnoughMemory);
-          }
-
-          p.target->size = p.size;
-        }          
+        PluginMemoryBuffer64 buffer;
+        buffer.Resize(p.size);
+        buffer.Release(p.target);
 
         return true;
       }
@@ -5587,6 +5743,114 @@
         return true;
       }
 
+      case _OrthancPluginService_AdoptDicomInstance:
+      {
+        const _OrthancPluginAdoptDicomInstance& p = *reinterpret_cast<const _OrthancPluginAdoptDicomInstance*>(parameters);
+        ApplyAdoptDicomInstance(p);
+        return true;
+      }
+
+      case _OrthancPluginService_GetAttachmentCustomData:
+      {
+        const _OrthancPluginGetAttachmentCustomData& p = *reinterpret_cast<const _OrthancPluginGetAttachmentCustomData*>(parameters);
+        ApplyGetAttachmentCustomData(p);
+        return true;
+      }
+
+      case _OrthancPluginService_SetAttachmentCustomData:
+      {
+        const _OrthancPluginSetAttachmentCustomData& p = *reinterpret_cast<const _OrthancPluginSetAttachmentCustomData*>(parameters);
+        ApplySetAttachmentCustomData(p);
+        return true;
+      }
+
+      case _OrthancPluginService_StoreKeyValue:
+      {
+        const _OrthancPluginStoreKeyValue& p = *reinterpret_cast<const _OrthancPluginStoreKeyValue*>(parameters);
+        ApplyStoreKeyValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_DeleteKeyValue:
+      {
+        const _OrthancPluginDeleteKeyValue& p = *reinterpret_cast<const _OrthancPluginDeleteKeyValue*>(parameters);
+        ApplyDeleteKeyValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_GetKeyValue:
+      {
+        const _OrthancPluginGetKeyValue& p = *reinterpret_cast<const _OrthancPluginGetKeyValue*>(parameters);
+        ApplyGetKeyValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_CreateKeysValuesIterator:
+      {
+        const _OrthancPluginCreateKeysValuesIterator& p = *reinterpret_cast<const _OrthancPluginCreateKeysValuesIterator*>(parameters);
+        ApplyCreateKeysValuesIterator(p);
+        return true;
+      }
+
+      case _OrthancPluginService_FreeKeysValuesIterator:
+      {
+        const _OrthancPluginFreeKeysValuesIterator& p = *reinterpret_cast<const _OrthancPluginFreeKeysValuesIterator*>(parameters);
+        delete reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator);
+        return true;
+      }
+
+      case _OrthancPluginService_KeysValuesIteratorNext:
+      {
+        const _OrthancPluginKeysValuesIteratorNext& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorNext*>(parameters);
+        StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator);
+        *p.done = iterator.Next() ? 1 : 0;
+        return true;
+      }
+
+      case _OrthancPluginService_KeysValuesIteratorGetKey:
+      {
+        const _OrthancPluginKeysValuesIteratorGetKey& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetKey*>(parameters);
+        StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator);
+        *p.target = iterator.GetKey().c_str();
+        return true;
+      }
+
+      case _OrthancPluginService_KeysValuesIteratorGetValue:
+      {
+        const _OrthancPluginKeysValuesIteratorGetValue& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetValue*>(parameters);
+        StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator);
+        CopyToMemoryBuffer(p.target, iterator.GetValue());
+        return true;
+      }
+
+      case _OrthancPluginService_EnqueueValue:
+      {
+        const _OrthancPluginEnqueueValue& p = *reinterpret_cast<const _OrthancPluginEnqueueValue*>(parameters);
+        ApplyEnqueueValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_DequeueValue:
+      {
+        const _OrthancPluginDequeueValue& p = *reinterpret_cast<const _OrthancPluginDequeueValue*>(parameters);
+        ApplyDequeueValue(p);
+        return true;
+      }
+
+      case _OrthancPluginService_GetQueueSize:
+      {
+        const _OrthancPluginGetQueueSize& p = *reinterpret_cast<const _OrthancPluginGetQueueSize*>(parameters);
+        ApplyGetQueueSize(p);
+        return true;
+      }
+
+      case _OrthancPluginService_SetStableStatus:
+      {
+        const _OrthancPluginSetStableStatus& p = *reinterpret_cast<const _OrthancPluginSetStableStatus*>(parameters);
+        ApplySetStableStatus(p);
+        return true;
+      }
+
       default:
         return false;
     }
@@ -5670,23 +5934,35 @@
 
       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()));
+            pimpl_->hasStorageAreaCustomData_ = true;
+          }
           else
           {
             throw OrthancException(ErrorCode_InternalError);
@@ -5809,7 +6085,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 +6150,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 +6249,7 @@
   }
 
 
-  IStorageArea* OrthancPlugins::CreateStorageArea()
+  IPluginStorageArea* OrthancPlugins::CreateStorageArea()
   {
     if (!HasStorageArea())
     {
@@ -6495,13 +6771,13 @@
            transcoder = pimpl_->transcoderCallbacks_.begin();
          transcoder != pimpl_->transcoderCallbacks_.end(); ++transcoder)
     {
-      MemoryBufferRaii a;
+      PluginMemoryBuffer32 a;
 
       if ((*transcoder) (a.GetObject(), buffer, size, uids.empty() ? NULL : &uids[0],
                          static_cast<uint32_t>(uids.size()), allowNewSopInstanceUid) ==
           OrthancPluginErrorCode_Success)
       {
-        a.ToString(target);
+        a.MoveToString(target);
         return true;
       }
     }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Fri Jun 27 15:00:33 2025 +0200
@@ -56,6 +56,7 @@
 #include "../../Sources/IDicomImageDecoder.h"
 #include "../../Sources/IServerListener.h"
 #include "../../Sources/ServerJobs/IStorageCommitmentFactory.h"
+#include "PluginMemoryBuffer64.h"
 #include "PluginsManager.h"
 
 #include <list>
@@ -88,11 +89,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 +224,28 @@
 
     void ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& parameters);
 
+    void ApplyAdoptDicomInstance(const _OrthancPluginAdoptDicomInstance& parameters);
+
+    void ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters);
+
+    void ApplySetAttachmentCustomData(const _OrthancPluginSetAttachmentCustomData& parameters);
+
+    void ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters);
+
+    void ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters);
+
+    void ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters);
+
+    void ApplyCreateKeysValuesIterator(const _OrthancPluginCreateKeysValuesIterator& parameters);
+
+    void ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters);
+
+    void ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters);
+
+    void ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters);
+
+    void ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters);
+
     void ComputeHash(_OrthancPluginService service,
                      const void* parameters);
 
@@ -284,14 +310,14 @@
                                               const DicomInstanceToStore& instance,
                                               const Json::Value& simplified) ORTHANC_OVERRIDE;
 
-    OrthancPluginReceivedInstanceAction ApplyReceivedInstanceCallbacks(MallocMemoryBuffer& modified,
+    OrthancPluginReceivedInstanceAction ApplyReceivedInstanceCallbacks(PluginMemoryBuffer64& modified,
                                                                        const void* receivedDicomBuffer,
                                                                        size_t receivedDicomBufferSize,
                                                                        RequestOrigin origin);
 
     bool HasStorageArea() const;
 
-    IStorageArea* CreateStorageArea();  // To be freed after use
+    IPluginStorageArea* CreateStorageArea();  // To be freed after use
 
     const SharedLibrary& GetStorageAreaLibrary() const;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,200 @@
+/**
+ * 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/>.
+ **/
+
+
+#include "../../Sources/PrecompiledHeadersServer.h"
+#include "PluginMemoryBuffer32.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../OrthancFramework/Sources/Toolbox.h"
+
+#define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc SDK is necessary to use buffers > 4GB, but is currently not available"
+
+
+namespace Orthanc
+{
+  void PluginMemoryBuffer32::Clear()
+  {
+    if (buffer_.size != 0)
+    {
+      ::free(buffer_.data);
+    }
+
+    buffer_.data = NULL;
+    buffer_.size = 0;
+  }
+
+
+  void PluginMemoryBuffer32::SanityCheck() const
+  {
+    if ((buffer_.data == NULL && buffer_.size != 0) ||
+        (buffer_.data != NULL && buffer_.size == 0))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  PluginMemoryBuffer32::PluginMemoryBuffer32()
+  {
+    buffer_.size = 0;
+    buffer_.data = NULL;
+  }
+
+
+  void PluginMemoryBuffer32::MoveToString(std::string& target)
+  {
+    SanityCheck();
+
+    target.resize(buffer_.size);
+
+    if (buffer_.size != 0)
+    {
+      memcpy(&target[0], buffer_.data, buffer_.size);
+    }
+
+    Clear();
+  }
+
+
+  const void* PluginMemoryBuffer32::GetData() const
+  {
+    SanityCheck();
+
+    if (buffer_.size == 0)
+    {
+      return NULL;
+    }
+    else
+    {
+      return buffer_.data;
+    }
+  }
+
+
+  size_t PluginMemoryBuffer32::GetSize() const
+  {
+    SanityCheck();
+    return buffer_.size;
+  }
+
+
+  void PluginMemoryBuffer32::Release(OrthancPluginMemoryBuffer* target)
+  {
+    SanityCheck();
+
+    if (target == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    target->data = buffer_.data;
+    target->size = buffer_.size;
+
+    buffer_.data = NULL;
+    buffer_.size = 0;
+  }
+
+
+  void PluginMemoryBuffer32::Release(OrthancPluginMemoryBuffer64* target)
+  {
+    SanityCheck();
+
+    if (target == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    target->data = buffer_.data;
+    target->size = buffer_.size;
+
+    buffer_.data = NULL;
+    buffer_.size = 0;
+  }
+
+
+  void PluginMemoryBuffer32::Resize(size_t size)
+  {
+    if (static_cast<size_t>(static_cast<uint32_t>(size)) != size)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT);
+    }
+
+    if (size != buffer_.size)
+    {
+      Clear();
+
+      if (size == 0)
+      {
+        buffer_.data = NULL;
+      }
+      else
+      {
+        buffer_.data = ::malloc(size);
+
+        if (buffer_.data == NULL)
+        {
+          throw OrthancException(ErrorCode_NotEnoughMemory);
+        }
+      }
+
+      buffer_.size = size;
+    }
+  }
+
+
+  void PluginMemoryBuffer32::Assign(const void* data,
+                                    size_t size)
+  {
+    Resize(size);
+
+    if (size != 0)
+    {
+      memcpy(buffer_.data, data, size);
+    }
+  }
+
+
+  void PluginMemoryBuffer32::Assign(const std::string& data)
+  {
+    if (data.empty())
+    {
+      Assign(NULL, 0);
+    }
+    else
+    {
+      Assign(data.c_str(), data.size());
+    }
+  }
+
+
+  void PluginMemoryBuffer32::ToJsonObject(Json::Value& target) const
+  {
+    SanityCheck();
+
+    if (!Toolbox::ReadJson(target, buffer_.data, buffer_.size) ||
+        target.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_Plugin, "The plugin has not provided a valid JSON object");
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,78 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#  error The plugin support is disabled
+#endif
+
+#include "../../../OrthancFramework/Sources/MallocMemoryBuffer.h"
+#include "../Include/orthanc/OrthancCPlugin.h"
+
+#include <json/value.h>
+
+namespace Orthanc
+{
+  class PluginMemoryBuffer32 : public IMemoryBuffer
+  {
+  private:
+    OrthancPluginMemoryBuffer  buffer_;
+
+    void SanityCheck() const;
+
+  public:
+    PluginMemoryBuffer32();
+
+    virtual ~PluginMemoryBuffer32()
+    {
+      Clear();
+    }
+
+    virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE;
+
+    virtual const void* GetData() const ORTHANC_OVERRIDE;
+
+    virtual size_t GetSize() const ORTHANC_OVERRIDE;
+
+    OrthancPluginMemoryBuffer* GetObject()
+    {
+      return &buffer_;
+    }
+
+    void Release(OrthancPluginMemoryBuffer* target);
+
+    void Release(OrthancPluginMemoryBuffer64* target);
+
+    void Clear();
+
+    void Resize(size_t size);
+
+    void Assign(const void* data,
+                size_t size);
+
+    void Assign(const std::string& data);
+
+    void ToJsonObject(Json::Value& target) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,163 @@
+/**
+ * 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/>.
+ **/
+
+
+#include "../../Sources/PrecompiledHeadersServer.h"
+#include "PluginMemoryBuffer64.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+
+namespace Orthanc
+{
+  void PluginMemoryBuffer64::Clear()
+  {
+    if (buffer_.size != 0)
+    {
+      ::free(buffer_.data);
+    }
+
+    buffer_.data = NULL;
+    buffer_.size = 0;
+  }
+
+
+  void PluginMemoryBuffer64::SanityCheck() const
+  {
+    if ((buffer_.data == NULL && buffer_.size != 0) ||
+        (buffer_.data != NULL && buffer_.size == 0))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  PluginMemoryBuffer64::PluginMemoryBuffer64()
+  {
+    buffer_.size = 0;
+    buffer_.data = NULL;
+  }
+
+
+  void PluginMemoryBuffer64::MoveToString(std::string& target)
+  {
+    SanityCheck();
+
+    target.resize(buffer_.size);
+
+    if (buffer_.size != 0)
+    {
+      memcpy(&target[0], buffer_.data, buffer_.size);
+    }
+
+    Clear();
+  }
+
+
+  const void* PluginMemoryBuffer64::GetData() const
+  {
+    SanityCheck();
+
+    if (buffer_.size == 0)
+    {
+      return NULL;
+    }
+    else
+    {
+      return buffer_.data;
+    }
+  }
+
+
+  size_t PluginMemoryBuffer64::GetSize() const
+  {
+    SanityCheck();
+    return buffer_.size;
+  }
+
+
+  void PluginMemoryBuffer64::Release(OrthancPluginMemoryBuffer64* target)
+  {
+    SanityCheck();
+
+    if (target == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    target->data = buffer_.data;
+    target->size = buffer_.size;
+
+    buffer_.data = NULL;
+    buffer_.size = 0;
+  }
+
+
+  void PluginMemoryBuffer64::Resize(size_t size)
+  {
+    if (size != buffer_.size)
+    {
+      Clear();
+
+      if (size == 0)
+      {
+        buffer_.data = NULL;
+      }
+      else
+      {
+        buffer_.data = ::malloc(size);
+
+        if (buffer_.data == NULL)
+        {
+          throw OrthancException(ErrorCode_NotEnoughMemory);
+        }
+      }
+
+      buffer_.size = size;
+    }
+  }
+
+
+  void PluginMemoryBuffer64::Assign(const void* data,
+                                    size_t size)
+  {
+    Resize(size);
+
+    if (size != 0)
+    {
+      memcpy(buffer_.data, data, size);
+    }
+  }
+
+
+  void PluginMemoryBuffer64::Assign(const std::string& data)
+  {
+    if (data.empty())
+    {
+      Assign(NULL, 0);
+    }
+    else
+    {
+      Assign(data.c_str(), data.size());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,74 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#  error The plugin support is disabled
+#endif
+
+#include "../../../OrthancFramework/Sources/MallocMemoryBuffer.h"
+#include "../Include/orthanc/OrthancCPlugin.h"
+
+#include <json/value.h>
+
+namespace Orthanc
+{
+  class PluginMemoryBuffer64 : public IMemoryBuffer
+  {
+  private:
+    OrthancPluginMemoryBuffer64  buffer_;
+
+    void SanityCheck() const;
+
+  public:
+    PluginMemoryBuffer64();
+
+    virtual ~PluginMemoryBuffer64()
+    {
+      Clear();
+    }
+
+    virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE;
+
+    virtual const void* GetData() const ORTHANC_OVERRIDE;
+
+    virtual size_t GetSize() const ORTHANC_OVERRIDE;
+
+    OrthancPluginMemoryBuffer64* GetObject()
+    {
+      return &buffer_;
+    }
+
+    void Release(OrthancPluginMemoryBuffer64* target);
+
+    void Clear();
+
+    void Resize(size_t size);
+
+    void Assign(const void* data,
+                size_t size);
+
+    void Assign(const std::string& data);
+  };
+}
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Fri Jun 27 15:00:33 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/Engine/PluginsJob.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsJob.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -32,6 +32,7 @@
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
 #include "../../../OrthancFramework/Sources/Toolbox.h"
+#include "PluginMemoryBuffer32.h"
 
 #include <cassert>
 
@@ -152,53 +153,11 @@
     return parameters_.getProgress(parameters_.job);
   }
 
-
-  namespace
-  {
-    class MemoryBufferRaii : public boost::noncopyable
-    {
-    private:
-      OrthancPluginMemoryBuffer  buffer_;
-
-    public:
-      MemoryBufferRaii()
-      {
-        buffer_.size = 0;
-        buffer_.data = NULL;
-      }
-
-      ~MemoryBufferRaii()
-      {
-        if (buffer_.size != 0)
-        {
-          free(buffer_.data);
-        }
-      }
-
-      OrthancPluginMemoryBuffer* GetObject()
-      {
-        return &buffer_;
-      }
-
-      void ToJsonObject(Json::Value& target) const
-      {
-        if ((buffer_.data == NULL && buffer_.size != 0) ||
-            (buffer_.data != NULL && buffer_.size == 0) ||
-            !Toolbox::ReadJson(target, buffer_.data, buffer_.size) ||
-            target.type() != Json::objectValue)
-        {
-          throw OrthancException(ErrorCode_Plugin,
-                                 "A job plugin must provide a JSON object as its public content and as its serialization");
-        }
-      }
-    };
-  }
-  
   void PluginsJob::GetPublicContent(Json::Value& value) const
   {
     if (parameters_.getContent != NULL)
     {
-      MemoryBufferRaii target;
+      PluginMemoryBuffer32 target;
 
       OrthancPluginErrorCode code = parameters_.getContent(target.GetObject(), parameters_.job);
 
@@ -236,7 +195,7 @@
   {
     if (parameters_.getSerialized != NULL)
     {
-      MemoryBufferRaii target;
+      PluginMemoryBuffer32 target;
 
       int32_t code = parameters_.getContent(target.GetObject(), parameters_.job);
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Fri Jun 27 15:00:33 2025 +0200
@@ -80,6 +80,23 @@
   } _OrthancPluginDatabaseAnswerType;
 
 
+  typedef enum
+  {
+    OrthancPluginDatabaseTransactionType_ReadOnly = 1,
+    OrthancPluginDatabaseTransactionType_ReadWrite = 2,
+    OrthancPluginDatabaseTransactionType_INTERNAL = 0x7fffffff
+  } OrthancPluginDatabaseTransactionType;
+
+
+  typedef enum
+  {
+    OrthancPluginDatabaseEventType_DeletedAttachment = 1,
+    OrthancPluginDatabaseEventType_DeletedResource = 2,
+    OrthancPluginDatabaseEventType_RemainingAncestor = 3,
+    OrthancPluginDatabaseEventType_INTERNAL = 0x7fffffff
+  } OrthancPluginDatabaseEventType;
+
+
   typedef struct
   {
     const char* uuid;
@@ -899,7 +916,9 @@
     OrthancPluginDatabaseContext* result = NULL;
     _OrthancPluginRegisterDatabaseBackend params;
 
-    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
+    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType))
     {
       return NULL;
     }
@@ -951,7 +970,9 @@
     OrthancPluginDatabaseContext* result = NULL;
     _OrthancPluginRegisterDatabaseBackendV2 params;
 
-    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
+    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType))
     {
       return NULL;
     }
@@ -982,23 +1003,6 @@
    **/
 
 /*<! @cond Doxygen_Suppress */
-  typedef enum
-  {
-    OrthancPluginDatabaseTransactionType_ReadOnly = 1,
-    OrthancPluginDatabaseTransactionType_ReadWrite = 2,
-    OrthancPluginDatabaseTransactionType_INTERNAL = 0x7fffffff
-  } OrthancPluginDatabaseTransactionType;
-
-
-  typedef enum
-  {
-    OrthancPluginDatabaseEventType_DeletedAttachment = 1,
-    OrthancPluginDatabaseEventType_DeletedResource = 2,
-    OrthancPluginDatabaseEventType_RemainingAncestor = 3,
-    OrthancPluginDatabaseEventType_INTERNAL = 0x7fffffff
-  } OrthancPluginDatabaseEventType;
-
-
   typedef struct
   {
     OrthancPluginDatabaseEventType type;
@@ -1327,8 +1331,6 @@
 
   } OrthancPluginDatabaseBackendV3;
 
-/*<! @endcond */
-  
 
   typedef struct
   {
@@ -1348,7 +1350,9 @@
   {
     _OrthancPluginRegisterDatabaseBackendV3 params;
 
-    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
+    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType))
     {
       return OrthancPluginErrorCode_Plugin;
     }
@@ -1361,7 +1365,10 @@
 
     return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, &params);
   }
-  
+
+/*<! @endcond */
+
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Jun 27 15:00:33 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  9
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -421,7 +421,7 @@
   } OrthancPluginHttpRequest;
 
 
-  typedef enum 
+  typedef enum
   {
     /* Generic services */
     _OrthancPluginService_LogInfo = 1,
@@ -469,7 +469,21 @@
     _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_AdoptDicomInstance = 46,                  /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_GetAttachmentCustomData = 47,             /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_SetAttachmentCustomData = 48,             /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_StoreKeyValue = 49,                       /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_DeleteKeyValue = 50,                      /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_GetKeyValue = 51,                         /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_CreateKeysValuesIterator = 52,            /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_FreeKeysValuesIterator = 53,              /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_KeysValuesIteratorNext = 54,              /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_KeysValuesIteratorGetKey = 55,            /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_KeysValuesIteratorGetValue = 56,          /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_EnqueueValue = 57,                        /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_DequeueValue = 58,                        /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_GetQueueSize = 59,                        /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_SetStableStatus = 60,                     /* New in Orthanc 1.12.9 */
 
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -492,6 +506,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.8 */
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -562,7 +577,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,
@@ -752,7 +767,7 @@
 
   /**
    * The supported types of changes that can be signaled to the change callback.
-   * Note: this enum is not used to store changes in the DB !
+   * Note: This enumeration is not used to store changes in the database!
    * @ingroup Callbacks
    **/
   typedef enum
@@ -791,6 +806,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.8) */
 
     _OrthancPluginCompressionType_INTERNAL = 0x7fffffff
   } OrthancPluginCompressionType;
@@ -1130,6 +1146,43 @@
 
 
   /**
+   * The store status related to the adoption of a DICOM instance.
+   **/
+  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 modes to remove an element from a queue.
+   **/
+  typedef enum
+  {
+    OrthancPluginQueueOrigin_Front = 0,    /*!< Dequeue from the front of the queue */
+    OrthancPluginQueueOrigin_Back = 1,     /*!< Dequeue from the back of the queue */
+
+    _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff
+  } OrthancPluginQueueOrigin;
+
+  /**
+   * The "stable" status of a resource.
+   **/
+  typedef enum
+  {
+    OrthancPluginStableStatus_Stable = 0,     /*!< The resource is stable */
+    OrthancPluginStableStatus_Unstable = 1,   /*!< The resource is unstable */
+
+    _OrthancPluginStableStatus_INTERNAL = 0x7fffffff
+  } OrthancPluginStableStatus;
+
+
+  /**
    * @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 +1420,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 +1495,83 @@
 
 
   /**
+   * @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 OrthancPluginCreateMemoryBuffer(). The core of Orthanc will free it.
+   * If the plugin does not generate custom data, leave `customData` unchanged; it will default to an empty value.
+   * @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 that was 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 type The content type corresponding to this file.
+   * @param rangeStart Start position of the requested range in the file.
+   * @param customData The custom data of the file of interest.
+   * @param customDataSize The size of the custom data.
+   * @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,
+    uint32_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 type The content type corresponding to this file.
+   * @param customData The custom data of the file to be removed.
+   * @param customDataSize The size of the custom data.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) (
+    const char* uuid,
+    OrthancPluginContentType type,
+    const void* customData,
+    uint32_t customDataSize);
+
+
+  /**
    * @brief Callback to handle the C-Find SCP requests for worklists.
    *
    * Signature of a callback function that is triggered when Orthanc
@@ -1509,9 +1638,9 @@
    * concurrently by different threads of the Web server of
    * Orthanc. You must implement proper locking if applicable.
    *
-   * Note that if you are using HTTP basic authentication, you can extract
-   * the username from the "Authorization" HTTP header.  The value of that header
-   * contains username:pwd encoded in base64.
+   * Note that if you are using HTTP basic authentication, you can
+   * extract the username from the "Authorization" HTTP header. The
+   * value of that header contains username:pwd encoded in base64.
    *
    * @param method The HTTP method used by the request.
    * @param uri The URI of interest.
@@ -2014,13 +2143,17 @@
         sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
         sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
         sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginJobStopReason) ||
         sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
         sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
         sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) ||
+        sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction) ||
         sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) ||
         sizeof(int32_t) != sizeof(OrthancPluginLogLevel) ||
-        sizeof(int32_t) != sizeof(OrthancPluginLogCategory))
+        sizeof(int32_t) != sizeof(OrthancPluginLogCategory) ||
+        sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin))
     {
       /* Mismatch in the size of the enumerations */
       return 0;
@@ -3327,7 +3460,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 +5048,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 +5094,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 +5135,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 +9056,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 +9508,40 @@
   }
 
 
+  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 readRange The callback function to read some range of a file from the custom storage area.
+   * @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.
@@ -9635,6 +9809,536 @@
   }
 
 
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*    instanceId;
+    OrthancPluginMemoryBuffer*    attachmentUuid;
+    OrthancPluginStoreStatus*     storeStatus;
+    const void*                   dicom;
+    uint64_t                      dicomSize;
+    const void*                   customData;
+    uint32_t                      customDataSize;
+  } _OrthancPluginAdoptDicomInstance;
+
+  /**
+   * @brief Adopt a DICOM instance read from the filesystem.
+   *
+   * This function requests Orthanc to create a DICOM resource at the
+   * "Instance" level in its database, using the content of a DICOM
+   * instance read from the filesystem. The newly created DICOM
+   * resource is associated with an attachment whose content type is
+   * "OrthancPluginContentType_Dicom". The attachment is associated
+   * with the provided custom data.
+   *
+   * This function should only be used in combination with a custom
+   * storage area featuring support for custom data (i.e., installed
+   * using OrthancPluginRegisterStorageArea3()). The custom storage
+   * area is responsible for *not* duplicating the DICOM file into the
+   * storage area of Orthanc, hence the name "Adopt". The support for
+   * custom data is necessary for the custom storage area to
+   * distinguish between adopted and non-adopted DICOM instances.
+   *
+   * Check out the "AdoptDicomInstance" plugin in the source
+   * distribution of Orthanc for a working sample:
+   * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Plugins/Samples/AdoptDicomInstance/
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instanceId The target memory buffer that will be filled by
+   * the Orthanc core with the public identifier of the newly created
+   * instance. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param attachmentUuid The target memory buffer that will be
+   * filled by the Orthanc core with the UUID of the newly created
+   * attachment corresponding to the adopted DICOM instance. It must
+   * be freed with OrthancPluginFreeMemoryBuffer().
+   * @param storeStatus Variable that will be filled by the Orthanc core
+   * with the status of store operation.
+   * @param dicom Pointer to the DICOM instance read from the filesystem.
+   * @param dicomSize Size of the DICOM instance.
+   * @param customData The custom data to associated with the attachment.
+   * @param customDataSize The size of the custom data.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginAdoptDicomInstance(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    instanceId,        /* out */
+    OrthancPluginMemoryBuffer*    attachmentUuid,    /* out */
+    OrthancPluginStoreStatus*     storeStatus,       /* out */
+    const void*                   dicom,
+    uint64_t                      dicomSize,
+    const void*                   customData,
+    uint32_t                      customDataSize)
+  {
+    _OrthancPluginAdoptDicomInstance params;
+    params.instanceId = instanceId;
+    params.attachmentUuid = attachmentUuid;
+    params.storeStatus = storeStatus;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.customData = customData;
+    params.customDataSize = customDataSize;
+
+    return context->InvokeService(context, _OrthancPluginService_AdoptDicomInstance, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*    customData;
+    const char*                   attachmentUuid;
+  } _OrthancPluginGetAttachmentCustomData;
+
+  /**
+   * @brief Retrieve the custom data associated with an attachment in the Orthanc database.
+   *
+   * If no custom data is associated with the attachment of interest,
+   * the target memory buffer is filled with the NULL value and a zero size.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param customData Memory buffer where to store the retrieved value. It must be freed
+   * by the plugin by calling OrthancPluginFreeMemoryBuffer().
+   * @param attachmentUuid The UUID of the attachment of interest.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetAttachmentCustomData(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    customData,     /* out */
+    const char*                   attachmentUuid  /* in */)
+  {
+    _OrthancPluginGetAttachmentCustomData params;
+    params.customData = customData;
+    params.attachmentUuid = attachmentUuid;
+
+    return context->InvokeService(context, _OrthancPluginService_GetAttachmentCustomData, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*  attachmentUuid;
+    const void*  customData;
+    uint32_t     customDataSize;
+  } _OrthancPluginSetAttachmentCustomData;
+
+  /**
+   * @brief Update the custom data associated with an attachment in the Orthanc database.
+   *
+   * This function is notably used in the "orthanc-advanced-storage"
+   * when the plugin moves an attachment.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param attachmentUuid The UUID of the attachment of interest.
+   * @param customData The value to store.
+   * @param customDataSize The size of the value to store.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetAttachmentCustomData(
+    OrthancPluginContext*         context,
+    const char*                   attachmentUuid, /* in */
+    const void*                   customData,     /* in */
+    uint32_t                      customDataSize  /* in */)
+  {
+    _OrthancPluginSetAttachmentCustomData params;
+    params.attachmentUuid = attachmentUuid;
+    params.customData = customData;
+    params.customDataSize = customDataSize;
+
+    return context->InvokeService(context, _OrthancPluginService_SetAttachmentCustomData, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*                   storeId;
+    const char*                   key;
+    const void*                   value;
+    uint32_t                      valueSize;
+  } _OrthancPluginStoreKeyValue;
+
+  /**
+   * @brief Store a key-value pair in the Orthanc database.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the key-value 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 length of the value to store.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStoreKeyValue(
+    OrthancPluginContext*         context,
+    const char*                   storeId,  /* in */
+    const char*                   key,      /* in */
+    const void*                   value,    /* in */
+    uint32_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 Delete a key-value pair from the Orthanc database.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the key-value store.
+   * @param key The key of the value to store (note: storeId + key must be unique).
+   * @return 0 if success, other value if error.
+   **/
+  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
+  {
+    uint8_t*                      found;
+    OrthancPluginMemoryBuffer*    target;
+    const char*                   storeId;
+    const char*                   key;
+  } _OrthancPluginGetKeyValue;
+
+  /**
+   * @brief Get the value associated with a key in the Orthanc key-value store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param found Pointer to a Boolean that is set to "true" iff. the key exists in the key-value store.
+   * @param target Memory buffer where to store the retrieved value. It must be freed
+   * by the plugin by calling OrthancPluginFreeMemoryBuffer().
+   * @param storeId A unique identifier identifying both the plugin and the key-value store.
+   * @param key The key of the value to retrieve from the store (note: storeId + key must be unique).
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetKeyValue(
+    OrthancPluginContext*         context,
+    uint8_t*                      found,   /* out */
+    OrthancPluginMemoryBuffer*    target,  /* out */
+    const char*                   storeId, /* in */
+    const char*                   key      /* in */)
+  {
+    _OrthancPluginGetKeyValue params;
+    params.found = found;
+    params.target = target;
+    params.storeId = storeId;
+    params.key = key;
+
+    return context->InvokeService(context, _OrthancPluginService_GetKeyValue, &params);
+  }
+
+
+  /**
+   * @brief Opaque structure that represents an iterator over the keys and values of
+   * a key-value store.
+   * @ingroup Callbacks
+   **/
+  typedef struct _OrthancPluginKeysValuesIterator_t OrthancPluginKeysValuesIterator;
+
+
+  typedef struct
+  {
+    OrthancPluginKeysValuesIterator**  target;
+    const char*                        storeId;
+  } _OrthancPluginCreateKeysValuesIterator;
+
+
+  /**
+   * @brief Create an iterator over the key-value pairs of a key-value store in the Orthanc database.
+   *
+   * The iterator loops over the keys according to the lexicographical order.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the key-value store.
+   * @return The newly allocated iterator, or NULL in the case of an error.
+   * The iterator must be freed by calling OrthancPluginFreeKeysValuesIterator().
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginKeysValuesIterator* OrthancPluginCreateKeysValuesIterator(
+    OrthancPluginContext*  context,
+    const char*            storeId)
+  {
+    OrthancPluginKeysValuesIterator* target = NULL;
+
+    _OrthancPluginCreateKeysValuesIterator params;
+    params.target = &target;
+    params.storeId = storeId;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateKeysValuesIterator, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginKeysValuesIterator*   iterator;
+  } _OrthancPluginFreeKeysValuesIterator;
+
+  /**
+   * @brief Free an iterator over a key-value store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param iterator The iterator of interest.
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeKeysValuesIterator(
+    OrthancPluginContext*             context,
+    OrthancPluginKeysValuesIterator*  iterator)
+  {
+    _OrthancPluginFreeKeysValuesIterator params;
+    params.iterator = iterator;
+
+    context->InvokeService(context, _OrthancPluginService_FreeKeysValuesIterator, &params);
+  }
+
+
+  typedef struct
+  {
+    uint8_t*                          done;
+    OrthancPluginKeysValuesIterator*  iterator;
+  } _OrthancPluginKeysValuesIteratorNext;
+
+  /**
+   * @brief Read the next element of an iterator over a key-value store.
+  *
+   * The iterator loops over the keys according to the lexicographical order.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param done Pointer to a Boolean that is set to "true" iff. the iterator has reached the end of the store.
+   * @param iterator The iterator of interest.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorNext(
+    OrthancPluginContext*             context,
+    uint8_t*                          done,     /* out */
+    OrthancPluginKeysValuesIterator*  iterator  /* in */)
+  {
+    _OrthancPluginKeysValuesIteratorNext params;
+    params.done = done;
+    params.iterator = iterator;
+
+    return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorNext, &params);
+  }
+
+
+  typedef struct
+  {
+    const char**                      target;
+    OrthancPluginKeysValuesIterator*  iterator;
+  } _OrthancPluginKeysValuesIteratorGetKey;
+
+  /**
+   * @brief Get the current key of an iterator over a key-value store.
+   *
+   * Before using this function, the function OrthancPluginKeysValuesIteratorNext()
+   * must have been called at least once.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param iterator The iterator of interest.
+   * @return The current key, or NULL in the case of an error.
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginKeysValuesIteratorGetKey(
+    OrthancPluginContext*             context,
+    OrthancPluginKeysValuesIterator*  iterator)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginKeysValuesIteratorGetKey params;
+    params.target = &target;
+    params.iterator = iterator;
+
+    if (context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetKey, &params) == OrthancPluginErrorCode_Success)
+    {
+      return target;
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*        target;
+    OrthancPluginKeysValuesIterator*  iterator;
+  } _OrthancPluginKeysValuesIteratorGetValue;
+
+  /**
+   * @brief Get the current value of an iterator over a key-value store.
+   *
+   * Before using this function, the function OrthancPluginKeysValuesIteratorNext()
+   * must have been called at least once.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target Memory buffer where to store the value that has been retrieved from the key-value store.
+   * It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param iterator The iterator of interest.
+   * @return The current value, or NULL in the case of an error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorGetValue(
+    OrthancPluginContext*             context,
+    OrthancPluginMemoryBuffer*        target   /* out */,
+    OrthancPluginKeysValuesIterator*  iterator /* in */)
+  {
+    _OrthancPluginKeysValuesIteratorGetValue params;
+    params.target = target;
+    params.iterator = iterator;
+
+    return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetValue, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*                   queueId;
+    const void*                   value;
+    uint32_t                      valueSize;
+  } _OrthancPluginEnqueueValue;
+
+  /**
+   * @brief Append a value to the back of 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 size of the value to store.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginEnqueueValue(
+    OrthancPluginContext*         context,
+    const char*                   queueId,  /* in */
+    const void*                   value,    /* in */
+    uint32_t                      valueSize /* in */)
+  {
+    _OrthancPluginEnqueueValue params;
+    params.queueId = queueId;
+    params.value = value;
+    params.valueSize = valueSize;
+
+    return context->InvokeService(context, _OrthancPluginService_EnqueueValue, &params);
+  }
+
+
+  typedef struct
+  {
+    uint8_t*                      found;
+    OrthancPluginMemoryBuffer*    target;
+    const char*                   queueId;
+    OrthancPluginQueueOrigin      origin;
+  } _OrthancPluginDequeueValue;
+
+  /**
+   * @brief Dequeue a value from a queue.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param found Pointer to a Boolean that is set to "true" iff. a value has been dequeued.
+   * @param target Memory buffer where to store the value that has been retrieved from the queue.
+   * It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param queueId A unique identifier identifying both the plugin and the queue.
+   * @param origin The position from where the value is dequeued (back for LIFO, front for FIFO).
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDequeueValue(
+    OrthancPluginContext*         context,
+    uint8_t*                      found,    /* out */
+    OrthancPluginMemoryBuffer*    target,   /* out */
+    const char*                   queueId,  /* in */
+    OrthancPluginQueueOrigin      origin    /* in */)
+  {
+    _OrthancPluginDequeueValue params;
+    params.found = found;
+    params.target = target;
+    params.queueId = queueId;
+    params.origin = origin;
+
+    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.
+   * @return 0 if success, other value if error.
+   **/
+  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);
+  }
+
+
+  typedef struct
+  {
+    const char*                   resourceId;
+    OrthancPluginStableStatus     stableStatus;
+    int32_t*                      statusHasChanged;
+  } _OrthancPluginSetStableStatus;
+
+  /**
+   * @brief Change the "Stable" status of a resource.  
+   *        Forcing a resource to "Stable" if it is currently "Unstable" will change 
+   *        its Stable status AND trigger a new Stable change which will also trigger 
+   *        listener callbacks.
+   *        Forcing a resource to "Stable" if it is already "Stable" is a no-op.
+   *        Forcing a resource to "Unstable" will change its Stable status to "Unstable"
+   *        AND reset its stabilization period, no matter of its initial state.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param statusHasChanged Wheter the status has changed (1) or not (0) during the execution of this command.
+   * @param resourceId The Orthanc identifier of the DICOM resource of interest.
+   * @param stableStatus The new stable status of the resource of interest.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetStableStatus(
+    OrthancPluginContext*         context,
+    int32_t*                      statusHasChanged,  /* out */
+    const char*                   resourceId,  /* in */
+    OrthancPluginStableStatus     stableStatus /* in */)
+  {
+    _OrthancPluginSetStableStatus params;
+    params.resourceId = resourceId;
+    params.stableStatus= stableStatus;
+    params.statusHasChanged = statusHasChanged;
+
+    return context->InvokeService(context, _OrthancPluginService_SetStableStatus, &params);
+  }
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Fri Jun 27 15:00:33 2025 +0200
@@ -55,6 +55,7 @@
   int32   compression_type = 5;  // opaque "CompressionType" in Orthanc
   uint64  compressed_size = 6;
   string  compressed_hash = 7;
+  bytes   custom_data = 8;       // New in 1.12.8
 }
 
 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,9 @@
     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.8
+    bool supports_queues = 11;            // New in Orthanc 1.12.8
+    bool has_attachment_custom_data = 12; // New in Orthanc 1.12.8
   }
 }
 
@@ -321,6 +330,16 @@
   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.8
+  OPERATION_DELETE_KEY_VALUE = 54;            // New in Orthanc 1.12.8
+  OPERATION_GET_KEY_VALUE = 55;               // New in Orthanc 1.12.8
+  OPERATION_LIST_KEY_VALUES = 56;             // New in Orthanc 1.12.8
+  OPERATION_ENQUEUE_VALUE = 57;               // New in Orthanc 1.12.8
+  OPERATION_DEQUEUE_VALUE = 58;               // New in Orthanc 1.12.8
+  OPERATION_GET_QUEUE_SIZE = 59;              // New in Orthanc 1.12.8
+  OPERATION_GET_ATTACHMENT_CUSTOM_DATA = 60;  // New in Orthanc 1.12.8
+  OPERATION_SET_ATTACHMENT_CUSTOM_DATA = 61;  // New in Orthanc 1.12.8
+
 }
 
 message Rollback {
@@ -974,6 +993,108 @@
   }
 }
 
+message StoreKeyValue {
+  message Request {
+    string store_id = 1;
+    string key = 2;
+    bytes value = 3;
+  }
+
+  message Response {
+  }
+}
+
+message DeleteKeyValue {
+  message Request {
+    string store_id = 1;
+    string key = 2;
+  }
+
+  message Response {
+  }
+}
+
+message GetKeyValue {
+  message Request {
+    string store_id = 1;
+    string key = 2;
+  }
+
+  message Response {
+    bool found = 1;
+    bytes value = 2;
+  }
+}
+
+message ListKeysValues {
+  message Request {
+    string store_id = 1;
+    bool from_first = 2;
+    string from_key = 3;  // Only meaningful if "from_first == false"
+    uint64 limit = 4;
+  }
+
+  message Response {
+    message KeyValue {
+      string key = 1;
+      bytes value = 2;
+    }
+    repeated KeyValue keys_values = 1;
+  }
+}
+
+message EnqueueValue {
+  message Request {
+    string queue_id = 1;
+    bytes value = 2;
+  }
+
+  message Response {
+  }
+}
+
+message DequeueValue {
+  message Request {
+    string queue_id = 1;
+    QueueOrigin origin = 2;
+  }
+
+  message Response {
+    bool found = 1;
+    bytes value = 2;
+  }
+}
+
+message GetQueueSize {
+  message Request {
+    string queue_id = 1;
+  }
+
+  message Response {
+    uint64 size = 1;
+  }
+}
+
+message GetAttachmentCustomData {
+  message Request {
+    string uuid = 1;
+  }
+
+  message Response {
+    bytes custom_data = 1;
+  }
+}
+
+message SetAttachmentCustomData {
+  message Request {
+    string uuid = 1;
+    bytes custom_data = 2;
+  }
+
+  message Response {
+  }
+}
+
 message TransactionRequest {
   sfixed64              transaction = 1;
   TransactionOperation  operation = 2;
@@ -1031,6 +1152,15 @@
   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;
+  ListKeysValues.Request                  list_keys_values = 156;
+  EnqueueValue.Request                    enqueue_value = 157;
+  DequeueValue.Request                    dequeue_value = 158;
+  GetQueueSize.Request                    get_queue_size = 159;
+  GetAttachmentCustomData.Request         get_attachment_custom_data = 160;
+  SetAttachmentCustomData.Request         set_attachment_custom_data = 161;
 }
 
 message TransactionResponse {
@@ -1087,6 +1217,15 @@
   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;
+  ListKeysValues.Response                  list_keys_values = 156;
+  EnqueueValue.Response                    enqueue_value = 157;
+  DequeueValue.Response                    dequeue_value = 158;
+  GetQueueSize.Response                    get_queue_size = 159;
+  GetAttachmentCustomData.Response         get_attachment_custom_data = 160;
+  SetAttachmentCustomData.Response         set_attachment_custom_data = 161;
 }
 
 enum RequestType {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/CMakeLists.txt	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,88 @@
+# 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/>.
+
+
+cmake_minimum_required(VERSION 2.8...4.0)
+cmake_policy(SET CMP0058 NEW)
+
+project(AdoptDicomInstance)
+
+SET(PLUGIN_NAME "sample-adopt" CACHE STRING "Name of the plugin")
+SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin")
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake)
+
+set(ENABLE_SQLITE OFF)
+set(ENABLE_MODULE_IMAGES OFF)
+set(ENABLE_MODULE_JOBS OFF)
+set(ENABLE_MODULE_DICOM OFF)
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+include(${CMAKE_CURRENT_LIST_DIR}/../Common/OrthancPluginsExports.cmake)
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  execute_process(
+    COMMAND
+    ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/WindowsResources.py
+    ${PLUGIN_VERSION} AdoptDicomInstance AdoptDicomInstance.dll "Sample Orthanc plugin illustrating how to adopt DICOM instances"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/AdoptDicomInstance.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  list(APPEND ADDITIONAL_RESOURCES ${AUTOGENERATED_DIR}/AdoptDicomInstance.rc)
+endif()
+
+add_definitions(
+  -DHAS_ORTHANC_EXCEPTION=1
+  -DPLUGIN_NAME="${PLUGIN_NAME}"
+  -DPLUGIN_VERSION="${PLUGIN_VERSION}"
+  -DORTHANC_ENABLE_LOGGING=1
+  -DORTHANC_ENABLE_PLUGINS=1
+  )
+
+include_directories(
+  ${CMAKE_SOURCE_DIR}/../../Include/
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/
+  )
+
+add_library(AdoptDicomInstance SHARED
+  ${ADDITIONAL_RESOURCES}
+  ${ORTHANC_CORE_SOURCES}
+  ${CMAKE_SOURCE_DIR}/Plugin.cpp
+  ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp
+  )
+
+DefineSourceBasenameForTarget(AdoptDicomInstance)
+
+set_target_properties(
+  AdoptDicomInstance PROPERTIES
+  VERSION ${PLUGIN_VERSION}
+  SOVERSION ${PLUGIN_VERSION}
+  )
+
+install(
+  TARGETS AdoptDicomInstance
+  DESTINATION .
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/Plugin.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,266 @@
+/**
+ * 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/>.
+ **/
+
+
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/SystemToolbox.h"
+#include "../../../../OrthancFramework/Sources/Toolbox.h"
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <boost/filesystem.hpp>
+
+
+static boost::filesystem::path storageDirectory_;
+
+
+static std::string GetStorageDirectoryPath(const char* uuid)
+{
+  if (uuid == NULL)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+  }
+  else
+  {
+    return (storageDirectory_ / std::string(uuid)).string();
+  }
+}
+
+
+#define CATCH_EXCEPTIONS(errorValue)                    \
+  catch (Orthanc::OrthancException& e)                  \
+  {                                                     \
+    LOG(ERROR) << "Orthanc exception: " << e.What();    \
+    return errorValue;                                  \
+  }                                                     \
+  catch (std::runtime_error& e)                         \
+  {                                                     \
+    LOG(ERROR) << "Native exception: " << e.what();     \
+    return errorValue;                                  \
+  }                                                     \
+  catch (...)                                           \
+  {                                                     \
+    return errorValue;                                  \
+  }
+
+
+OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData,
+                                     const char* uuid,
+                                     const void* content,
+                                     uint64_t size,
+                                     OrthancPluginContentType type,
+                                     OrthancPluginCompressionType compressionType,
+                                     const OrthancPluginDicomInstance* dicomInstance)
+{
+  try
+  {
+    Json::Value info;
+    info["IsAdopted"] = false;
+
+    OrthancPlugins::MemoryBuffer buffer;
+    buffer.Assign(info.toStyledString());
+    *customData = buffer.Release();
+
+    const std::string path = GetStorageDirectoryPath(uuid);
+    LOG(WARNING) << "Creating non-adopted file: " << path;
+    Orthanc::SystemToolbox::WriteFile(content, size, path);
+
+    return OrthancPluginErrorCode_Success;
+  }
+  CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin);
+}
+
+
+OrthancPluginErrorCode StorageReadRange(OrthancPluginMemoryBuffer64* target,
+                                        const char* uuid,
+                                        OrthancPluginContentType type,
+                                        uint64_t rangeStart,
+                                        const void* customData,
+                                        uint32_t customDataSize)
+{
+  try
+  {
+    Json::Value info;
+    if (!Orthanc::Toolbox::ReadJson(info, customData, customDataSize))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    std::string path;
+
+    if (info["IsAdopted"].asBool())
+    {
+      path = info["AdoptedPath"].asString();
+      LOG(WARNING) << "Reading adopted file from: " << path;
+    }
+    else
+    {
+      path = GetStorageDirectoryPath(uuid);
+      LOG(WARNING) << "Reading non-adopted file from: " << path;
+    }
+
+    std::string range;
+    Orthanc::SystemToolbox::ReadFileRange(range, path, rangeStart, rangeStart + target->size, true);
+
+    assert(range.size() == target->size);
+
+    if (target->size != 0)
+    {
+      memcpy(target->data, range.c_str(), target->size);
+    }
+
+    return OrthancPluginErrorCode_Success;
+  }
+  CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin);
+}
+
+
+OrthancPluginErrorCode StorageRemove(const char* uuid,
+                                     OrthancPluginContentType type,
+                                     const void* customData,
+                                     uint32_t customDataSize)
+{
+  try
+  {
+    Json::Value info;
+    if (!Orthanc::Toolbox::ReadJson(info, customData, customDataSize))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    // Only remove non-adopted files (i.e., whose for which Orthanc has the ownership)
+    if (info["IsAdopted"].asBool())
+    {
+      LOG(WARNING) << "Don't removing adopted file: " << info["AdoptedPath"].asString();
+    }
+    else
+    {
+      const std::string path = GetStorageDirectoryPath(uuid);
+      LOG(WARNING) << "Removing non-adopted file from: " << path;
+      Orthanc::SystemToolbox::RemoveFile(path);
+    }
+
+    return OrthancPluginErrorCode_Success;
+  }
+  CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin);
+}
+
+
+OrthancPluginErrorCode Adopt(OrthancPluginRestOutput* output,
+                             const char* url,
+                             const OrthancPluginHttpRequest* request)
+{
+  try
+  {
+    if (request->method != OrthancPluginHttpMethod_Post)
+    {
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
+      return OrthancPluginErrorCode_Success;
+    }
+    else
+    {
+      const std::string path(reinterpret_cast<const char*>(request->body), request->bodySize);
+      LOG(WARNING) << "Adopting DICOM instance from path: " << path;
+
+      std::string dicom;
+      Orthanc::SystemToolbox::ReadFile(dicom, path);
+
+      Json::Value info;
+      info["IsAdopted"] = true;
+      info["AdoptedPath"] = path;
+
+      const std::string customData = info.toStyledString();
+
+      OrthancPluginStoreStatus status;
+      OrthancPlugins::MemoryBuffer instanceId, attachmentUuid;
+
+      OrthancPluginErrorCode code = OrthancPluginAdoptDicomInstance(
+        OrthancPlugins::GetGlobalContext(), *instanceId, *attachmentUuid, &status,
+        dicom.empty() ? NULL : dicom.c_str(), dicom.size(),
+        customData.empty() ? NULL : customData.c_str(), customData.size());
+
+      if (code == OrthancPluginErrorCode_Success)
+      {
+        const std::string answer = "OK\n";
+        OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, answer.c_str(), answer.size(), "text/plain");
+        return OrthancPluginErrorCode_Success;
+      }
+      else
+      {
+        return code;
+      }
+    }
+  }
+  CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin);
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    OrthancPlugins::SetGlobalContext(context, PLUGIN_NAME);
+    Orthanc::Logging::InitializePluginContext(context, PLUGIN_NAME);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(context) == 0)
+    {
+      char info[1024];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              context->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(context, info);
+      return -1;
+    }
+
+    OrthancPluginSetDescription2(context, PLUGIN_NAME, "Sample plugin illustrating the adoption of DICOM instances.");
+    OrthancPluginRegisterStorageArea3(context, StorageCreate, StorageReadRange, StorageRemove);
+
+    try
+    {
+      OrthancPlugins::OrthancConfiguration config;
+      storageDirectory_ = config.GetStringValue("StorageDirectory", "OrthancStorage");
+
+      Orthanc::SystemToolbox::MakeDirectory(storageDirectory_.string());
+
+      OrthancPluginRegisterRestCallback(context, "/adopt", Adopt);
+    }
+    CATCH_EXCEPTIONS(-1)
+
+    return 0;
+  }
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return PLUGIN_NAME;
+  }
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/README	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,13 @@
+This sample plugin illustrates how to use the
+"OrthancPluginAdoptDicomInstance()" primitive in the Orthanc SDK.
+
+The plugin replaces the built-in storage area of Orthanc, by a flat
+directory "./OrthancStorage" containing the attachments.
+
+DICOM instances can then be adopted by typing:
+
+$ curl http://localhost:8042/adopt -d /tmp/sample.dcm
+
+An adopted DICOM instance is not copied inside the "OrthancStorage"
+folder, but is read from its original location (in the example above,
+from "/tmp/sample.dcm").
--- a/OrthancServer/Plugins/Samples/Basic/Plugin.c	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c	Fri Jun 27 15:00:33 2025 +0200
@@ -226,6 +226,44 @@
 }
 
 
+
+OrthancPluginErrorCode CallbackStabilizeStudy(OrthancPluginRestOutput* output,
+                                              const char* url,
+                                              const OrthancPluginHttpRequest* request)
+{
+
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "POST");
+  }
+  else
+  {
+    const char* studyId = request->groups[0];
+    int32_t statusHasChanged = 0;
+
+    if (strcmp(request->groups[1], "stabilize") == 0)
+    {
+      OrthancPluginSetStableStatus(context, &statusHasChanged, studyId, OrthancPluginStableStatus_Stable);
+    }
+    else
+    {
+      OrthancPluginSetStableStatus(context, &statusHasChanged, studyId, OrthancPluginStableStatus_Unstable);
+    }
+
+    if (statusHasChanged)
+    {
+      OrthancPluginAnswerBuffer(context, output, "CHANGED\n", 8, "text/plain");
+    }
+    else
+    {
+      OrthancPluginAnswerBuffer(context, output, "UNCHANGED\n", 10, "text/plain");
+    }
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
 OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output,
                                            const char* url,
                                            const OrthancPluginHttpRequest* request)
@@ -572,6 +610,7 @@
   OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5);
   OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom);
   OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/dicom-web", CallbackDicomWeb);
+  OrthancPluginRegisterRestCallback(context, "/studies/([^/]+)/(stabilize|unstabilize)", CallbackStabilizeStudy);
 
   OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback);
   OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -220,28 +220,6 @@
   }
 
 
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-  MemoryBuffer::MemoryBuffer(const void* buffer,
-                             size_t size)
-  {
-    uint32_t s = static_cast<uint32_t>(size);
-    if (static_cast<size_t>(s) != size)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
-    }
-    else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) !=
-             OrthancPluginErrorCode_Success)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
-    }
-    else
-    {
-      memcpy(buffer_.data, buffer, size);
-    }
-  }
-#endif
-
-
   void MemoryBuffer::Clear()
   {
     if (buffer_.data != NULL)
@@ -253,6 +231,41 @@
   }
 
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  void MemoryBuffer::Assign(const void* buffer,
+                            size_t size)
+  {
+    uint32_t s = static_cast<uint32_t>(size);
+    if (static_cast<size_t>(s) != size)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+
+    Clear();
+
+    if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) !=
+        OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+    else
+    {
+      if (size > 0)
+      {
+        memcpy(buffer_.data, buffer, size);
+      }
+    }
+  }
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  void MemoryBuffer::Assign(const std::string& s)
+  {
+    Assign(s.empty() ? NULL : s.c_str(), s.size());
+  }
+#endif
+
+
   void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other)
   {
     Clear();
@@ -673,7 +686,7 @@
   {
     OrthancString str;
     str.Assign(OrthancPluginDicomBufferToJson
-               (GetGlobalContext(), GetData(), GetSize(), format, flags, maxStringLength));
+               (GetGlobalContext(), reinterpret_cast<const char*>(GetData()), GetSize(), format, flags, maxStringLength));
     str.ToJson(target);
   }
 
@@ -1566,7 +1579,7 @@
     {
       if (!answer.IsEmpty())
       {
-        result.assign(answer.GetData(), answer.GetSize());
+        result.assign(reinterpret_cast<const char*>(answer.GetData()), answer.GetSize());
       }
       return true;
     }
@@ -4347,4 +4360,209 @@
     }
   }
 #endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator  *iterator) :
+    iterator_(iterator)
+  {
+    if (iterator_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  KeyValueStore::Iterator::~Iterator()
+  {
+    OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_);
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  bool KeyValueStore::Iterator::Next()
+  {
+    uint8_t done;
+    OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorNext(OrthancPlugins::GetGlobalContext(), &done, iterator_);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else
+    {
+      return (done != 0);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  std::string KeyValueStore::Iterator::GetKey() const
+  {
+    const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_);
+    if (s == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+    else
+    {
+      return s;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  void KeyValueStore::Iterator::GetValue(std::string& value) const
+  {
+    OrthancPlugins::MemoryBuffer valueBuffer;
+    OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorGetValue(OrthancPlugins::GetGlobalContext(), *valueBuffer, iterator_);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else
+    {
+      valueBuffer.ToString(value);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  void KeyValueStore::Store(const std::string& key,
+                            const void* value,
+                            size_t valueSize)
+  {
+    if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+
+    OrthancPluginErrorCode code = OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(), storeId_.c_str(),
+                                                             key.c_str(), value, static_cast<uint32_t>(valueSize));
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  bool KeyValueStore::GetValue(std::string& value,
+                               const std::string& key)
+  {
+    uint8_t found = false;
+    OrthancPlugins::MemoryBuffer valueBuffer;
+    OrthancPluginErrorCode code = OrthancPluginGetKeyValue(OrthancPlugins::GetGlobalContext(), &found,
+                                                           *valueBuffer, storeId_.c_str(), key.c_str());
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else if (found)
+    {
+      valueBuffer.ToString(value);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  void KeyValueStore::DeleteKey(const std::string& key)
+  {
+    OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(),
+                                                              storeId_.c_str(), key.c_str());
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  KeyValueStore::Iterator* KeyValueStore::CreateIterator()
+  {
+    return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str()));
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  void Queue::Enqueue(const void* value,
+                      size_t valueSize)
+  {
+    if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+
+    OrthancPluginErrorCode code = OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(),
+                                                            queueId_.c_str(), value, static_cast<uint32_t>(valueSize));
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  bool Queue::DequeueInternal(std::string& value,
+                              OrthancPluginQueueOrigin origin)
+  {
+    uint8_t found = false;
+    OrthancPlugins::MemoryBuffer valueBuffer;
+
+    OrthancPluginErrorCode code = OrthancPluginDequeueValue(OrthancPlugins::GetGlobalContext(), &found,
+                                                            *valueBuffer, queueId_.c_str(), origin);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else if (found)
+    {
+      valueBuffer.ToString(value);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  uint64_t Queue::GetSize()
+  {
+    uint64_t size = 0;
+    OrthancPluginErrorCode code = OrthancPluginGetQueueSize(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), &size);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else
+    {
+      return size;
+    }
+  }
+#endif
 }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Fri Jun 27 15:00:33 2025 +0200
@@ -134,6 +134,14 @@
 #  define HAS_ORTHANC_PLUGIN_LOG_MESSAGE  0
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+#  define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES  1
+#  define HAS_ORTHANC_PLUGIN_QUEUES            1
+#else
+#  define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES  0
+#  define HAS_ORTHANC_PLUGIN_QUEUES            0
+#endif
+
 
 // Macro to tag a function as having been deprecated
 #if (__cplusplus >= 201402L)  // C++14
@@ -203,13 +211,6 @@
   public:
     MemoryBuffer();
 
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    // This constructor makes a copy of the given buffer in the memory
-    // handled by the Orthanc core
-    MemoryBuffer(const void* buffer,
-                 size_t size);
-#endif
-
     ~MemoryBuffer()
     {
       Clear();
@@ -220,6 +221,16 @@
       return &buffer_;
     }
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    // Copy of the given buffer into the memory managed by the Orthanc core
+    void Assign(const void* buffer,
+                size_t size);
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    void Assign(const std::string& s);
+#endif
+
     // This transfers ownership from "other" to "this"
     void Assign(OrthancPluginMemoryBuffer& other);
 
@@ -227,11 +238,11 @@
 
     OrthancPluginMemoryBuffer Release();
 
-    const char* GetData() const
+    const void* GetData() const
     {
       if (buffer_.size > 0)
       {
-        return reinterpret_cast<const char*>(buffer_.data);
+        return buffer_.data;
       }
       else
       {
@@ -1618,4 +1629,101 @@
     bool GetAnswerJson(Json::Value& output) const;
   };
 #endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  class KeyValueStore : public boost::noncopyable
+  {
+  public:
+    class Iterator : public boost::noncopyable
+    {
+    private:
+      OrthancPluginKeysValuesIterator  *iterator_;
+
+    public:
+      Iterator(OrthancPluginKeysValuesIterator  *iterator);
+
+      ~Iterator();
+
+      bool Next();
+
+      std::string GetKey() const;
+
+      void GetValue(std::string& target) const;
+    };
+
+  private:
+    std::string storeId_;
+
+  public:
+    explicit KeyValueStore(const std::string& storeId) :
+      storeId_(storeId)
+    {
+    }
+
+    const std::string& GetStoreId() const
+    {
+      return storeId_;
+    }
+
+    void Store(const std::string& key,
+               const void* value,
+               size_t valueSize);
+
+    void Store(const std::string& key,
+               const std::string& value)
+    {
+      Store(key, value.empty() ? NULL : value.c_str(), value.size());
+    }
+
+    bool GetValue(std::string& value,
+                  const std::string& key);
+
+    void DeleteKey(const std::string& key);
+
+    Iterator* CreateIterator();
+  };
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  class Queue : public boost::noncopyable
+  {
+  private:
+    std::string queueId_;
+
+    bool DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin);
+
+  public:
+    explicit Queue(const std::string& queueId) :
+      queueId_(queueId)
+    {
+    }
+
+    const std::string& GetQueueId() const
+    {
+      return queueId_;
+    }
+
+    void Enqueue(const void* value,
+                 size_t valueSize);
+
+    void Enqueue(const std::string& value)
+    {
+      Enqueue(value.empty() ? NULL : value.c_str(), value.size());
+    }
+
+    bool DequeueBack(std::string& value)
+    {
+      return DequeueInternal(value, OrthancPluginQueueOrigin_Back);
+    }
+
+    bool DequeueFront(std::string& value)
+    {
+      return DequeueInternal(value, OrthancPluginQueueOrigin_Front);
+    }
+
+    uint64_t GetSize();
+  };
+#endif
 }
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Fri Jun 27 15:00:33 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/Plugins/Samples/Housekeeper/Plugin.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -41,6 +41,7 @@
 
 static int globalPropertyId_ = 0;
 static bool force_ = false;
+static bool forceReconstructFiles_ = false;
 static unsigned int throttleDelay_ = 0;
 static std::unique_ptr<boost::thread> workerThread_;
 static bool workerThreadShouldStop_ = false;
@@ -568,10 +569,10 @@
           {
             Json::Value result;
 
-            if (needsReconstruct || needsReingest ||force_)
+            if (needsReconstruct || needsReingest || force_)
             {
               Json::Value request;
-              if (needsReingest)
+              if (needsReingest || forceReconstructFiles_)
               {
                 request["ReconstructFiles"] = true;
               }
@@ -856,6 +857,13 @@
             // any changes in configuration
             "Force": false,
 
+            // New in 1.12.9
+            // If "Force" is set to true, forces the "ReconstructFiles"
+            // option when reconstructing resources even if the plugin 
+            // did not detect any changes in the configuration that 
+            // should trigger a Reconstruct.
+            "ForceReconstructFiles": false,
+
             // Delay (in seconds) between reconstruction of 2 studies
             // This avoids overloading Orthanc with the housekeeping
             // process and leaves room for other operations.
@@ -898,6 +906,7 @@
 
       globalPropertyId_ = housekeeper.GetIntegerValue("GlobalPropertyId", 1025);
       force_ = housekeeper.GetBooleanValue("Force", false);
+      forceReconstructFiles_ = housekeeper.GetBooleanValue("ForceReconstructFiles", false);
       throttleDelay_ = housekeeper.GetUnsignedIntegerValue("ThrottleDelay", 5);      
 
       if (housekeeper.GetJson().isMember("Triggers"))
--- a/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -221,7 +221,7 @@
       OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(),
                                  output, "Last-Modified", t.c_str());
 
-      Answer(output, content.GetData(), content.GetSize(), mime);
+      Answer(output, reinterpret_cast<const char*>(content.GetData()), content.GetSize(), mime);
     }
   }
 }
--- a/OrthancServer/Resources/RunCppCheck-2.17.0.sh	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Resources/RunCppCheck-2.17.0.sh	Fri Jun 27 15:00:33 2025 +0200
@@ -106,5 +106,6 @@
             ../../OrthancServer/Plugins/Samples/Housekeeper \
             ../../OrthancServer/Plugins/Samples/ModalityWorklists \
             ../../OrthancServer/Plugins/Samples/MultitenantDicom \
+            ../../OrthancServer/Plugins/Samples/AdoptDicomInstance \
             \
             2>&1
--- a/OrthancServer/Resources/RunCppCheck.sh	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Resources/RunCppCheck.sh	Fri Jun 27 15:00:33 2025 +0200
@@ -102,5 +102,6 @@
             ../../OrthancServer/Plugins/Samples/Housekeeper \
             ../../OrthancServer/Plugins/Samples/ModalityWorklists \
             ../../OrthancServer/Plugins/Samples/MultitenantDicom \
+            ../../OrthancServer/Plugins/Samples/AdoptDicomInstance \
             \
             2>&1
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Jun 27 15:00:33 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 void GetAttachmentCustomData(std::string& customData,
+                                           const std::string& attachmentUuid) = 0;
+
+      virtual void SetAttachmentCustomData(const std::string& attachmentUuid,
+                                           const void* customData,
+                                           size_t customDataSize) = 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,42 @@
                                       int64_t to,
                                       uint32_t limit,
                                       const std::set<ChangeType>& filterType) = 0;
+
+      // New in Orthanc 1.12.8
+      virtual void StoreKeyValue(const std::string& storeId,
+                                 const std::string& key,
+                                 const void* value,
+                                 size_t valueSize) = 0;
+
+      // New in Orthanc 1.12.8
+      virtual void DeleteKeyValue(const std::string& storeId,
+                                  const std::string& key) = 0;
+
+      // New in Orthanc 1.12.8
+      virtual bool GetKeyValue(std::string& value,
+                               const std::string& storeId,
+                               const std::string& key) = 0;
+
+      // New in Orthanc 1.12.8
+      virtual void ListKeysValues(std::list<std::string>& keys /* out */,
+                                  std::list<std::string>& values /* out */,
+                                  const std::string& storeId,
+                                  bool first,
+                                  const std::string& from /* exclusive bound, only used if "first == false" */,
+                                  uint64_t limit /* maximum number of elements */) = 0;
+
+      // New in Orthanc 1.12.8
+      virtual void EnqueueValue(const std::string& queueId,
+                                const void* value,
+                                size_t valueSize) = 0;
+
+      // New in Orthanc 1.12.8
+      virtual bool DequeueValue(std::string& value,
+                                const std::string& queueId,
+                                QueueOrigin origin) = 0;
+
+      // New in Orthanc 1.12.8, for statistics only
+      virtual uint64_t GetQueueSize(const std::string& queueId) = 0;
     };
 
 
@@ -456,7 +535,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/InstallDeletedFiles.sql	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,52 @@
+-- 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 DeletedFiles(
+       uuid TEXT NOT NULL,        -- 0
+       customData BLOB            -- 1
+);
+
+-- We need to use another AttachedFileDeleted trigger than the legacy one in "Upgrade4To5.sql".
+--
+-- 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 preserve 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.
+
+DROP TRIGGER IF EXISTS 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;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/InstallKeyValueStoresAndQueues.sql	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,35 @@
+-- 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 BLOB NOT NULL,
+       PRIMARY KEY(storeId, key)  -- Prevents duplicates
+       );
+
+CREATE TABLE Queues (
+       id INTEGER PRIMARY KEY AUTOINCREMENT,
+       queueId TEXT NOT NULL,
+       value BLOB NOT NULL
+);
+
+CREATE INDEX QueuesIndex ON Queues (queueId, id);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql	Fri Jun 27 15:00:33 2025 +0200
@@ -0,0 +1,35 @@
+-- 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/>.
+
+
+--
+-- 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 BLOB;
+
+-- Record that this upgrade has been performed
+
+INSERT INTO GlobalProperties VALUES (7, 1);  -- GlobalProperty_SQLiteHasCustomDataAndRevision
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/Database/PrepareDatabase.sql	Fri Jun 27 15:00:33 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.8 (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.8 (added in InstallRevisionAndCustomData.sql)
+       customData BLOB,       -- New in Orthanc 1.12.8 (added in InstallDeletedFiles.sql)
        PRIMARY KEY(id, fileType)
        );              
 
@@ -95,22 +98,12 @@
        patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
        );
 
--- New in Orthanc 1.12.0
-CREATE TABLE Labels(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       label TEXT NOT NULL,
-       PRIMARY KEY(id, label)  -- Prevents duplicates
-       );
-
 CREATE INDEX ChildrenIndex ON Resources(parentId);
 CREATE INDEX PublicIndex ON Resources(publicId);
 CREATE INDEX ResourceTypeIndex ON Resources(resourceType);
 CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
 
 CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id);
--- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up
--- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
--- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY);
 
 -- The 3 following indexes were added in Orthanc 0.8.5 (database v5)
 CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
@@ -119,18 +112,6 @@
 
 CREATE INDEX ChangesIndex ON Changes(internalId);
 
--- New in Orthanc 1.12.0
-CREATE INDEX LabelsIndex1 ON Labels(id);
-CREATE INDEX LabelsIndex2 ON Labels(label);  -- This index allows efficient lookups
-
-CREATE TRIGGER AttachedFileDeleted
-AFTER DELETE ON AttachedFiles
-BEGIN
-  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
-                           old.compressionType, old.compressedSize,
-                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
-                           old.uncompressedMD5, old.compressedMD5);
-END;
 
 CREATE TRIGGER ResourceDeleted
 AFTER DELETE ON Resources
@@ -156,6 +137,27 @@
 END;
 
 
+-- new in Orthanc 1.5.1 -------------------------- equivalent to InstallTrackAttachmentsSize.sql
+${INSTALL_TRACK_ATTACHMENTS_SIZE}
+
+
+-- new in Orthanc 1.12.0 ------------------------- equivalent to InstallLabelsTable.sql
+${INSTALL_LABELS_TABLE}
+
+
+-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallDeletedFiles.sql
+${INSTALL_DELETED_FILES}
+
+
+-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallKeyValueStoresAndQueues.sql
+${INSTALL_KEY_VALUE_STORES_AND_QUEUES}
+
+
+-- Track the fact that the "revision" column exists in the "Metadata" and "AttachedFiles"
+-- tables, and that the "customData" column exists in the "AttachedFiles" table
+INSERT INTO GlobalProperties VALUES (7, 1);  -- GlobalProperty_SQLiteHasCustomDataAndRevision
+
+
 -- Set the version of the database schema
 -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
 INSERT INTO GlobalProperties VALUES (1, "6");
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -39,8 +39,10 @@
 #include <OrthancServerResources.h>
 
 #include <stdio.h>
+#include <boost/algorithm/string/replace.hpp>
 #include <boost/lexical_cast.hpp>
 
+
 namespace Orthanc
 {  
   static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec)
@@ -384,19 +386,22 @@
       }
     }
 
-    boost::mutex::scoped_lock  lock_;
+    boost::recursive_mutex::scoped_lock  lock_;
     IDatabaseListener&         listener_;
     SignalRemainingAncestor&   signalRemainingAncestor_;
+    bool                       hasFastTotalSize_;
 
   public:
-    TransactionBase(boost::mutex& mutex,
+    TransactionBase(boost::recursive_mutex& mutex,
                     SQLite::Connection& db,
                     IDatabaseListener& listener,
-                    SignalRemainingAncestor& signalRemainingAncestor) :
+                    SignalRemainingAncestor& signalRemainingAncestor,
+                    bool hasFastTotalSize) :
       UnitTestsTransaction(db),
       lock_(mutex),
       listener_(listener),
-      signalRemainingAncestor_(signalRemainingAncestor)
+      signalRemainingAncestor_(signalRemainingAncestor),
+      hasFastTotalSize_(hasFastTotalSize)
     {
     }
 
@@ -410,8 +415,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 +426,11 @@
       s.BindInt(5, attachment.GetCompressionType());
       s.BindString(6, attachment.GetUncompressedMD5());
       s.BindString(7, attachment.GetCompressedMD5());
+      s.BindInt(8, revision);
+      s.BindBlob(9, attachment.GetCustomData());
       s.Run();
     }
 
-
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
                                       const DatabaseDicomTagConstraints& lookup,
@@ -473,10 +480,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
@@ -525,6 +534,19 @@
     }
 
 
+    static void ReadCustomData(FileInfo& info,
+                               SQLite::Statement& statement,
+                               int column)
+    {
+      std::string customData;
+      if (!statement.ColumnIsNull(column) &&
+          statement.ColumnBlobAsString(column, &customData))
+      {
+        info.SwapCustomData(customData);
+      }
+    }
+
+
     virtual void ExecuteFind(FindResponse& response,
                              const FindRequest& request,
                              const Capabilities& capabilities) ORTHANC_OVERRIDE
@@ -588,10 +610,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 +629,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 +644,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 +660,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 +681,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 +701,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 +721,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 +742,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 +764,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 +785,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 +808,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 +830,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 +854,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 +875,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 +897,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 +918,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 +939,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 +963,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 +984,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 +1005,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 +1026,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 +1047,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 +1068,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 +1111,21 @@
           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));
+            ReadCustomData(file, s, 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 +1133,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 +1142,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 +1151,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 +1159,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 +1167,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 +1199,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 +1240,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 +1266,18 @@
           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));
+            ReadCustomData(file, s, C6_STRING_4);
+
             res.AddOneInstanceAttachment(file);
           }; break;
 
@@ -1312,6 +1384,32 @@
       }
     }
 
+    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())
+      { 
+        if (s.ColumnIsNull(0) ||
+            !s.ColumnBlobAsString(0, &customData))
+        {
+          customData.clear();
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+    }
 
     virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
                                 int64_t id) ORTHANC_OVERRIDE
@@ -1597,23 +1695,39 @@
 
     virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE
     {
-      // Old SQL query that was used in Orthanc <= 1.5.0:
-      // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
-
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0");
-      s.Run();
-      return static_cast<uint64_t>(s.ColumnInt64(0));
+      std::unique_ptr<SQLite::Statement> statement;
+
+      if (hasFastTotalSize_)
+      {
+        statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0"));
+      }
+      else
+      {
+        // Old SQL query that was used in Orthanc <= 1.5.0:
+        statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"));
+      }
+
+      statement->Run();
+      return static_cast<uint64_t>(statement->ColumnInt64(0));
     }
 
     
     virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE
     {
-      // Old SQL query that was used in Orthanc <= 1.5.0:
-      // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
-
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1");
-      s.Run();
-      return static_cast<uint64_t>(s.ColumnInt64(0));
+      std::unique_ptr<SQLite::Statement> statement;
+
+      if (hasFastTotalSize_)
+      {
+        statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1"));
+      }
+      else
+      {
+        // Old SQL query that was used in Orthanc <= 1.5.0:
+        statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"));
+      }
+
+      statement->Run();
+      return static_cast<uint64_t>(statement->ColumnInt64(0));
     }
 
 
@@ -1687,7 +1801,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);
 
@@ -1704,11 +1818,46 @@
                               static_cast<CompressionType>(s.ColumnInt(2)),
                               s.ColumnInt64(3),
                               s.ColumnString(5));
-        revision = 0;   // TODO - REVISIONS
+        ReadCustomData(attachment, s, 7);
+        revision = s.ColumnInt(6);
         return true;
       }
     }
 
+    virtual void GetAttachmentCustomData(std::string& customData,
+                                         const std::string& attachmentUuid) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT customData FROM AttachedFiles WHERE uuid=?");
+      s.BindString(0, attachmentUuid);
+
+      if (!s.Step())
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+      else
+      {
+        if (s.ColumnIsNull(0))
+        {
+          customData.clear();
+        }
+        else if (!s.ColumnBlobAsString(0, &customData))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+
+    virtual void SetAttachmentCustomData(const std::string& attachmentUuid,
+                                         const void* customData,
+                                         size_t customDataSize) ORTHANC_OVERRIDE
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "UPDATE AttachedFiles SET customData=? WHERE uuid=?");
+      s.BindBlob(0, customData, customDataSize);
+      s.BindString(1, attachmentUuid);
+      s.Run();
+    }
 
     virtual bool LookupGlobalProperty(std::string& target,
                                       GlobalProperty property,
@@ -1739,7 +1888,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 +1899,7 @@
       else
       {
         target = s.ColumnString(0);
-        revision = 0;   // TODO - REVISIONS
+        revision = s.ColumnInt(1);
         return true;
       }
     }
@@ -1922,11 +2071,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 +2176,170 @@
         target.insert(s.ColumnString(0));
       }
     }
+
+    virtual void StoreKeyValue(const std::string& storeId,
+                               const std::string& key,
+                               const void* value,
+                               size_t valueSize) 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.BindBlob(2, value, valueSize);
+      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
+      {
+        if (!s.ColumnBlobAsString(0, &value))
+        {
+          throw OrthancException(ErrorCode_NotEnoughMemory);
+        }
+        else
+        {
+          return true;
+        }
+      }    
+    }
+
+    // New in Orthanc 1.12.8
+    virtual void ListKeysValues(std::list<std::string>& keys /* out */,
+                                std::list<std::string>& values /* out */,
+                                const std::string& storeId,
+                                bool first,
+                                const std::string& from /* only used if "first == false" */,
+                                uint64_t limit) ORTHANC_OVERRIDE
+    {
+      int64_t actualLimit = limit;
+      if (limit == 0)
+      {
+        actualLimit = -1;  // In SQLite, "if negative, there is no upper bound on the number of rows returned"
+      }
+
+      std::unique_ptr<SQLite::Statement> statement;
+
+      if (first)
+      {
+        statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? ORDER BY key ASC LIMIT ?"));
+        statement->BindString(0, storeId);
+        statement->BindInt64(1, actualLimit);
+      }
+      else
+      {
+        statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? AND key>? ORDER BY key ASC LIMIT ?"));
+        statement->BindString(0, storeId);
+        statement->BindString(1, from);
+        statement->BindInt64(2, actualLimit);
+      }
+
+      while (statement->Step())
+      {
+        std::string value;
+        if (!statement->ColumnBlobAsString(1, &value))
+        {
+          throw OrthancException(ErrorCode_NotEnoughMemory);
+        }
+
+        keys.push_back(statement->ColumnString(0));
+        values.push_back(value);
+      }
+    }
+
+
+    // New in Orthanc 1.12.8
+    virtual void EnqueueValue(const std::string& queueId,
+                              const void* value,
+                              size_t valueSize) ORTHANC_OVERRIDE
+    {
+      if (static_cast<size_t>(static_cast<int>(valueSize)) != valueSize)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory, "Value is too large for a SQLite database");
+      }
+
+      SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                          "INSERT INTO Queues (queueId, value) VALUES (?, ?)");
+      s.BindString(0, queueId);
+      s.BindBlob(1, value, valueSize);
+      s.Run();
+    }
+
+    // New in Orthanc 1.12.8
+    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);
+
+        if (!s->ColumnBlobAsString(1, &value))
+        {
+          throw OrthancException(ErrorCode_NotEnoughMemory);
+        }
+
+        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.8
+    virtual uint64_t GetQueueSize(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();
+      return s.ColumnInt64(0);
+    }
   };
 
 
@@ -2055,6 +2368,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))
@@ -2074,8 +2392,10 @@
                       static_cast<CompressionType>(context.GetIntValue(3)),
                       static_cast<uint64_t>(context.GetInt64Value(4)),
                       compressedMD5);
+        info.SwapCustomData(customData);
 
         sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info);
+        sqlite_.activeTransaction_->DeleteDeletedFile(id);
       }
     }
   };
@@ -2120,20 +2440,40 @@
     SQLiteDatabaseWrapper&                that_;
     std::unique_ptr<SQLite::Transaction>  transaction_;
     int64_t                               initialDiskSize_;
+    bool                                  isNested_;
+
+  // Rationale for the isNested_ field: 
+  //   This was added while implementing the DelayedDeletion part of the advanced-storage plugin.
+  //   When Orthanc deletes an attachment, a SQLite transaction is created to delete the attachment from
+  //   the SQLite DB and, while the transaction is still active, the StorageRemove callback is called.
+  //   The DelayedDeleter does not delete the file directly but, instead, it queues it for deletion.
+  //   Queuing is done through the Orthanc SDK that creates a RW transaction (because it is a generic function).
+  //   Since there is already an active RW transaction, this "nested" transaction does not need to perform anything
+  //   in its Begin/Commit since this will be performed at higher level by the current activeTransaction_.
+  //   However, in case of Rollback, this nested transaction must call the top level transaction Rollback.
 
   public:
     ReadWriteTransaction(SQLiteDatabaseWrapper& that,
-                         IDatabaseListener& listener) :
-      TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_),
+                         IDatabaseListener& listener,
+                         bool hasFastTotalSize) :
+      TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_, hasFastTotalSize),
       that_(that),
-      transaction_(new SQLite::Transaction(that_.db_))
+      transaction_(new SQLite::Transaction(that_.db_)),
+      isNested_(false)
     {
       if (that_.activeTransaction_ != NULL)
       {
-        throw OrthancException(ErrorCode_InternalError);
+        if (dynamic_cast<SQLiteDatabaseWrapper::ReadWriteTransaction*>(that_.activeTransaction_) == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError, "Unable to create a nested RW transaction, the current transaction is not a RW transaction");
+        }
+        
+        isNested_ = true;
       }
-      
-      that_.activeTransaction_ = this;
+      else
+      {
+        that_.activeTransaction_ = this;
+      }
 
 #if defined(NDEBUG)
       // Release mode
@@ -2146,26 +2486,42 @@
 
     virtual ~ReadWriteTransaction()
     {
-      assert(that_.activeTransaction_ != NULL);    
-      that_.activeTransaction_ = NULL;
+      if (!isNested_)
+      {
+        assert(that_.activeTransaction_ != NULL);    
+        that_.activeTransaction_ = NULL;
+      }
     }
 
-    void Begin()
+    virtual void Begin()
     {
-      transaction_->Begin();
+      if (!isNested_)
+      {
+        transaction_->Begin();
+      }
     }
 
     virtual void Rollback() ORTHANC_OVERRIDE
     {
-      transaction_->Rollback();
+      if (isNested_)
+      {
+        that_.activeTransaction_->Rollback();
+      }
+      else
+      {
+        transaction_->Rollback();
+      }
     }
 
     virtual void Commit(int64_t fileSizeDelta /* only used in debug */) ORTHANC_OVERRIDE
     {
-      transaction_->Commit();
-
-      assert(initialDiskSize_ + fileSizeDelta >= 0 &&
-             initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(GetTotalCompressedSize()));
+      if (!isNested_)
+      {
+        transaction_->Commit();
+
+        assert(initialDiskSize_ + fileSizeDelta >= 0 &&
+              initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(GetTotalCompressedSize()));
+      }
     }
   };
 
@@ -2174,25 +2530,34 @@
   {
   private:
     SQLiteDatabaseWrapper&  that_;
+    bool                    isNested_;  // see explanation on the ReadWriteTransaction
     
   public:
     ReadOnlyTransaction(SQLiteDatabaseWrapper& that,
-                        IDatabaseListener& listener) :
-      TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_),
-      that_(that)
+                        IDatabaseListener& listener,
+                        bool hasFastTotalSize) :
+      TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_, hasFastTotalSize),
+      that_(that),
+      isNested_(false)
     {
       if (that_.activeTransaction_ != NULL)
       {
-        throw OrthancException(ErrorCode_InternalError);
+        isNested_ = true;
+        // throw OrthancException(ErrorCode_InternalError);
       }
-      
-      that_.activeTransaction_ = this;
+      else
+      {      
+        that_.activeTransaction_ = this;
+      }
     }
 
     virtual ~ReadOnlyTransaction()
     {
-      assert(that_.activeTransaction_ != NULL);    
-      that_.activeTransaction_ = NULL;
+      if (!isNested_)
+      {
+        assert(that_.activeTransaction_ != NULL);    
+        that_.activeTransaction_ = NULL;
+      }
     }
 
     virtual void Rollback() ORTHANC_OVERRIDE
@@ -2214,11 +2579,14 @@
     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);
+    dbCapabilities_.SetAttachmentCustomDataSupport(true);
     db_.Open(path);
   }
 
@@ -2228,11 +2596,14 @@
     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);
+    dbCapabilities_.SetAttachmentCustomDataSupport(true);
     db_.OpenInMemory();
   }
 
@@ -2245,10 +2616,29 @@
   }
 
 
+  static void ExecuteEmbeddedScript(SQLite::Connection& db,
+                                    ServerResources::FileResourceId resourceId)
+  {
+    std::string script;
+    ServerResources::GetFileResource(script, resourceId);
+    db.Execute(script);
+  }
+
+
+  static void InjectEmbeddedScript(std::string& sql,
+                                   const std::string& name,
+                                   ServerResources::FileResourceId resourceId)
+  {
+    std::string script;
+    ServerResources::GetFileResource(script, resourceId);
+    boost::replace_all(sql, name, script);
+  }
+
+
   void SQLiteDatabaseWrapper::Open()
   {
     {
-      boost::mutex::scoped_lock lock(mutex_);
+      boost::recursive_mutex::scoped_lock lock(mutex_);
 
       if (signalRemainingAncestor_ != NULL)
       {
@@ -2283,6 +2673,12 @@
         LOG(INFO) << "Creating the database";
         std::string query;
         ServerResources::GetFileResource(query, ServerResources::PREPARE_DATABASE);
+
+        InjectEmbeddedScript(query, "${INSTALL_TRACK_ATTACHMENTS_SIZE}", ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE);
+        InjectEmbeddedScript(query, "${INSTALL_LABELS_TABLE}", ServerResources::INSTALL_LABELS_TABLE);
+        InjectEmbeddedScript(query, "${INSTALL_DELETED_FILES}", ServerResources::INSTALL_DELETED_FILES);
+        InjectEmbeddedScript(query, "${INSTALL_KEY_VALUE_STORES_AND_QUEUES}", ServerResources::INSTALL_KEY_VALUE_STORES_AND_QUEUES);
+
         db_.Execute(query);
       }
 
@@ -2317,18 +2713,35 @@
             tmp != "1")
         {
           LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments";
-          std::string query;
-          ServerResources::GetFileResource(query, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE);
-          db_.Execute(query);
+          ExecuteEmbeddedScript(db_, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE);
         }
 
         // New in Orthanc 1.12.0
         if (!db_.DoesTableExist("Labels"))
         {
           LOG(INFO) << "Installing the \"Labels\" table";
-          std::string query;
-          ServerResources::GetFileResource(query, ServerResources::INSTALL_LABELS_TABLE);
-          db_.Execute(query);
+          ExecuteEmbeddedScript(db_, ServerResources::INSTALL_LABELS_TABLE);
+        }
+
+        // New in Orthanc 1.12.8
+        if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasRevisionAndCustomData, true /* unused in SQLite */)
+            || tmp != "1")
+        {
+          LOG(INFO) << "Upgrading SQLite schema to support revision and customData";
+          ExecuteEmbeddedScript(db_, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA);
+        }
+
+        // New in Orthanc 1.12.8
+        if (!db_.DoesTableExist("DeletedFiles"))
+        {
+          ExecuteEmbeddedScript(db_, ServerResources::INSTALL_DELETED_FILES);
+        }
+
+        // New in Orthanc 1.12.8
+        if (!db_.DoesTableExist("KeyValueStores"))
+        {
+          LOG(INFO) << "Installing the \"KeyValueStores\" and \"Queues\" tables";
+          ExecuteEmbeddedScript(db_, ServerResources::INSTALL_KEY_VALUE_STORES_AND_QUEUES);
         }
       }
 
@@ -2339,7 +2752,7 @@
 
   void SQLiteDatabaseWrapper::Close()
   {
-    boost::mutex::scoped_lock lock(mutex_);
+    boost::recursive_mutex::scoped_lock lock(mutex_);
     // close and delete the WAL when exiting properly -> the DB is stored in a single file (no more -wal and -shm files)
     db_.Execute("PRAGMA JOURNAL_MODE=DELETE;");
     db_.Close();
@@ -2358,9 +2771,9 @@
 
 
   void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion,
-                                      IStorageArea& storageArea)
+                                      IPluginStorageArea& storageArea)
   {
-    boost::mutex::scoped_lock lock(mutex_);
+    boost::recursive_mutex::scoped_lock lock(mutex_);
 
     if (targetVersion != 6)
     {
@@ -2401,33 +2814,59 @@
       VoidDatabaseListener listener;
       
       {
-        std::unique_ptr<ITransaction> transaction(StartTransaction(TransactionType_ReadWrite, listener));
-        ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Patient);
-        ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Study);
-        ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Series);
-        ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Instance);
+        ReadWriteTransaction transaction(*this, listener, false /* GetTotalSizeIsFast necessitates the table "GlobalIntegers" */);
+        transaction.Begin();
+
+        // ReconstructMaindDicomTags uses LookupAttachment that needs revision and customData.  Since we don't want to maintain a legacy version
+        // of LookupAttachment, we modify the table now)
+        LOG(INFO) << "First upgrading SQLite schema to support revision and customData to be able to reconstruct main DICOM tags";
+        std::string query;
+        ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA);
+        db_.Execute(query);
+
+        ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Patient);
+        ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Study);
+        ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Series);
+        ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Instance);
         db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" +
                     boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";");
-        transaction->Commit(0);
+        transaction.Commit(0);
       }
       
       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:
-        return new ReadOnlyTransaction(*this, listener);  // This is a no-op transaction in SQLite (thanks to mutex)
+        return new ReadOnlyTransaction(*this, listener, true);  // This is a no-op transaction in SQLite (thanks to mutex)
 
       case TransactionType_ReadWrite:
       {
         std::unique_ptr<ReadWriteTransaction> transaction;
-        transaction.reset(new ReadWriteTransaction(*this, listener));
+        transaction.reset(new ReadWriteTransaction(*this, listener, true));
         transaction->Begin();
         return transaction.release();
       }
@@ -2440,7 +2879,7 @@
   
   void SQLiteDatabaseWrapper::FlushToDisk()
   {
-    boost::mutex::scoped_lock lock(mutex_);
+    boost::recursive_mutex::scoped_lock lock(mutex_);
     db_.FlushToDisk();
   }
 
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Fri Jun 27 15:00:33 2025 +0200
@@ -27,7 +27,7 @@
 
 #include "../../../OrthancFramework/Sources/SQLite/Connection.h"
 
-#include <boost/thread/mutex.hpp>
+#include <boost/thread/recursive_mutex.hpp>
 
 namespace Orthanc
 {
@@ -47,7 +47,7 @@
     class ReadWriteTransaction;
     class LookupFormatter;
 
-    boost::mutex              mutex_;
+    boost::recursive_mutex    mutex_;
     SQLite::Connection        db_;
     TransactionBase*          activeTransaction_;
     SignalRemainingAncestor*  signalRemainingAncestor_;
@@ -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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -1394,28 +1394,36 @@
   }
 
 
-  bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type,
-                                                       const std::string& publicId)
+  bool StatelessDatabaseOperations::LookupResource(int64_t& internalId,
+                                                   ResourceType& type,
+                                                   const std::string& publicId)
   {
-    class Operations : public ReadOnlyOperationsT3<bool&, ResourceType&, const std::string&>
+    class Operations : public ReadOnlyOperationsT4<bool&, int64_t&, ResourceType&, const std::string&>
     {
     public:
       virtual void ApplyTuple(ReadOnlyTransaction& transaction,
                               const Tuple& tuple) ORTHANC_OVERRIDE
       {
         // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        int64_t id;
-        tuple.get<0>() = transaction.LookupResource(id, tuple.get<1>(), tuple.get<2>());
+        tuple.get<0>() = transaction.LookupResource(tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
       }
     };
 
     bool found;
     Operations operations;
-    operations.Apply(*this, found, type, publicId);
+    operations.Apply(*this, found, internalId, type, publicId);
     return found;
   }
 
 
+  bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type,
+                                                       const std::string& publicId)
+  {
+    int64_t internalId;
+    return LookupResource(internalId, type, publicId);
+  }
+
+
   bool StatelessDatabaseOperations::LookupParent(std::string& target,
                                                  const std::string& publicId,
                                                  ResourceType parentType)
@@ -3200,6 +3208,24 @@
     return db_.GetDatabaseCapabilities().HasFindSupport();
   }
 
+  bool StatelessDatabaseOperations::HasAttachmentCustomDataSupport()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport();
+  }
+
+  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 +3346,413 @@
       }
     }
   }
+
+  void StatelessDatabaseOperations::StoreKeyValue(const std::string& storeId,
+                                                  const std::string& key,
+                                                  const void* value,
+                                                  size_t valueSize)
+  {
+    if (storeId.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (value == NULL &&
+        valueSize > 0)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& storeId_;
+      const std::string& key_;
+      const void* value_;
+      size_t valueSize_;
+
+    public:
+      Operations(const std::string& storeId,
+                 const std::string& key,
+                 const void* value,
+                 size_t valueSize) :
+        storeId_(storeId),
+        key_(key),
+        value_(value),
+        valueSize_(valueSize)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.StoreKeyValue(storeId_, key_, value_, valueSize_);
+      }
+    };
+
+    Operations operations(storeId, key, value, valueSize);
+    Apply(operations);
+  }
+
+  void StatelessDatabaseOperations::DeleteKeyValue(const std::string& storeId,
+                                                   const std::string& key)
+  {
+    if (storeId.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    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)
+  {
+    if (storeId.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    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::EnqueueValue(const std::string& queueId,
+                                                 const void* value,
+                                                 size_t valueSize)
+  {
+    if (queueId.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (value == NULL &&
+        valueSize > 0)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& queueId_;
+      const void* value_;
+      size_t valueSize_;
+
+    public:
+      Operations(const std::string& queueId,
+                 const void* value,
+                 size_t valueSize) :
+        queueId_(queueId),
+        value_(value),
+        valueSize_(valueSize)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.EnqueueValue(queueId_, value_, valueSize_);
+      }
+    };
+
+    Operations operations(queueId, value, valueSize);
+    Apply(operations);
+  }
+
+  bool StatelessDatabaseOperations::DequeueValue(std::string& value,
+                                                 const std::string& queueId,
+                                                 QueueOrigin origin)
+  {
+    if (queueId.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    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();
+  }
+
+  uint64_t StatelessDatabaseOperations::GetQueueSize(const std::string& queueId)
+  {
+    if (queueId.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    class Operations : public ReadOnlyOperationsT2<uint64_t&, const std::string& >
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        tuple.get<0>() = transaction.GetQueueSize(tuple.get<1>());
+      }
+    };
+
+    uint64_t size;
+
+    Operations operations;
+    operations.Apply(*this, size, queueId);
+
+    return size;
+  }
+
+
+  void StatelessDatabaseOperations::GetAttachmentCustomData(std::string& customData,
+                                                            const std::string& attachmentUuid)
+  {
+    class Operations : public ReadOnlyOperationsT2<std::string&, const std::string& >
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.GetAttachmentCustomData(tuple.get<0>(), tuple.get<1>());
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, customData, attachmentUuid);
+  }
+
+
+  void StatelessDatabaseOperations::SetAttachmentCustomData(const std::string& attachmentUuid,
+                                                            const void* customData,
+                                                            size_t customDataSize)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& attachmentUuid_;
+      const void*        customData_;
+      size_t             customDataSize_;
+
+    public:
+      Operations(const std::string& attachmentUuid,
+                 const void* customData,
+                 size_t customDataSize) :
+        attachmentUuid_(attachmentUuid),
+        customData_(customData),
+        customDataSize_(customDataSize)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.SetAttachmentCustomData(attachmentUuid_, customData_, customDataSize_);
+      }
+    };
+
+    Operations operations(attachmentUuid, customData, customDataSize);
+    Apply(operations);
+  }
+
+
+  StatelessDatabaseOperations::KeysValuesIterator::KeysValuesIterator(StatelessDatabaseOperations& db,
+                                                                      const std::string& storeId) :
+    db_(db),
+    state_(State_Waiting),
+    storeId_(storeId),
+    limit_(100)
+  {
+    if (storeId.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  bool StatelessDatabaseOperations::KeysValuesIterator::Next()
+  {
+    if (state_ == State_Done)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (state_ == State_Available)
+    {
+      assert(currentKey_ != keys_.end());
+      assert(currentValue_ != values_.end());
+      ++currentKey_;
+      ++currentValue_;
+
+      if (currentKey_ != keys_.end() &&
+          currentValue_ != values_.end())
+      {
+        // A value is still available in the last keys-values block fetched from the database
+        return true;
+      }
+      else if (currentKey_ != keys_.end() ||
+               currentValue_ != values_.end())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    class Operations : public ReadOnlyOperationsT6<std::list<std::string>&, std::list<std::string>&, const std::string&, bool, const std::string&, uint64_t>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ListKeysValues(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>());
+      }
+    };
+
+    if (state_ == State_Waiting)
+    {
+      keys_.clear();
+      values_.clear();
+
+      Operations operations;
+      operations.Apply(db_, keys_, values_, storeId_, true, "", limit_);
+    }
+    else
+    {
+      assert(state_ == State_Available);
+      if (keys_.empty())
+      {
+        state_ = State_Done;
+        return false;
+      }
+      else
+      {
+        const std::string lastKey = keys_.back();
+        keys_.clear();
+        values_.clear();
+
+        Operations operations;
+        operations.Apply(db_, keys_, values_, storeId_, false, lastKey, limit_);
+      }
+    }
+
+    if (keys_.size() != values_.size())
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+
+    if (limit_ != 0 &&
+        keys_.size() > limit_)
+    {
+      // The database plugin has returned too many key-value pairs
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+
+    if (keys_.empty() &&
+        values_.empty())
+    {
+      state_ = State_Done;
+      return false;
+    }
+    else if (!keys_.empty() &&
+             !values_.empty())
+    {
+      state_ = State_Available;
+      currentKey_ = keys_.begin();
+      currentValue_ = values_.begin();
+      return true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);  // Should never happen
+    }
+  }
+
+  const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetKey() const
+  {
+    if (state_ == State_Available)
+    {
+      return *currentKey_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetValue() const
+  {
+    if (state_ == State_Available)
+    {
+      return *currentValue_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Jun 27 15:00:33 2025 +0200
@@ -226,6 +226,12 @@
         return transaction_.LookupAttachment(attachment, revision, id, contentType);
       }
       
+      void GetAttachmentCustomData(std::string& customData,
+                                   const std::string& attachmentUuid)
+      {
+        return transaction_.GetAttachmentCustomData(customData, attachmentUuid);
+      }
+
       bool LookupGlobalProperty(std::string& target,
                                 GlobalProperty property,
                                 bool shared)
@@ -293,6 +299,28 @@
       {
         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);
+      }
+
+      uint64_t GetQueueSize(const std::string& queueId)
+      {
+        return transaction_.GetQueueSize(queueId);
+      }
+
+      void ListKeysValues(std::list<std::string>& keys,
+                          std::list<std::string>& values,
+                          const std::string& storeId,
+                          bool first,
+                          const std::string& from,
+                          uint64_t limit)
+      {
+        return transaction_.ListKeysValues(keys, values, storeId, first, from, limit);
+      }
     };
 
 
@@ -428,6 +456,41 @@
       {
         transaction_.RemoveLabel(id, label);
       }
+
+      void StoreKeyValue(const std::string& storeId,
+                         const std::string& key,
+                         const void* value,
+                         size_t valueSize)
+      {
+        transaction_.StoreKeyValue(storeId, key, value, valueSize);
+      }
+
+      void DeleteKeyValue(const std::string& storeId,
+                          const std::string& key)
+      {
+        transaction_.DeleteKeyValue(storeId, key);
+      }
+
+      void EnqueueValue(const std::string& queueId,
+                        const void* value,
+                        size_t valueSize)
+      {
+        transaction_.EnqueueValue(queueId, value, valueSize);
+      }
+
+      bool DequeueValue(std::string& value,
+                        const std::string& queueId,
+                        QueueOrigin origin)
+      {
+        return transaction_.DequeueValue(value, queueId, origin);
+      }
+
+      void SetAttachmentCustomData(const std::string& attachmentUuid,
+                                      const void* customData,
+                                      size_t customDataSize)
+      {
+        return transaction_.SetAttachmentCustomData(attachmentUuid, customData, customDataSize);
+      }
     };
 
 
@@ -523,6 +586,13 @@
                              /* out */ uint64_t& countSeries, 
                              /* out */ uint64_t& countInstances);
 
+    void GetAttachmentCustomData(std::string& customData,
+                                 const std::string& attachmentUuid);
+
+    void SetAttachmentCustomData(const std::string& attachmentUuid,
+                                 const void* customData,
+                                 size_t customDataSize);
+
     bool LookupAttachment(FileInfo& attachment,
                           int64_t& revision,
                           ResourceType level,
@@ -544,6 +614,12 @@
     bool HasExtendedChanges();
 
     bool HasFindSupport();
+
+    bool HasAttachmentCustomDataSupport();
+
+    bool HasKeyValueStoresSupport();
+
+    bool HasQueuesSupport();
     
     void GetExportedResources(Json::Value& target,
                               int64_t since,
@@ -615,6 +691,10 @@
     bool GetAllMainDicomTags(DicomMap& result,
                              const std::string& instancePublicId);
 
+    bool LookupResource(int64_t& id,
+                        ResourceType& type,
+                        const std::string& publicId);
+
     bool LookupResourceType(ResourceType& type,
                             const std::string& publicId);
 
@@ -724,5 +804,80 @@
 
     void ExecuteCount(uint64_t& count,
                       const FindRequest& request);
+
+    void StoreKeyValue(const std::string& storeId,
+                       const std::string& key,
+                       const void* value,
+                       size_t valueSize);
+
+    void StoreKeyValue(const std::string& storeId,
+                       const std::string& key,
+                       const std::string& value)
+    {
+      StoreKeyValue(storeId, key, value.empty() ? NULL : value.c_str(), value.size());
+    }
+
+    void DeleteKeyValue(const std::string& storeId,
+                        const std::string& key);
+
+    bool GetKeyValue(std::string& value,
+                     const std::string& storeId,
+                     const std::string& key);
+
+    void EnqueueValue(const std::string& queueId,
+                      const void* value,
+                      size_t valueSize);
+
+    void EnqueueValue(const std::string& queueId,
+                      const std::string& value)
+    {
+      EnqueueValue(queueId, value.empty() ? NULL : value.c_str(), value.size());
+    }
+
+    bool DequeueValue(std::string& value,
+                      const std::string& queueId,
+                      QueueOrigin origin);
+    
+    uint64_t GetQueueSize(const std::string& queueId);
+
+    class KeysValuesIterator : public boost::noncopyable
+    {
+    private:
+      enum State
+      {
+        State_Waiting,
+        State_Available,
+        State_Done
+      };
+
+      StatelessDatabaseOperations&            db_;
+      State                                   state_;
+      std::string                             storeId_;
+      uint64_t                                limit_;
+      std::list<std::string>                  keys_;
+      std::list<std::string>                  values_;
+      std::list<std::string>::const_iterator  currentKey_;
+      std::list<std::string>::const_iterator  currentValue_;
+
+    public:
+      KeysValuesIterator(StatelessDatabaseOperations& db,
+                         const std::string& storeId);
+
+      void SetLimit(uint64_t limit)
+      {
+        limit_ = limit;
+      }
+
+      uint64_t GetLimit() const
+      {
+        return limit_;
+      }
+
+      bool Next();
+
+      const std::string& GetKey() const;
+
+      const std::string& GetValue() const;
+    };
   };
 }
--- a/OrthancServer/Sources/LuaScripting.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/LuaScripting.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -570,6 +570,60 @@
     return 1;
   }
 
+  // Syntax in Lua: SetStableStatus(resourceId, true)
+  int LuaScripting::SetStableStatus(lua_State* state)
+  {
+    ServerContext* serverContext = GetServerContext(state);
+    if (serverContext == NULL)
+    {
+      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
+      lua_pushnil(state);
+      return 1;
+    }
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if (nArgs < 1 || nArgs > 3 ||
+        !lua_isstring(state, 1) ||                 // Resource
+        !lua_isboolean(state, 2))                  // newStateIsStable
+    {
+      LOG(ERROR) << "Lua: Bad parameters to SetStableStatus()";
+      lua_pushnil(state);
+      return 1;
+    }
+
+    const char* resourceId = lua_tostring(state, 1);
+    bool newStateIsStable = lua_toboolean(state, 2);
+    
+    try
+    {
+      bool hasStateChanged = false;
+
+      if (serverContext->GetIndex().SetStableStatus(hasStateChanged, resourceId, newStateIsStable))
+      {
+        if (hasStateChanged)
+        {
+          lua_pushboolean(state, 1);
+        }
+        else
+        {
+          lua_pushboolean(state, 0);
+        }
+
+        return 1;
+      }
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: " << e.What();
+    }
+
+    LOG(ERROR) << "Lua: Error in SetStableStatus() for Resource: " << resourceId;
+    lua_pushnil(state);
+
+    return 1;
+  }
+
 
   // Syntax in Lua: GetOrthancConfiguration()
   int LuaScripting::GetOrthancConfiguration(lua_State *state)
@@ -760,6 +814,7 @@
     lua_.RegisterFunction("RestApiPut", RestApiPut);
     lua_.RegisterFunction("RestApiDelete", RestApiDelete);
     lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration);
+    lua_.RegisterFunction("SetStableStatus", SetStableStatus);
 
     LOG(INFO) << "Initializing Lua for the event handler";
     LoadGlobalConfiguration();
--- a/OrthancServer/Sources/LuaScripting.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/LuaScripting.h	Fri Jun 27 15:00:33 2025 +0200
@@ -62,6 +62,7 @@
     static int RestApiPut(lua_State *state);
     static int RestApiDelete(lua_State *state);
     static int GetOrthancConfiguration(lua_State *state);
+    static int SetStableStatus(lua_State* state);
 
     size_t ParseOperation(LuaJobManager::Lock& lock,
                           const std::string& operation,
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.h	Fri Jun 27 15:00:33 2025 +0200
@@ -35,7 +35,7 @@
 
   IDatabaseWrapper* CreateDatabaseWrapper();
 
-  IStorageArea* CreateStorageArea();
+  IPluginStorageArea* CreateStorageArea();
 
   void SetGlobalVerbosity(Verbosity verbosity);
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Fri Jun 27 15:00:33 2025 +0200
@@ -356,7 +356,7 @@
 
 
   ServerContext::ServerContext(IDatabaseWrapper& database,
-                               IStorageArea& area,
+                               IPluginStorageArea& area,
                                bool unitTesting,
                                size_t maxCompletedJobs,
                                bool readOnly,
@@ -613,10 +613,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);
   }
 
 
@@ -625,6 +626,37 @@
                                                                   StoreInstanceMode mode,
                                                                   bool isReconstruct)
   {
+    FileInfo adoptedFileNotUsed;
+
+    return StoreAfterTranscoding(resultPublicId,
+                                 dicom,
+                                 mode,
+                                 isReconstruct,
+                                 false,
+                                 adoptedFileNotUsed);
+  }
+
+  ServerContext::StoreResult ServerContext::AdoptDicomInstance(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)
     {
@@ -727,19 +759,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_);
+      StatelessDatabaseOperations::Attachments attachments;
+      FileInfo dicomInfo;
 
-      ServerIndex::Attachments attachments;
-      attachments.push_back(dicomInfo);
+      if (!isAdoption)
+      {
+        accessor.Write(dicomInfo, 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_);
+        accessor.Write(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL);
         attachments.push_back(dicomUntilPixelData);
       }
 
@@ -784,7 +822,10 @@
             
       if (result.GetStatus() != StoreStatus_Success)
       {
-        accessor.Remove(dicomInfo);
+        if (!isAdoption)
+        {
+          accessor.Remove(dicomInfo);
+        }
 
         if (dicomUntilPixelData.IsValid())
         {
@@ -798,7 +839,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:
@@ -846,7 +894,7 @@
     {
       if (e.GetErrorCode() == ErrorCode_InexistentTag)
       {
-        summary.LogMissingTagsForStore();
+        LOG(ERROR) << summary.FormatMissingTagsForStore();
       }
       
       throw;
@@ -860,12 +908,12 @@
   { 
     DicomInstanceToStore* dicom = &receivedDicom;
 
+#if ORTHANC_ENABLE_PLUGINS == 1
     // WARNING: The scope of "modifiedBuffer" and "modifiedDicom" must
     // be the same as that of "dicom"
-    MallocMemoryBuffer modifiedBuffer;
+    PluginMemoryBuffer64 modifiedBuffer;
     std::unique_ptr<DicomInstanceToStore> modifiedDicom;
 
-#if ORTHANC_ENABLE_PLUGINS == 1
     if (HasPlugins())
     {
       // New in Orthanc 1.10.0
@@ -1038,8 +1086,8 @@
     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(modified, content.empty() ? NULL : content.c_str(), content.size(), attachmentType, compression, storeMD5_, NULL);
 
     try
     {
@@ -1221,7 +1269,7 @@
 
 
       if (hasPixelDataOffset &&
-          area_.HasReadRange() &&
+          area_.HasEfficientReadRange() &&
           LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments) &&
           attachment.GetCompressionType() == CompressionType_None)
       {
@@ -1299,13 +1347,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 */);
             }
           }
         }
@@ -1374,7 +1422,7 @@
       return true;
     }
 
-    if (!area_.HasReadRange())
+    if (!area_.HasEfficientReadRange())
     {
       return false;
     }
@@ -1533,6 +1581,7 @@
 
   bool ServerContext::AddAttachment(int64_t& newRevision,
                                     const std::string& resourceId,
+                                    ResourceType resourceType,
                                     FileContentType attachmentType,
                                     const void* data,
                                     size_t size,
@@ -1546,7 +1595,11 @@
     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(attachment, data, size, attachmentType, compression, storeMD5_, NULL);
 
     try
     {
--- a/OrthancServer/Sources/ServerContext.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Fri Jun 27 15:00:33 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 AdoptDicomInstance(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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Fri Jun 27 15:00:33 2025 +0200
@@ -173,6 +173,7 @@
     GlobalProperty_AnonymizationSequence = 3,
     GlobalProperty_JobsRegistry = 5,
     GlobalProperty_GetTotalSizeIsFast = 6,      // New in Orthanc 1.5.2
+    GlobalProperty_SQLiteHasRevisionAndCustomData = 7,     // New in Orthanc 1.12.8
     GlobalProperty_Modalities = 20,             // New in Orthanc 1.5.0
     GlobalProperty_Peers = 21,                  // New in Orthanc 1.5.0
 
@@ -262,6 +263,11 @@
     Warnings_007_MissingRequestedTagsNotReadFromDisk       // new in Orthanc 1.12.5
   };
 
+  enum QueueOrigin
+  {
+    QueueOrigin_Front,
+    QueueOrigin_Back
+  };
 
   void InitializeServerEnumerations();
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Fri Jun 27 15:00:33 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)
         {
@@ -305,7 +312,7 @@
   bool ServerIndex::IsUnstableResource(ResourceType type,
                                        int64_t id)
   {
-    boost::mutex::scoped_lock lock(monitoringMutex_);
+    boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
     return unstableResources_.Contains(std::make_pair(type, id));
   }
 
@@ -387,7 +394,7 @@
   void ServerIndex::SetMaximumPatientCount(unsigned int count) 
   {
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumPatients_ = count;
       
       if (count == 0)
@@ -407,7 +414,7 @@
   void ServerIndex::SetMaximumStorageSize(uint64_t size) 
   {
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumStorageSize_ = size;
       
       if (size == 0)
@@ -426,7 +433,7 @@
   void ServerIndex::SetMaximumStorageMode(MaxStorageMode mode) 
   {
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumStorageMode_ = mode;
       
       if (mode == MaxStorageMode_Recycle)
@@ -479,7 +486,7 @@
         int64_t stableId;
 
         {      
-          boost::mutex::scoped_lock lock(that->monitoringMutex_);
+          boost::recursive_mutex::scoped_lock lock(that->monitoringMutex_);
 
           if (!that->unstableResources_.IsEmpty() &&
               that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge))
@@ -498,43 +505,52 @@
           }
         }
 
-        try
-        {
-          /**
-           * WARNING: Don't protect the calls to "LogChange()" using
-           * "monitoringMutex_", as this could lead to deadlocks in
-           * other threads (typically, if "Store()" is being running in
-           * another thread, which leads to calls to "MarkAsUnstable()",
-           * which leads to two lockings of "monitoringMutex_").
-           **/
-          switch (stableLevel)
-          {
-            case ResourceType_Patient:
-              that->LogChange(stableId, ChangeType_StablePatient, stablePayload.GetPublicId(), ResourceType_Patient);
-              break;
-            
-            case ResourceType_Study:
-              that->LogChange(stableId, ChangeType_StableStudy, stablePayload.GetPublicId(), ResourceType_Study);
-              break;
-            
-            case ResourceType_Series:
-              that->LogChange(stableId, ChangeType_StableSeries, stablePayload.GetPublicId(), ResourceType_Series);
-              break;
-            
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-        }
-        catch (OrthancException& e)
-        {
-          LOG(ERROR) << "Cannot log a change about a stable resource into the database";
-        }          
+        // must not be protected by monitoringMutex_
+        that->LogStableChange(stableLevel, stableId, stablePayload.GetPublicId());
       }
     }
 
     LOG(INFO) << "Closing the monitor thread for stable resources";
   }
   
+  void ServerIndex::LogStableChange(ResourceType stableLevel,
+                                    int64_t stableId,
+                                    const std::string& publicId)
+  {
+    try
+    {
+      /**
+        * WARNING: Don't protect the calls to "LogChange()" using
+        * "monitoringMutex_", as this could lead to deadlocks in
+        * other threads (typically, if "Store()" is being running in
+        * another thread, which leads to calls to "MarkAsUnstable()",
+        * which leads to two lockings of "monitoringMutex_").
+        **/
+      switch (stableLevel)
+      {
+        case ResourceType_Patient:
+          LogChange(stableId, ChangeType_StablePatient, publicId, ResourceType_Patient);
+          break;
+        
+        case ResourceType_Study:
+          LogChange(stableId, ChangeType_StableStudy, publicId, ResourceType_Study);
+          break;
+        
+        case ResourceType_Series:
+          LogChange(stableId, ChangeType_StableSeries, publicId, ResourceType_Series);
+          break;
+        
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Cannot log a change about a stable resource into the database";
+    }          
+  }
+
+
 
   void ServerIndex::MarkAsUnstable(ResourceType type,
                                    int64_t id,
@@ -545,13 +561,59 @@
            type == ResourceType_Series);
 
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       UnstableResourcePayload payload(publicId);
       unstableResources_.AddOrMakeMostRecent(std::make_pair(type, id), payload);
       //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
     }
   }
 
+  bool ServerIndex::SetStableStatus(bool& statusHasChanged,
+                                    const std::string& resourceId,
+                                    bool setNewStatusToStable)
+  {
+    int64_t id;
+    ResourceType type;
+
+    if (LookupResource(id, type, resourceId))
+    {
+      if (setNewStatusToStable)
+      {
+        {
+          boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
+
+          if (IsUnstableResource(type, id))
+          {
+            unstableResources_.Invalidate(std::pair<ResourceType, int64_t>(type, id));
+            statusHasChanged = true;
+          }
+        }
+
+        if (statusHasChanged)
+        {
+          // must not be protected by monitoringMutex_
+          LogStableChange(type, id, resourceId);
+        }
+      }
+      else
+      {
+        {
+          boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
+
+          statusHasChanged = !IsUnstableResource(type, id);
+
+          // no matter what was the status, we mark it as unstable to reset its stabilization period
+          MarkAsUnstable(type, id, resourceId);
+        }
+
+      }
+
+      return true;
+    }
+
+    return false;
+  }
+
 
   StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
                                  const DicomMap& dicomSummary,
@@ -571,7 +633,7 @@
     MaxStorageMode maximumStorageMode;
     
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumStorageSize = maximumStorageSize_;
       maximumPatients = maximumPatients_;
       maximumStorageMode = maximumStorageMode_;
@@ -595,7 +657,7 @@
     unsigned int maximumPatients;
     
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumStorageSize = maximumStorageSize_;
       maximumPatients = maximumPatients_;
     }
--- a/OrthancServer/Sources/ServerIndex.h	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Fri Jun 27 15:00:33 2025 +0200
@@ -40,7 +40,7 @@
     class UnstableResourcePayload;
 
     bool done_;
-    boost::mutex monitoringMutex_;
+    boost::recursive_mutex monitoringMutex_;
     boost::thread flushThread_;
     boost::thread unstableResourcesMonitorThread_;
 
@@ -61,6 +61,10 @@
                         int64_t id,
                         const std::string& publicId);
 
+    void LogStableChange(ResourceType type,
+                         int64_t id,
+                         const std::string& publicId);
+
   public:
     ServerIndex(ServerContext& context,
                 IDatabaseWrapper& database,
@@ -101,5 +105,9 @@
 
     bool IsUnstableResource(ResourceType type,
                             int64_t id);
+
+    bool SetStableStatus(bool& statusHasChanged,
+                         const std::string& resourceId,
+                         bool setNewStatusToStable);
   };
 }
--- a/OrthancServer/Sources/ServerToolbox.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/ServerToolbox.h	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/Sources/main.cpp	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Fri Jun 27 15:00:33 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"
 
@@ -196,6 +197,74 @@
       transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);
     }
   };
+
+  class DummyTransactionContextFactory : public StatelessDatabaseOperations::ITransactionContextFactory
+  {
+  public:
+    virtual StatelessDatabaseOperations::ITransactionContext* Create()
+    {
+      class DummyTransactionContext : public StatelessDatabaseOperations::ITransactionContext
+      {
+      public:
+        virtual void SignalRemainingAncestor(ResourceType parentType,
+                                             const std::string& publicId) ORTHANC_OVERRIDE
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        virtual void SignalResourceDeleted(ResourceType type,
+                                           const std::string& publicId) ORTHANC_OVERRIDE
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        virtual void Commit() ORTHANC_OVERRIDE
+        {
+        }
+
+        virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE
+        {
+          return 0;
+        }
+
+        virtual bool IsUnstableResource(Orthanc::ResourceType type,
+                                        int64_t id) ORTHANC_OVERRIDE
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */,
+                                          ResourceType& remainingLevel   /* out */) ORTHANC_OVERRIDE
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        virtual void MarkAsUnstable(Orthanc::ResourceType type,
+                                    int64_t id,
+                                    const std::string& publicId) ORTHANC_OVERRIDE
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        virtual void SignalAttachmentsAdded(uint64_t compressedSize)  ORTHANC_OVERRIDE
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+
+        virtual void SignalChange(const ServerIndexChange& change)  ORTHANC_OVERRIDE
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+      };
+
+      return new DummyTransactionContext;
+    }
+  };
 }
 
 
@@ -295,12 +364,18 @@
   transaction_->GetAllMetadata(md, a[4]);
   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);
+  FileInfo attachment1("my json file", FileContentType_DicomAsJson, 42, "md5",
+                       CompressionType_ZlibWithSize, 21, "compressedMD5");
+  attachment1.SetCustomData("hello");
+  transaction_->AddAttachment(a[4], attachment1, 42);
+
+  FileInfo attachment2("my dicom file", FileContentType_Dicom, 43, "md5_2");
+  transaction_->AddAttachment(a[4], attachment2, 43);
+
+  FileInfo attachment3("world", FileContentType_Dicom, 44, "md5_3");
+  attachment3.SetCustomData("world");
+  transaction_->AddAttachment(a[6], attachment3, 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]);
@@ -326,8 +401,8 @@
   ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]);
 
 
-  ASSERT_EQ(21u + 42u + 44u, transaction_->GetTotalCompressedSize());
-  ASSERT_EQ(42u + 42u + 44u, transaction_->GetTotalUncompressedSize());
+  ASSERT_EQ(21u + 43u + 44u, transaction_->GetTotalCompressedSize());
+  ASSERT_EQ(42u + 43u + 44u, transaction_->GetTotalUncompressedSize());
 
   transaction_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName");
 
@@ -339,17 +414,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,22 +432,34 @@
 
   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());
   ASSERT_EQ("compressedMD5", att.GetCompressedMD5());
   ASSERT_EQ(42u, att.GetUncompressedSize());
   ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType());
+  ASSERT_EQ("hello", att.GetCustomData());
+
+  ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_Dicom));
+  ASSERT_EQ(43, revision);
+  ASSERT_EQ("my dicom file", att.GetUuid());
+  ASSERT_EQ(43u, att.GetCompressedSize());
+  ASSERT_EQ("md5_2", att.GetUncompressedMD5());
+  ASSERT_EQ("md5_2", att.GetCompressedMD5());
+  ASSERT_EQ(43u, att.GetUncompressedSize());
+  ASSERT_EQ(CompressionType_None, att.GetCompressionType());
+  ASSERT_TRUE(att.GetCustomData().empty());
 
   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());
-  ASSERT_EQ("md5", att.GetCompressedMD5());
+  ASSERT_EQ("md5_3", att.GetUncompressedMD5());
+  ASSERT_EQ("md5_3", att.GetCompressedMD5());
   ASSERT_EQ(44u, att.GetUncompressedSize());
   ASSERT_EQ(CompressionType_None, att.GetCompressionType());
+  ASSERT_EQ("world", att.GetCustomData());
 
   ASSERT_EQ(0u, listener_->deletedFiles_.size());
   ASSERT_EQ(0u, listener_->deletedResources_.size());
@@ -402,7 +489,7 @@
 
   CheckTableRecordCount(0, "Resources");
   CheckTableRecordCount(0, "AttachedFiles");
-  CheckTableRecordCount(3, "GlobalProperties");
+  CheckTableRecordCount(4, "GlobalProperties");
 
   std::string tmp;
   ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true));
@@ -618,7 +705,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 +787,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 */);
@@ -817,7 +904,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 +1069,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 */);
@@ -1058,3 +1145,215 @@
   ASSERT_FALSE(ServerToolbox::IsValidLabel("&"));
   ASSERT_FALSE(ServerToolbox::IsValidLabel("."));
 }
+
+
+TEST(SQLiteDatabaseWrapper, KeyValueStores)
+{
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+  db.Open();
+
+  {
+    StatelessDatabaseOperations op(db, false);
+    op.SetTransactionContextFactory(new DummyTransactionContextFactory);
+
+    for (unsigned int limit = 0; limit < 5; limit++)
+    {
+      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
+      it.SetLimit(limit);
+      ASSERT_THROW(it.GetValue(), OrthancException);
+      ASSERT_THROW(it.GetKey(), OrthancException);
+      ASSERT_FALSE(it.Next());
+      ASSERT_THROW(it.GetValue(), OrthancException);
+      ASSERT_THROW(it.GetKey(), OrthancException);
+    }
+
+    op.StoreKeyValue("test", "hello", "world");
+
+    for (unsigned int limit = 0; limit < 5; limit++)
+    {
+      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
+      it.SetLimit(limit);
+      ASSERT_THROW(it.GetValue(), OrthancException);
+      ASSERT_THROW(it.GetKey(), OrthancException);
+      ASSERT_TRUE(it.Next());
+      ASSERT_EQ("hello", it.GetKey());
+      ASSERT_EQ("world", it.GetValue());
+      ASSERT_FALSE(it.Next());
+      ASSERT_THROW(it.GetValue(), OrthancException);
+      ASSERT_THROW(it.GetKey(), OrthancException);
+    }
+
+    op.StoreKeyValue("test", "hello2", "world2");
+    op.StoreKeyValue("test", "hello3", "world3");
+
+    for (unsigned int limit = 0; limit < 5; limit++)
+    {
+      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
+      it.SetLimit(limit);
+      ASSERT_THROW(it.GetValue(), OrthancException);
+      ASSERT_THROW(it.GetKey(), OrthancException);
+      ASSERT_TRUE(it.Next());
+      ASSERT_EQ("hello", it.GetKey());
+      ASSERT_EQ("world", it.GetValue());
+      ASSERT_TRUE(it.Next());
+      ASSERT_EQ("hello2", it.GetKey());
+      ASSERT_EQ("world2", it.GetValue());
+      ASSERT_TRUE(it.Next());
+      ASSERT_EQ("hello3", it.GetKey());
+      ASSERT_EQ("world3", it.GetValue());
+      ASSERT_FALSE(it.Next());
+      ASSERT_THROW(it.GetValue(), OrthancException);
+      ASSERT_THROW(it.GetKey(), OrthancException);
+    }
+
+    op.DeleteKeyValue("test", "hello2");
+
+    for (unsigned int limit = 0; limit < 5; limit++)
+    {
+      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
+      it.SetLimit(limit);
+      ASSERT_TRUE(it.Next());
+      ASSERT_EQ("hello", it.GetKey());
+      ASSERT_EQ("world", it.GetValue());
+      ASSERT_TRUE(it.Next());
+      ASSERT_EQ("hello3", it.GetKey());
+      ASSERT_EQ("world3", it.GetValue());
+      ASSERT_FALSE(it.Next());
+    }
+
+    std::string s;
+    ASSERT_TRUE(op.GetKeyValue(s, "test", "hello"));   ASSERT_EQ("world", s);
+    ASSERT_TRUE(op.GetKeyValue(s, "test", "hello3"));  ASSERT_EQ("world3", s);
+    ASSERT_FALSE(op.GetKeyValue(s, "test", "hello2"));
+
+    ASSERT_TRUE(op.GetKeyValue(s, "test", "hello"));   ASSERT_EQ("world", s);
+    op.StoreKeyValue("test", "hello", "overwritten");
+    ASSERT_TRUE(op.GetKeyValue(s, "test", "hello"));   ASSERT_EQ("overwritten", s);
+
+    op.DeleteKeyValue("test", "nope");
+
+    op.DeleteKeyValue("test", "hello");
+    op.DeleteKeyValue("test", "hello3");
+
+    for (unsigned int limit = 0; limit < 5; limit++)
+    {
+      StatelessDatabaseOperations::KeysValuesIterator it(op, "test");
+      it.SetLimit(limit);
+      ASSERT_FALSE(it.Next());
+    }
+
+    {
+      std::string blob;
+      blob.push_back(0);
+      blob.push_back(1);
+      blob.push_back(0);
+      blob.push_back(2);
+      op.StoreKeyValue("test", "blob", blob); // Storing binary values
+    }
+
+    ASSERT_TRUE(op.GetKeyValue(s, "test", "blob"));
+    ASSERT_EQ(4u, s.size());
+    ASSERT_EQ(0, static_cast<uint8_t>(s[0]));
+    ASSERT_EQ(1, static_cast<uint8_t>(s[1]));
+    ASSERT_EQ(0, static_cast<uint8_t>(s[2]));
+    ASSERT_EQ(2, static_cast<uint8_t>(s[3]));
+    op.DeleteKeyValue("test", "blob");
+    ASSERT_FALSE(op.GetKeyValue(s, "test", "blob"));
+  }
+
+  db.Close();
+}
+
+
+TEST(SQLiteDatabaseWrapper, Queues)
+{
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+  db.Open();
+
+  {
+    StatelessDatabaseOperations op(db, false);
+    op.SetTransactionContextFactory(new DummyTransactionContextFactory);
+
+    ASSERT_EQ(0u, op.GetQueueSize("test"));
+    op.EnqueueValue("test", "hello");
+    ASSERT_EQ(1u, op.GetQueueSize("test"));
+    op.EnqueueValue("test", "world");
+    ASSERT_EQ(2u, op.GetQueueSize("test"));
+
+    std::string s;
+    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back));  ASSERT_EQ("world", s);
+    ASSERT_EQ(1u, op.GetQueueSize("test"));
+    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back));  ASSERT_EQ("hello", s);
+    ASSERT_EQ(0u, op.GetQueueSize("test"));
+    ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Back));
+
+    op.EnqueueValue("test", "hello");
+    op.EnqueueValue("test", "world");
+    ASSERT_EQ(2u, op.GetQueueSize("test"));
+
+    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front));  ASSERT_EQ("hello", s);
+    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front));  ASSERT_EQ("world", s);
+    ASSERT_EQ(0u, op.GetQueueSize("test"));
+    ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front));
+
+    {
+      std::string blob;
+      blob.push_back(0);
+      blob.push_back(1);
+      blob.push_back(0);
+      blob.push_back(2);
+      op.EnqueueValue("test", blob); // Storing binary values
+    }
+
+    ASSERT_EQ(1u, op.GetQueueSize("test"));
+    ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front));
+    ASSERT_EQ(0u, op.GetQueueSize("test"));
+    ASSERT_EQ(4u, s.size());
+    ASSERT_EQ(0, static_cast<uint8_t>(s[0]));
+    ASSERT_EQ(1, static_cast<uint8_t>(s[1]));
+    ASSERT_EQ(0, static_cast<uint8_t>(s[2]));
+    ASSERT_EQ(2, static_cast<uint8_t>(s[3]));
+    ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front));
+  }
+
+  db.Close();
+}
+
+
+TEST_F(DatabaseWrapperTest, BinaryCustomData)
+{
+  int64_t patient = transaction_->CreateResource("Patient", ResourceType_Patient);
+
+  {
+    FileInfo info("hello", FileContentType_Dicom, 10, "md5");
+
+    {
+      std::string blob;
+      blob.push_back(0);
+      blob.push_back(1);
+      blob.push_back(0);
+      blob.push_back(2);
+      info.SetCustomData(blob);
+    }
+
+    transaction_->AddAttachment(patient, info, 43);
+  }
+
+  {
+    FileInfo info;
+    int64_t revision;
+    ASSERT_TRUE(transaction_->LookupAttachment(info, revision, patient, FileContentType_Dicom));
+    ASSERT_EQ(43u, revision);
+    ASSERT_EQ("hello", info.GetUuid());
+    ASSERT_EQ(CompressionType_None, info.GetCompressionType());
+    ASSERT_EQ(10u, info.GetCompressedSize());
+    ASSERT_EQ("md5", info.GetCompressedMD5());
+    ASSERT_EQ(4u, info.GetCustomData().size());
+    ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[0]));
+    ASSERT_EQ(1, static_cast<uint8_t>(info.GetCustomData()[1]));
+    ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[2]));
+    ASSERT_EQ(2, static_cast<uint8_t>(info.GetCustomData()[3]));
+  }
+
+  transaction_->DeleteResource(patient);
+}
\ No newline at end of file
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Fri Jun 27 14:59:41 2025 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Fri Jun 27 15:00:33 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 Jun 27 14:59:41 2025 +0200
+++ b/TODO	Fri Jun 27 15:00:33 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 ===
 =======================