changeset 6020:8d9bc0217a38 attach-custom-data

integration mainline->attach-custom-data
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 24 Feb 2025 09:19:12 +0100
parents 91394d783aef (diff) fa5aa4209b63 (current diff)
children 80acfe62ace3
files NEWS OrthancFramework/Sources/FileStorage/StorageAccessor.cpp OrthancFramework/Sources/FileStorage/StorageAccessor.h OrthancServer/Resources/Configuration.json OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h TODO
diffstat 33 files changed, 1254 insertions(+), 390 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Feb 24 09:09:31 2025 +0100
+++ b/.hgignore	Mon Feb 24 09:19:12 2025 +0100
@@ -15,3 +15,4 @@
 .project
 Resources/Testing/Issue32/Java/bin
 Resources/Testing/Issue32/Java/target
+build/
--- a/NEWS	Mon Feb 24 09:09:31 2025 +0100
+++ b/NEWS	Mon Feb 24 09:19:12 2025 +0100
@@ -1,6 +1,15 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* SQLite default DB engine now supports metadata and attachment revisions
+* Upgraded the DB to allow plugins to store customData for each attachment.
+* New sample Advanced Storage plugin that allows:
+  - using multiple disk for image storage
+  - use more human friendly storage structure (experimental feature)
+
 REST API
 --------
 
@@ -12,6 +21,11 @@
   accepts a "lossy-quality" url argument or a "LossyQuality" field to define the compression quality factor.
   If not specified, the "DicomLossyTranscodingQuality" configuration is taken into account.
 
+Plugins
+-------
+
+* New database plugin SDK (vX) to handle customData for attachments.
+* New storage plugin SDK (v3) to handle customData for attachments,
 
 Maintenance
 -----------
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Mon Feb 24 09:19:12 2025 +0100
@@ -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 "27")
+set(ORTHANC_API_VERSION "28")
 
 
 #####################################################################
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -42,7 +42,8 @@
   FileInfo::FileInfo(const std::string& uuid,
                      FileContentType contentType,
                      uint64_t size,
-                     const std::string& md5) :
+                     const std::string& md5,
+                     const std::string& customData) :
     valid_(true),
     uuid_(uuid),
     contentType_(contentType),
@@ -50,7 +51,8 @@
     uncompressedMD5_(md5),
     compressionType_(CompressionType_None),
     compressedSize_(size),
-    compressedMD5_(md5)
+    compressedMD5_(md5),
+    customData_(customData)
   {
   }
 
@@ -61,7 +63,8 @@
                      const std::string& uncompressedMD5,
                      CompressionType compressionType,
                      uint64_t compressedSize,
-                     const std::string& compressedMD5) :
+                     const std::string& compressedMD5,
+                     const std::string& customData) :
     valid_(true),
     uuid_(uuid),
     contentType_(contentType),
@@ -69,7 +72,8 @@
     uncompressedMD5_(uncompressedMD5),
     compressionType_(compressionType),
     compressedSize_(compressedSize),
-    compressedMD5_(compressedMD5)
+    compressedMD5_(compressedMD5),
+    customData_(customData)
   {
   }
 
@@ -169,4 +173,16 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
   }
+
+  const std::string& FileInfo::GetCustomData() const
+  {
+    if (valid_)
+    {
+      return customData_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
 }
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.h	Mon Feb 24 09:19:12 2025 +0100
@@ -42,6 +42,7 @@
     CompressionType  compressionType_;
     uint64_t         compressedSize_;
     std::string      compressedMD5_;
+    std::string      customData_;
 
   public:
     FileInfo();
@@ -52,7 +53,8 @@
     FileInfo(const std::string& uuid,
              FileContentType contentType,
              uint64_t size,
-             const std::string& md5);
+             const std::string& md5,
+             const std::string& customData);
 
     /**
      * Constructor for a compressed attachment.
@@ -63,7 +65,8 @@
              const std::string& uncompressedMD5,
              CompressionType compressionType,
              uint64_t compressedSize,
-             const std::string& compressedMD5);
+             const std::string& compressedMD5,
+             const std::string& customData);
 
     bool IsValid() const;
     
@@ -80,5 +83,7 @@
     const std::string& GetCompressedMD5() const;
 
     const std::string& GetUncompressedMD5() const;
+
+    const std::string& GetCustomData() const;
   };
 }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -279,6 +279,7 @@
   {
     namespace fs = boost::filesystem;
     typedef std::set<std::string> List;
+    std::string customDataNotUsed;
 
     List result;
     ListAllFiles(result);
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Mon Feb 24 09:19:12 2025 +0100
@@ -43,7 +43,7 @@
 
 namespace Orthanc
 {
-  class ORTHANC_PUBLIC FilesystemStorage : public IStorageArea
+  class ORTHANC_PUBLIC FilesystemStorage : public ICoreStorageArea
   {
     // TODO REMOVE THIS
     friend class FilesystemHttpSender;
--- a/OrthancFramework/Sources/FileStorage/IStorageArea.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h	Mon Feb 24 09:19:12 2025 +0100
@@ -33,6 +33,8 @@
 
 namespace Orthanc
 {
+  class DicomInstanceToStore;
+
   class IStorageArea : public boost::noncopyable
   {
   public:
@@ -40,8 +42,92 @@
     {
     }
 
+    virtual void CreateInstance(std::string& customData,
+                               const DicomInstanceToStore& instance,
+                               const std::string& uuid,
+                               const void* content,
+                               size_t size,
+                               FileContentType type,
+                               bool isCompressed) = 0;
+
+    virtual void CreateAttachment(std::string& customData,
+                                  const std::string& resourceId,
+                                  ResourceType resourceLevel,
+                                  const std::string& uuid,
+                                  const void* content,
+                                  size_t size,
+                                  FileContentType type,
+                                  bool isCompressed) = 0;
+
+    virtual IMemoryBuffer* Read(const std::string& uuid,
+                                FileContentType type,
+                                const std::string& customData) = 0;
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */,
+                                     const std::string& customData) = 0;
+
+    virtual bool HasReadRange() const = 0;
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type,
+                        const std::string& customData) = 0;
+  };
+
+  // storage area without customData (customData are used only in plugins)
+  class ICoreStorageArea : public IStorageArea
+  {
+  public:
+    virtual void CreateInstance(std::string& customData,
+                               const DicomInstanceToStore& instance,
+                               const std::string& uuid,
+                               const void* content,
+                               size_t size,
+                               FileContentType type,
+                               bool isCompressed)
+    {
+      Create(uuid, content, size, type);
+    }
+
+    virtual void CreateAttachment(std::string& customData,
+                                  const std::string& resourceId,
+                                  ResourceType resourceLevel,
+                                  const std::string& uuid,
+                                  const void* content,
+                                  size_t size,
+                                  FileContentType type,
+                                  bool isCompressed)
+    {
+      Create(uuid, content, size, type);
+    }
+
+    virtual IMemoryBuffer* Read(const std::string& uuid,
+                                FileContentType type,
+                                const std::string& /*customData*/)
+    {
+      return Read(uuid, type);
+    }
+
+    virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                     FileContentType type,
+                                     uint64_t start /* inclusive */,
+                                     uint64_t end /* exclusive */,
+                                     const std::string& /*customData */)
+    {
+      return ReadRange(uuid, type, start, end);
+    }
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type,
+                        const std::string& customData)
+    {
+      Remove(uuid, type);
+    }
+
     virtual void Create(const std::string& uuid,
-                        const void* content,
+                        const void* content, 
                         size_t size,
                         FileContentType type) = 0;
 
@@ -53,9 +139,8 @@
                                      uint64_t start /* inclusive */,
                                      uint64_t end /* exclusive */) = 0;
 
-    virtual bool HasReadRange() const = 0;
-
     virtual void Remove(const std::string& uuid,
                         FileContentType type) = 0;
+
   };
 }
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Mon Feb 24 09:19:12 2025 +0100
@@ -33,7 +33,7 @@
 
 namespace Orthanc
 {
-  class MemoryStorageArea : public IStorageArea
+  class MemoryStorageArea : public ICoreStorageArea
   {
   private:
     typedef std::map<std::string, std::string*>  Content;
@@ -44,6 +44,7 @@
   public:
     virtual ~MemoryStorageArea();
     
+  protected:
     virtual void Create(const std::string& uuid,
                         const void* content,
                         size_t size,
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -310,14 +310,15 @@
   }
 
 
-  FileInfo StorageAccessor::Write(const void* data,
-                                  size_t size,
-                                  FileContentType type,
-                                  CompressionType compression,
-                                  bool storeMd5)
+  FileInfo StorageAccessor::WriteInstance(std::string& customData,
+                                          const DicomInstanceToStore& instance,
+                                          const void* data,
+                                          size_t size,
+                                          FileContentType type,
+                                          CompressionType compression,
+                                          bool storeMd5,
+                                          const std::string& uuid)
   {
-    std::string uuid = Toolbox::GenerateUuid();
-
     std::string md5;
 
     if (storeMd5)
@@ -331,7 +332,7 @@
       {
         {
           MetricsTimer timer(*this, METRICS_CREATE_DURATION);
-          area_.Create(uuid, data, size, type);
+          area_.CreateInstance(customData, instance, uuid, data, size, type, false);
         }
 
         if (metrics_ != NULL)
@@ -345,7 +346,7 @@
           cacheAccessor.Add(uuid, type, data, size);
         }
 
-        return FileInfo(uuid, type, size, md5);
+        return FileInfo(uuid, type, size, md5, customData);
       }
 
       case CompressionType_ZlibWithSize:
@@ -367,11 +368,11 @@
 
           if (compressed.size() > 0)
           {
-            area_.Create(uuid, &compressed[0], compressed.size(), type);
+            area_.CreateInstance(customData, instance, uuid, &compressed[0], compressed.size(), type, true);
           }
           else
           {
-            area_.Create(uuid, NULL, 0, type);
+            area_.CreateInstance(customData, instance, uuid, NULL, 0, type, true);
           }
         }
 
@@ -387,7 +388,7 @@
         }
 
         return FileInfo(uuid, type, size, md5,
-                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
+                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData);
       }
 
       default:
@@ -395,13 +396,92 @@
     }
   }
 
-  FileInfo StorageAccessor::Write(const std::string &data,
-                                  FileContentType type,
-                                  CompressionType compression,
-                                  bool storeMd5)
+  FileInfo StorageAccessor::WriteAttachment(std::string& customData,
+                                            const std::string& resourceId,
+                                            ResourceType resourceType,
+                                            const void* data,
+                                            size_t size,
+                                            FileContentType type,
+                                            CompressionType compression,
+                                            bool storeMd5,
+                                            const std::string& uuid)
   {
-    return Write((data.size() == 0 ? NULL : data.c_str()),
-                 data.size(), type, compression, storeMd5);
+    std::string md5;
+
+    if (storeMd5)
+    {
+      Toolbox::ComputeMD5(md5, data, size);
+    }
+
+    switch (compression)
+    {
+      case CompressionType_None:
+      {
+        {
+          MetricsTimer timer(*this, METRICS_CREATE_DURATION);
+          area_.CreateAttachment(customData, resourceId, resourceType, uuid, data, size, type, false);
+        }
+        
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, size);
+        }
+        
+        if (cache_ != NULL)
+        {
+          StorageCache::Accessor cacheAccessor(*cache_);
+          cacheAccessor.Add(uuid, type, data, size);
+        }
+
+
+        return FileInfo(uuid, type, size, md5, customData);
+      }
+
+      case CompressionType_ZlibWithSize:
+      {
+        ZlibCompressor zlib;
+
+        std::string compressed;
+        zlib.Compress(compressed, data, size);
+
+        std::string compressedMD5;
+      
+        if (storeMd5)
+        {
+          Toolbox::ComputeMD5(compressedMD5, compressed);
+        }
+
+        {
+          MetricsTimer timer(*this, METRICS_CREATE_DURATION);
+
+          if (compressed.size() > 0)
+          {
+            area_.CreateAttachment(customData, resourceId, resourceType, uuid, &compressed[0], compressed.size(), type, true);
+          }
+          else
+          {
+            area_.CreateAttachment(customData, resourceId, resourceType, uuid, NULL, 0, type, true);
+          }
+        }
+
+        if (metrics_ != NULL)
+        {
+          metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, compressed.size());
+        }
+
+        if (cache_ != NULL)
+        {
+          StorageCache::Accessor cacheAccessor(*cache_);
+          cacheAccessor.Add(uuid, type, data, size);    // always add uncompressed data to cache
+        }
+
+        return FileInfo(uuid, type, size, md5,
+                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData);
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
   }
 
 
@@ -446,7 +526,7 @@
 
         {
           MetricsTimer timer(*this, METRICS_READ_DURATION);
-          buffer.reset(area_.Read(info.GetUuid(), info.GetContentType()));
+          buffer.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
         }
 
         if (metrics_ != NULL)
@@ -467,7 +547,7 @@
         
         {
           MetricsTimer timer(*this, METRICS_READ_DURATION);
-          compressed.reset(area_.Read(info.GetUuid(), info.GetContentType()));
+          compressed.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
         }
         
         if (metrics_ != NULL)
@@ -526,7 +606,7 @@
 
     {
       MetricsTimer timer(*this, METRICS_READ_DURATION);
-      buffer.reset(area_.Read(info.GetUuid(), info.GetContentType()));
+      buffer.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
     }
 
     if (metrics_ != NULL)
@@ -539,7 +619,8 @@
 
 
   void StorageAccessor::Remove(const std::string& fileUuid,
-                               FileContentType type)
+                               FileContentType type,
+                               const std::string& customData)
   {
     if (cache_ != NULL)
     {
@@ -548,14 +629,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 +697,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 +763,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_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData()));
       }
 
       buffer->MoveToString(target);
@@ -785,4 +866,5 @@
     output.AnswerStream(transcoder);
   }
 #endif
+
 }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Mon Feb 24 09:19:12 2025 +0100
@@ -133,16 +133,24 @@
                     StorageCache& cache,
                     MetricsRegistry& metrics);
 
-    FileInfo Write(const void* data,
-                   size_t size,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
+    FileInfo WriteInstance(std::string& customData,
+                           const DicomInstanceToStore& instance,
+                           const void* data,
+                           size_t size,
+                           FileContentType type,
+                           CompressionType compression,
+                           bool storeMd5,
+                           const std::string& uuid);
 
-    FileInfo Write(const std::string& data,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
+    FileInfo WriteAttachment(std::string& customData,
+                             const std::string& resourceId,
+                             ResourceType resourceType,
+                             const void* data,
+                             size_t size,
+                             FileContentType type,
+                             CompressionType compression,
+                             bool storeMd5,
+                             const std::string& uuid);
 
     void Read(std::string& content,
               const FileInfo& info);
@@ -155,7 +163,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 +194,9 @@
                     const std::string& mime,
                     const std::string& contentFilename);
 #endif
+
+    bool HandlesCustomData();
+
   private:
     void ReadStartRangeInternal(std::string& target,
                                 const FileInfo& info,
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -174,7 +174,8 @@
   StorageAccessor accessor(s, cache);
 
   std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true);
+  std::string uuid = Toolbox::GenerateUuid();
+  FileInfo info = accessor.WriteAttachment(data, "", ResourceType_Instance, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, uuid);
   
   std::string r;
   accessor.Read(r, info);
@@ -196,7 +197,8 @@
   StorageAccessor accessor(s, cache);
 
   std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true);
+  std::string uuid = Toolbox::GenerateUuid();
+  FileInfo info = accessor.WriteAttachment(data, "", ResourceType_Instance, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, uuid);
   
   std::string r;
   accessor.Read(r, info);
@@ -207,6 +209,7 @@
   ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
   ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5());
   ASSERT_NE(info.GetUncompressedMD5(), info.GetCompressedMD5());
+  ASSERT_EQ(uuid, info.GetUuid());
 }
 
 
@@ -220,9 +223,9 @@
   std::string compressedData = "Hello";
   std::string uncompressedData = "HelloWorld";
 
-  FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom, CompressionType_ZlibWithSize, false);  
-  FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom, CompressionType_None, false);
-  
+  FileInfo compressedInfo = accessor.WriteAttachment(compressedData, "", ResourceType_Instance, compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, Toolbox::GenerateUuid());
+  FileInfo uncompressedInfo = accessor.WriteAttachment(uncompressedData, "", ResourceType_Instance, uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, Toolbox::GenerateUuid());
+
   accessor.Read(r, compressedInfo);
   ASSERT_EQ(compressedData, r);
 
--- a/OrthancServer/CMakeLists.txt	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/CMakeLists.txt	Mon Feb 24 09:19:12 2025 +0100
@@ -243,15 +243,16 @@
 #####################################################################
 
 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  
   )
 
 if (STANDALONE_BUILD)
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -83,13 +83,15 @@
     
     static FileInfo Convert(const OrthancPluginAttachment& attachment)
     {
+      std::string customData;
       return FileInfo(attachment.uuid,
                       static_cast<FileContentType>(attachment.contentType),
                       attachment.uncompressedSize,
                       attachment.uncompressedHash,
                       static_cast<CompressionType>(attachment.compressionType),
                       attachment.compressedSize,
-                      attachment.compressedHash);
+                      attachment.compressedHash,
+                      customData);
     }
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -62,13 +62,15 @@
 
     static FileInfo Convert(const OrthancPluginAttachment& attachment)
     {
+      std::string customData;
       return FileInfo(attachment.uuid,
                       static_cast<FileContentType>(attachment.contentType),
                       attachment.uncompressedSize,
                       attachment.uncompressedHash,
                       static_cast<CompressionType>(attachment.compressionType),
                       attachment.compressedSize,
-                      attachment.compressedHash);
+                      attachment.compressedHash,
+                      customData);
     }
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -108,7 +108,8 @@
                     source.uncompressed_hash(),
                     static_cast<CompressionType>(source.compression_type()),
                     source.compressed_size(),
-                    source.compressed_hash());
+                    source.compressed_hash(),
+                    source.custom_data());
   }
 
 
@@ -576,6 +577,7 @@
       request.mutable_add_attachment()->mutable_attachment()->set_compression_type(attachment.GetCompressionType());
       request.mutable_add_attachment()->mutable_attachment()->set_compressed_size(attachment.GetCompressedSize());
       request.mutable_add_attachment()->mutable_attachment()->set_compressed_hash(attachment.GetCompressedMD5());        
+      request.mutable_add_attachment()->mutable_attachment()->set_custom_data(attachment.GetCustomData());        // new in 1.12.7
       request.mutable_add_attachment()->set_revision(revision);
 
       ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request);
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -59,6 +59,7 @@
 #include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/OrthancConfiguration.h"
 #include "../../Sources/OrthancFindRequestHandler.h"
+#include "../../Sources/Search/IDatabaseConstraint.h"
 #include "../../Sources/Search/HierarchicalMatcher.h"
 #include "../../Sources/ServerContext.h"
 #include "../../Sources/ServerToolbox.h"
@@ -79,6 +80,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:
@@ -550,8 +670,8 @@
       }
     };
   
-
-    class StorageAreaBase : public IStorageArea
+    // "legacy" storage plugins don't store customData -> derive from ICoreStorageArea
+    class PluginStorageAreaBase : public ICoreStorageArea
     {
     private:
       OrthancPluginStorageCreate create_;
@@ -605,9 +725,9 @@
       }      
       
     public:
-      StorageAreaBase(OrthancPluginStorageCreate create,
-                      OrthancPluginStorageRemove remove,
-                      PluginsErrorDictionary&  errorDictionary) : 
+      PluginStorageAreaBase(OrthancPluginStorageCreate create,
+                            OrthancPluginStorageRemove remove,
+                            PluginsErrorDictionary&  errorDictionary) : 
         create_(create),
         remove_(remove),
         errorDictionary_(errorDictionary)
@@ -649,7 +769,7 @@
     };
 
 
-    class PluginStorageArea : public StorageAreaBase
+    class PluginStorageArea : public PluginStorageAreaBase
     {
     private:
       OrthancPluginStorageRead   read_;
@@ -666,7 +786,7 @@
     public:
       PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks,
                         PluginsErrorDictionary&  errorDictionary) :
-        StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
+        PluginStorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
         read_(callbacks.read),
         free_(callbacks.free)
       {
@@ -715,7 +835,7 @@
 
 
     // New in Orthanc 1.9.0
-    class PluginStorageArea2 : public StorageAreaBase
+    class PluginStorageArea2 : public PluginStorageAreaBase
     {
     private:
       OrthancPluginStorageReadWhole  readWhole_;
@@ -724,7 +844,7 @@
     public:
       PluginStorageArea2(const _OrthancPluginRegisterStorageArea2& callbacks,
                          PluginsErrorDictionary&  errorDictionary) :
-        StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
+        PluginStorageAreaBase(callbacks.create, callbacks.remove, errorDictionary),
         readWhole_(callbacks.readWhole),
         readRange_(callbacks.readRange)
       {
@@ -809,19 +929,252 @@
     };
 
 
+    // New in Orthanc 1.12.7
+    class PluginStorageArea3 : public IStorageArea
+    {
+    private:
+      OrthancPluginStorageCreateInstance createInstance_;
+      OrthancPluginStorageCreateAttachment createAttachment_;
+      OrthancPluginStorageRemove2 remove2_;
+      OrthancPluginStorageReadWhole2  readWhole2_;
+      OrthancPluginStorageReadRange2  readRange2_;
+
+      PluginsErrorDictionary&    errorDictionary_;
+
+    protected:
+      PluginsErrorDictionary& GetErrorDictionary() const
+      {
+        return errorDictionary_;
+      }
+
+      IMemoryBuffer* RangeFromWhole(const std::string& uuid,
+                                    const std::string& customData,
+                                    FileContentType type,
+                                    uint64_t start /* inclusive */,
+                                    uint64_t end /* exclusive */)
+      {
+        if (start > end)
+        {
+          throw OrthancException(ErrorCode_BadRange);
+        }
+        else if (start == end)
+        {
+          return new StringMemoryBuffer;  // Empty
+        }
+        else
+        {
+          std::unique_ptr<IMemoryBuffer> whole(Read(uuid, type, customData));
+
+          if (start == 0 &&
+              end == whole->GetSize())
+          {
+            return whole.release();
+          }
+          else if (end > whole->GetSize())
+          {
+            throw OrthancException(ErrorCode_BadRange);
+          }
+          else
+          {
+            std::string range;
+            range.resize(end - start);
+            assert(!range.empty());
+            
+            memcpy(&range[0], reinterpret_cast<const char*>(whole->GetData()) + start, range.size());
+
+            whole.reset(NULL);
+            return StringMemoryBuffer::CreateFromSwap(range);
+          }
+        }
+      }      
+      
+    public:
+      PluginStorageArea3(const _OrthancPluginRegisterStorageArea3& callbacks,
+                      PluginsErrorDictionary&  errorDictionary) : 
+        createInstance_(callbacks.createInstance),
+        createAttachment_(callbacks.createAttachment),
+        remove2_(callbacks.remove),
+        readWhole2_(callbacks.readWhole),
+        readRange2_(callbacks.readRange),
+        errorDictionary_(errorDictionary)
+      {
+        if (createInstance_ == NULL ||
+            createAttachment_ == NULL ||
+            remove2_ == NULL ||
+            readWhole2_ == NULL)
+        {
+          throw OrthancException(ErrorCode_Plugin, "Storage area plugin doesn't implement all the required primitives (createInstance, createAttachment, remove, readWhole");
+        }
+      }
+
+      virtual void CreateInstance(std::string& customData,
+                                const DicomInstanceToStore& instance,
+                                const std::string& uuid,
+                                const void* content,
+                                size_t size,
+                                FileContentType type,
+                                bool isCompressed) ORTHANC_OVERRIDE
+      {
+        OrthancPluginMemoryBuffer customDataBuffer;
+        Orthanc::OrthancPlugins::DicomInstanceFromCallback wrapped(instance);
+
+        OrthancPluginErrorCode error = createInstance_(&customDataBuffer,
+                                                       uuid.c_str(),
+                                                       reinterpret_cast<OrthancPluginDicomInstance*>(&wrapped),
+                                                       content, size, Plugins::Convert(type),
+                                                       isCompressed);
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+
+        if (customDataBuffer.size > 0)
+        {
+          customData.assign(reinterpret_cast<char*>(customDataBuffer.data), 
+                            static_cast<size_t>(customDataBuffer.size));
+        }
+      }
+
+      virtual void CreateAttachment(std::string& customData,
+                                    const std::string& resourceId,
+                                    ResourceType resourceLevel,
+                                    const std::string& uuid,
+                                    const void* content,
+                                    size_t size,
+                                    FileContentType type,
+                                    bool isCompressed) ORTHANC_OVERRIDE
+      {
+        OrthancPluginMemoryBuffer customDataBuffer;
+
+        OrthancPluginErrorCode error = createAttachment_(&customDataBuffer,
+                                                         uuid.c_str(),
+                                                         resourceId.c_str(),
+                                                         Plugins::Convert(resourceLevel),
+                                                         content, size, Plugins::Convert(type),
+                                                         isCompressed);
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+
+        if (customDataBuffer.size > 0)
+        {
+          customData.assign(reinterpret_cast<char*>(customDataBuffer.data), 
+                            static_cast<size_t>(customDataBuffer.size));
+        }
+      }
+
+      virtual void Remove(const std::string& uuid,
+                          FileContentType type,
+                          const std::string& customData) ORTHANC_OVERRIDE
+      {
+        OrthancPluginErrorCode error = remove2_
+          (uuid.c_str(), customData.c_str(), Plugins::Convert(type));
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+      }
+
+      virtual IMemoryBuffer* Read(const std::string& uuid,
+                                  FileContentType type,
+                                  const std::string& customData) ORTHANC_OVERRIDE
+      {
+        std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer);
+
+        OrthancPluginMemoryBuffer64 buffer;
+        buffer.size = 0;
+        buffer.data = NULL;
+        
+        OrthancPluginErrorCode error = readWhole2_(&buffer, uuid.c_str(), customData.c_str(), Plugins::Convert(type));
+
+        if (error == OrthancPluginErrorCode_Success)
+        {
+          result->Assign(buffer.data, buffer.size, ::free);
+          return result.release();
+        }
+        else
+        {
+          GetErrorDictionary().LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+      }
+
+      virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                       FileContentType type,
+                                       uint64_t start /* inclusive */,
+                                       uint64_t end /* exclusive */,
+                                       const std::string& customData) ORTHANC_OVERRIDE
+      {
+        if (readRange2_ == NULL)
+        {
+          return RangeFromWhole(uuid, customData, type, start, end);
+        }
+        else
+        {
+          if (start > end)
+          {
+            throw OrthancException(ErrorCode_BadRange);
+          }
+          else if (start == end)
+          {
+            return new StringMemoryBuffer;
+          }
+          else
+          {
+            std::string range;
+            range.resize(end - start);
+            assert(!range.empty());
+
+            OrthancPluginMemoryBuffer64 buffer;
+            buffer.data = &range[0];
+            buffer.size = static_cast<uint64_t>(range.size());
+
+            OrthancPluginErrorCode error =
+              readRange2_(&buffer, uuid.c_str(), customData.c_str(), Plugins::Convert(type), start);
+
+            if (error == OrthancPluginErrorCode_Success)
+            {
+              return StringMemoryBuffer::CreateFromSwap(range);
+            }
+            else
+            {
+              GetErrorDictionary().LogError(error, true);
+              throw OrthancException(static_cast<ErrorCode>(error));
+            }
+          }
+        }
+      }
+
+      virtual bool HasReadRange() const ORTHANC_OVERRIDE
+      {
+        return (readRange2_ != NULL);
+      }
+
+    };
+
+
     class StorageAreaFactory : public boost::noncopyable
     {
     private:
       enum Version
       {
         Version1,
-        Version2
+        Version2,
+        Version3
       };
       
       SharedLibrary&                      sharedLibrary_;
       Version                             version_;
       _OrthancPluginRegisterStorageArea   callbacks_;
       _OrthancPluginRegisterStorageArea2  callbacks2_;
+      _OrthancPluginRegisterStorageArea3  callbacks3_;
       PluginsErrorDictionary&             errorDictionary_;
 
       static void WarnNoReadRange()
@@ -855,6 +1208,20 @@
         }
       }
 
+      StorageAreaFactory(SharedLibrary& sharedLibrary,
+                         const _OrthancPluginRegisterStorageArea3& callbacks,
+                         PluginsErrorDictionary&  errorDictionary) :
+        sharedLibrary_(sharedLibrary),
+        version_(Version3),
+        callbacks3_(callbacks),
+        errorDictionary_(errorDictionary)
+      {
+        if (callbacks.readRange == NULL)
+        {
+          WarnNoReadRange();
+        }
+      }
+
       SharedLibrary&  GetSharedLibrary()
       {
         return sharedLibrary_;
@@ -870,6 +1237,9 @@
           case Version2:
             return new PluginStorageArea2(callbacks2_, errorDictionary_);
 
+          case Version3:
+            return new PluginStorageArea3(callbacks3_, errorDictionary_);
+
           default:
             throw OrthancException(ErrorCode_InternalError);
         }
@@ -2527,125 +2897,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)
@@ -5069,7 +5320,8 @@
       {
         const _OrthancPluginStorageAreaCreate& p =
           *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters);
-        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
+        PluginStorageAreaBase& storage = *reinterpret_cast<PluginStorageAreaBase*>(p.storageArea);
+        std::string customDataNotUsed;
         storage.Create(p.uuid, p.content, static_cast<size_t>(p.size), Plugins::Convert(p.type));
         return true;
       }
@@ -5079,7 +5331,8 @@
         const _OrthancPluginStorageAreaRead& p =
           *reinterpret_cast<const _OrthancPluginStorageAreaRead*>(parameters);
         IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        std::unique_ptr<IMemoryBuffer> content(storage.Read(p.uuid, Plugins::Convert(p.type)));
+        std::string customDataNotUsed;
+        std::unique_ptr<IMemoryBuffer> content(storage.Read(p.uuid, Plugins::Convert(p.type), customDataNotUsed));
         CopyToMemoryBuffer(*p.target, content->GetData(), content->GetSize());
         return true;
       }
@@ -5089,7 +5342,8 @@
         const _OrthancPluginStorageAreaRemove& p =
           *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters);
         IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        storage.Remove(p.uuid, Plugins::Convert(p.type));
+        std::string customDataNotUsed;
+        storage.Remove(p.uuid, Plugins::Convert(p.type), customDataNotUsed);
         return true;
       }
 
@@ -5670,23 +5924,34 @@
 
       case _OrthancPluginService_RegisterStorageArea:
       case _OrthancPluginService_RegisterStorageArea2:
-      {
-        CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area";
-        
+      case _OrthancPluginService_RegisterStorageArea3:
+      {
         if (pimpl_->storageArea_.get() == NULL)
         {
           if (service == _OrthancPluginService_RegisterStorageArea)
           {
+            CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v1)";
+    
             const _OrthancPluginRegisterStorageArea& p = 
               *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters);
             pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary()));
           }
           else if (service == _OrthancPluginService_RegisterStorageArea2)
           {
+            CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v2)";
+
             const _OrthancPluginRegisterStorageArea2& p = 
               *reinterpret_cast<const _OrthancPluginRegisterStorageArea2*>(parameters);
             pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary()));
           }
+          else if (service == _OrthancPluginService_RegisterStorageArea3)
+          {
+            CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v3)";
+
+            const _OrthancPluginRegisterStorageArea3& p = 
+              *reinterpret_cast<const _OrthancPluginRegisterStorageArea3*>(parameters);
+            pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary()));
+          }
           else
           {
             throw OrthancException(ErrorCode_InternalError);
@@ -5809,7 +6074,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);
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Mon Feb 24 09:19:12 2025 +0100
@@ -88,11 +88,14 @@
     class HttpClientChunkedAnswer;
     class HttpServerChunkedReader;
     class IDicomInstance;
-    class DicomInstanceFromCallback;
     class DicomInstanceFromBuffer;
     class DicomInstanceFromParsed;
     class WebDavCollection;
-    
+
+public:
+    class DicomInstanceFromCallback;
+
+private:
     void RegisterRestCallback(const void* parameters,
                               bool lock);
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Mon Feb 24 09:19:12 2025 +0100
@@ -1361,7 +1361,7 @@
 
     return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, &params);
   }
-  
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Feb 24 09:19:12 2025 +0100
@@ -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  7
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -492,6 +492,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.7 */
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -562,7 +563,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,
@@ -1367,7 +1368,7 @@
    * @param type The content type corresponding to this file. 
    * @return 0 if success, other value if error.
    * @ingroup Callbacks
-   * @deprecated New plugins should use OrthancPluginStorageRead2
+   * @deprecated New plugins should use OrthancPluginStorageReadWhole2 and OrthancPluginStorageReadRange2
    * 
    * @warning The "content" buffer *must* have been allocated using
    * the "malloc()" function of your C standard library (i.e. nor
@@ -1442,6 +1443,121 @@
 
 
 
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes an instance to the storage area.
+   *
+   * @param customData The custom data of the attachment (out)
+   * @param uuid The UUID of the file.
+   * @param instance The DICOM instance being stored.
+   * @param content The content of the file (might be compressed data, hence the need for the DICOM instance arg to access tags).
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreateInstance) (
+    OrthancPluginMemoryBuffer* customData,
+    const char* uuid,
+    const OrthancPluginDicomInstance*  instance,
+    const void* content,
+    int64_t size,
+    OrthancPluginContentType type,
+    bool isCompressed);
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
+   *
+   * @param customData The custom data of the attachment (out)
+   * @param uuid The UUID of the file.
+   * @param resourceId The resource ID the file is attached to.
+   * @param resourceType The resource Type the file is attached to.
+   * @param content The content of the file (might be compressed data, hence the need for the DICOM instance arg to access tags).
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreateAttachment) (
+    OrthancPluginMemoryBuffer* customData,
+    const char* uuid,
+    const char* resourceId,
+    OrthancPluginResourceType resourceType,
+    const void* content,
+    int64_t size,
+    OrthancPluginContentType type,
+    bool isCompressed);
+
+
+
+  /**
+   * @brief Callback for reading a whole file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * reads a whole file from the storage area.
+   *
+   * @param target Memory buffer where to store the content of the file. It must be allocated by the
+   * plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it.
+   * @param uuid The UUID of the file of interest.
+   * @param customData The custom data of the file to be removed.
+   * @param type The content type corresponding to this file.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageReadWhole2) (
+    OrthancPluginMemoryBuffer64* target,
+    const char* uuid,
+    const char* customData,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for reading a range of a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * reads a portion of a file from the storage area. Orthanc
+   * indicates the start position and the length of the range.
+   *
+   * @param target Memory buffer where to store the content of the range.
+   * The memory buffer is allocated and freed by Orthanc. The length of the range
+   * of interest corresponds to the size of this buffer.
+   * @param uuid The UUID of the file of interest.
+   * @param customData The custom data of the file to be removed.
+   * @param type The content type corresponding to this file.
+   * @param rangeStart Start position of the requested range in the file.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) (
+    OrthancPluginMemoryBuffer64* target,
+    const char* uuid,
+    const char* customData,
+    OrthancPluginContentType type,
+    uint64_t rangeStart);
+
+
+
+  /**
+   * @brief Callback for removing a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area.
+   *
+   * @param uuid The UUID of the file to be removed.
+   * @param customData The custom data of the file to be removed.
+   * @param type The content type corresponding to this file.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) (
+    const char* uuid,
+    const char* customData,
+    OrthancPluginContentType type);
+
+
   /**
    * @brief Callback to handle the C-Find SCP requests for worklists.
    *
@@ -9364,6 +9480,48 @@
   }
 
 
+  typedef struct
+  {
+    OrthancPluginStorageCreateInstance    createInstance;
+    OrthancPluginStorageCreateAttachment  createAttachment;
+    OrthancPluginStorageReadWhole2        readWhole;
+    OrthancPluginStorageReadRange2        readRange;
+    OrthancPluginStorageRemove2           remove;
+  } _OrthancPluginRegisterStorageArea3;
+
+  /**
+   * @brief Register a custom storage area, with support for custom data.
+   *
+   * This function registers a custom storage area, to replace the
+   * built-in way Orthanc stores its files on the filesystem. This
+   * function must be called during the initialization of the plugin,
+   * i.e. inside the OrthancPluginInitialize() public function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param create The callback function to store a file on the custom storage area.
+   * @param readWhole The callback function to read a whole file from the custom storage area.
+   * @param readRange The callback function to read some range of a file from the custom storage area.
+   * If this feature is not supported by the plugin, this value can be set to NULL.
+   * @param remove The callback function to remove a file from the custom storage area.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3(
+    OrthancPluginContext*                 context,
+    OrthancPluginStorageCreateInstance    createInstance,
+    OrthancPluginStorageCreateAttachment  createAttachement,
+    OrthancPluginStorageReadWhole2        readWhole,
+    OrthancPluginStorageReadRange2        readRange,
+    OrthancPluginStorageRemove2           remove)
+  {
+    _OrthancPluginRegisterStorageArea3 params;
+    params.createAttachment = createAttachement;
+    params.createInstance = createInstance;
+    params.readWhole = readWhole;
+    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.
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Mon Feb 24 09:19:12 2025 +0100
@@ -55,6 +55,7 @@
   int32   compression_type = 5;  // opaque "CompressionType" in Orthanc
   uint64  compressed_size = 6;
   string  compressed_hash = 7;
+  string  custom_data = 8;       // added in v 1.12.7
 }
 
 enum ResourceType {
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Feb 24 09:19:12 2025 +0100
@@ -56,6 +56,7 @@
       bool hasMeasureLatency_;
       bool hasFindSupport_;
       bool hasExtendedChanges_;
+      bool hasAttachmentCustomDataSupport_;
 
     public:
       Capabilities() :
@@ -66,7 +67,8 @@
         hasUpdateAndGetStatistics_(false),
         hasMeasureLatency_(false),
         hasFindSupport_(false),
-        hasExtendedChanges_(false)
+        hasExtendedChanges_(false),
+        hasAttachmentCustomDataSupport_(false)
       {
       }
 
@@ -100,6 +102,16 @@
         return hasLabelsSupport_;
       }
 
+      void SetAttachmentCustomDataSupport(bool value)
+      {
+        hasAttachmentCustomDataSupport_ = value;
+      }
+
+      bool HasAttachmentCustomDataSupport() const
+      {
+        return hasAttachmentCustomDataSupport_;
+      }
+      
       void SetHasExtendedChanges(bool value)
       {
         hasExtendedChanges_ = value;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql	Mon Feb 24 09:19:12 2025 +0100
@@ -0,0 +1,66 @@
+-- Orthanc - A Lightweight, RESTful DICOM Store
+-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+-- Department, University Hospital of Liege, Belgium
+-- Copyright (C) 2017-2022 Osimis S.A., Belgium
+-- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+--
+-- This program is free software: you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License as
+-- published by the Free Software Foundation, either version 3 of the
+-- License, or (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+--
+-- This SQLite script installs revision and customData without changing the Orthanc database version
+--
+
+-- Add new columns for revision
+ALTER TABLE Metadata ADD COLUMN revision INTEGER;
+ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER;
+
+-- Add new column for customData
+ALTER TABLE AttachedFiles ADD COLUMN customData TEXT;
+
+
+-- add another AttachedFileDeleted trigger 
+-- We want to keep backward compatibility and avoid changing the database version number (which would force
+-- users to upgrade the DB).  By keeping backward compatibility, we mean "allow a user to run a previous Orthanc
+-- version after it has run this update script".
+-- We must keep the signature of the initial trigger (it is impossible to have 2 triggers on the same event).
+-- We tried adding a trigger on "BEFORE DELETE" but then it is being called when running the previous Orthanc
+-- which makes it fail.
+-- But, we need the customData in the C++ function that is called when a AttachedFiles is deleted.
+-- The trick is then to save the customData in a DeletedFiles table.
+-- The SignalFileDeleted C++ function will then get the customData from this table and delete the entry.
+-- Drawback: if you downgrade Orthanc, the DeletedFiles table will remain and will be populated by the trigger
+-- but not consumed by the C++ function -> we consider this is an acceptable drawback for a few people compared 
+-- to the burden of upgrading the DB.
+
+CREATE TABLE DeletedFiles(
+       uuid TEXT NOT NULL,        -- 0
+       customData TEXT            -- 1
+);
+
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  INSERT INTO DeletedFiles VALUES(old.uuid, old.customData);
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           old.uncompressedMD5, old.compressedMD5
+                           );
+END;
+
+-- Record that this upgrade has been performed
+
+INSERT INTO GlobalProperties VALUES (7, 1);  -- GlobalProperty_SQLiteHasCustomDataAndRevision
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Sources/Database/PrepareDatabase.sql	Mon Feb 24 09:19:12 2025 +0100
@@ -55,6 +55,7 @@
        id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
        type INTEGER,
        value TEXT,
+       -- revision INTEGER,      -- New in Orthanc 1.12.7 (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.7 (added in InstallRevisionAndCustomData.sql)
+       -- customData TEXT,       -- New in Orthanc 1.12.7 (added in InstallRevisionAndCustomData.sql)
        PRIMARY KEY(id, fileType)
        );              
 
@@ -129,7 +132,8 @@
   SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
                            old.compressionType, old.compressedSize,
                            -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
-                           old.uncompressedMD5, old.compressedMD5);
+                           old.uncompressedMD5, old.compressedMD5
+                           );
 END;
 
 CREATE TRIGGER ResourceDeleted
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -41,6 +41,8 @@
 #include <stdio.h>
 #include <boost/lexical_cast.hpp>
 
+static std::map<std::string, std::string> filesToDeleteCustomData;
+
 namespace Orthanc
 {  
   static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec)
@@ -410,8 +412,9 @@
                                const FileInfo& attachment,
                                int64_t revision) ORTHANC_OVERRIDE
     {
-      // TODO - REVISIONS
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5) VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+        "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5, revision, customData) "
+        "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
       s.BindInt64(0, id);
       s.BindInt(1, attachment.GetContentType());
       s.BindString(2, attachment.GetUuid());
@@ -420,10 +423,11 @@
       s.BindInt(5, attachment.GetCompressionType());
       s.BindString(6, attachment.GetUncompressedMD5());
       s.BindString(7, attachment.GetCompressedMD5());
+      s.BindInt(8, revision);
+      s.BindString(9, attachment.GetCustomData());
       s.Run();
     }
 
-
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
                                       const DatabaseDicomTagConstraints& lookup,
@@ -473,10 +477,12 @@
 #define C3_STRING_1 3
 #define C4_STRING_2 4
 #define C5_STRING_3 5
-#define C6_INT_1 6
-#define C7_INT_2 7
-#define C8_BIG_INT_1 8
-#define C9_BIG_INT_2 9
+#define C6_STRING_4 6
+#define C7_INT_1 7
+#define C8_INT_2 8
+#define C9_INT_3 9
+#define C10_BIG_INT_1 10
+#define C11_BIG_INT_2 11
 
 #define QUERY_LOOKUP 1
 #define QUERY_MAIN_DICOM_TAGS 2
@@ -588,10 +594,12 @@
              "  Lookup.publicId AS c3_string1, "
              "  NULL AS c4_string2, "
              "  NULL AS c5_string3, "
-             "  NULL AS c6_int1, "
-             "  NULL AS c7_int2, "
-             "  NULL AS c8_big_int1, "
-             "  NULL AS c9_big_int2 "
+             "  NULL AS c6_string4, "
+             "  NULL AS c7_int1, "
+             "  NULL AS c8_int2, "
+             "  NULL AS c9_int3, "
+             "  NULL AS c10_big_int1, "
+             "  NULL AS c11_big_int2 "
              "  FROM Lookup ";
 
       // need one instance info ? (part 2: execute the queries)
@@ -605,10 +613,12 @@
                "    instancePublicId AS c3_string1, "
                "    NULL AS c4_string2, "
                "    NULL AS c5_string3, "
-               "    NULL AS c6_int1, "
-               "    NULL AS c7_int2, "
-               "    instanceInternalId AS c8_big_int1, "
-               "    NULL AS c9_big_int2 "
+               "    NULL AS c6_string4, "
+               "    NULL AS c7_int1, "
+               "    NULL AS c8_int2, "
+               "    NULL AS c9_int3, "
+               "    instanceInternalId AS c10_big_int1, "
+               "    NULL AS c11_big_int2 "
                "   FROM OneInstance ";
 
         sql += "   UNION SELECT"
@@ -618,10 +628,12 @@
                "    Metadata.value AS c3_string1, "
                "    NULL AS c4_string2, "
                "    NULL AS c5_string3, "
-               "    Metadata.type AS c6_int1, "
-               "    NULL AS c7_int2, "
-               "    NULL AS c8_big_int1, "
-               "    NULL AS c9_big_int2 "
+               "    NULL AS c6_string4, "
+               "    Metadata.type AS c7_int1, "
+               "    NULL AS c8_int2, "
+               "    NULL AS c9_int3, "
+               "    NULL AS c10_big_int1, "
+               "    NULL AS c11_big_int2 "
                "   FROM OneInstance "
                "   INNER JOIN Metadata ON Metadata.id = OneInstance.instanceInternalId ";
               
@@ -632,10 +644,12 @@
                "    uuid AS c3_string1, "
                "    uncompressedMD5 AS c4_string2, "
                "    compressedMD5 AS c5_string3, "
-               "    fileType AS c6_int1, "
-               "    compressionType AS c7_int2, "
-               "    compressedSize AS c8_big_int1, "
-               "    uncompressedSize AS c9_big_int2 "
+               "    customData AS c6_string4, "
+               "    fileType AS c7_int1, "
+               "    compressionType AS c8_int2, "
+               "    revision AS c9_int3, "
+               "    compressedSize AS c10_big_int1, "
+               "    uncompressedSize AS c11_big_int2 "
                "   FROM OneInstance "
                "   INNER JOIN AttachedFiles ON AttachedFiles.id = OneInstance.instanceInternalId ";
 
@@ -651,10 +665,12 @@
                "  value AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  tagGroup AS c6_int1, "
-               "  tagElement AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  tagGroup AS c7_int1, "
+               "  tagElement AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId ";
       }
@@ -669,10 +685,12 @@
                "  value AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  type AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  type AS c7_int1, "
+               "  revision AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN Metadata ON Metadata.id = Lookup.internalId ";
       }
@@ -687,10 +705,12 @@
                "  uuid AS c3_string1, "
                "  uncompressedMD5 AS c4_string2, "
                "  compressedMD5 AS c5_string3, "
-               "  fileType AS c6_int1, "
-               "  compressionType AS c7_int2, "
-               "  compressedSize AS c8_big_int1, "
-               "  uncompressedSize AS c9_big_int2 "
+               "  customData AS c6_string4, "
+               "  fileType AS c7_int1, "
+               "  compressionType AS c8_int2, "
+               "  revision AS c9_int3, "
+               "  compressedSize AS c10_big_int1, "
+               "  uncompressedSize AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId ";
       }
@@ -706,10 +726,12 @@
                "  label AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  NULL AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  NULL AS c7_int1, "
+               "  NULL AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "INNER JOIN Labels ON Labels.id = Lookup.internalId ";
       }
@@ -726,10 +748,12 @@
                  "  value AS c3_string1, "
                  "  NULL AS c4_string2, "
                  "  NULL AS c5_string3, "
-                 "  tagGroup AS c6_int1, "
-                 "  tagElement AS c7_int2, "
-                 "  NULL AS c8_big_int1, "
-                 "  NULL AS c9_big_int2 "
+                 "  NULL AS c6_string4, "
+                 "  tagGroup AS c7_int1, "
+                 "  tagElement AS c8_int2, "
+                 "  NULL AS c9_int3, "
+                 "  NULL AS c10_big_int1, "
+                 "  NULL AS c11_big_int2 "
                  "FROM Lookup "
                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                  "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId ";
@@ -745,10 +769,12 @@
                  "  value AS c3_string1, "
                  "  NULL AS c4_string2, "
                  "  NULL AS c5_string3, "
-                 "  type AS c6_int1, "
-                 "  NULL AS c7_int2, "
-                 "  NULL AS c8_big_int1, "
-                 "  NULL AS c9_big_int2 "
+                 "  NULL AS c6_string4, "
+                 "  type AS c7_int1, "
+                 "  revision AS c8_int2, "
+                 "  NULL AS c9_int3, "
+                 "  NULL AS c10_big_int1, "
+                 "  NULL AS c11_big_int2 "
                  "FROM Lookup "
                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                  "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId ";        
@@ -766,10 +792,12 @@
                   "  value AS c3_string1, "
                   "  NULL AS c4_string2, "
                   "  NULL AS c5_string3, "
-                  "  tagGroup AS c6_int1, "
-                  "  tagElement AS c7_int2, "
-                  "  NULL AS c8_big_int1, "
-                  "  NULL AS c9_big_int2 "
+                  "  NULL AS c6_string4, "
+                  "  tagGroup AS c7_int1, "
+                  "  tagElement AS c8_int2, "
+                  "  NULL AS c9_int3, "
+                  "  NULL AS c10_big_int1, "
+                  "  NULL AS c11_big_int2 "
                   "FROM Lookup "
                   "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                   "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -786,10 +814,12 @@
                   "  value AS c3_string1, "
                   "  NULL AS c4_string2, "
                   "  NULL AS c5_string3, "
-                  "  type AS c6_int1, "
-                  "  NULL AS c7_int2, "
-                  "  NULL AS c8_big_int1, "
-                  "  NULL AS c9_big_int2 "
+                  "  NULL AS c6_string4, "
+                  "  type AS c7_int1, "
+                  "  revision AS c8_int2, "
+                  "  NULL AS c9_int3, "
+                  "  NULL AS c10_big_int1, "
+                  "  NULL AS c11_big_int2 "
                   "FROM Lookup "
                   "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                   "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -808,10 +838,12 @@
                "  value AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  tagGroup AS c6_int1, "
-               "  tagElement AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  tagGroup AS c7_int1, "
+               "  tagElement AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                "  INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))); 
@@ -827,10 +859,12 @@
                 "  value AS c3_string1, "
                 "  NULL AS c4_string2, "
                 "  NULL AS c5_string3, "
-                "  tagGroup AS c6_int1, "
-                "  tagElement AS c7_int2, "
-                "  NULL AS c8_big_int1, "
-                "  NULL AS c9_big_int2 "
+                "  NULL AS c6_string4, "
+                "  tagGroup AS c7_int1, "
+                "  tagElement AS c8_int2, "
+                "  NULL AS c9_int3, "
+                "  NULL AS c10_big_int1, "
+                "  NULL AS c11_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -847,10 +881,12 @@
                "  parentLevel.publicId AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  NULL AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  NULL AS c7_int1, "
+               "  NULL AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId "
                "  INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId ";
@@ -866,10 +902,12 @@
                 "  value AS c3_string1, "
                 "  NULL AS c4_string2, "
                 "  NULL AS c5_string3, "
-                "  type AS c6_int1, "
-                "  NULL AS c7_int2, "
-                "  NULL AS c8_big_int1, "
-                "  NULL AS c9_big_int2 "
+                "  NULL AS c6_string4, "
+                "  type AS c7_int1, "
+                "  revision AS c8_int2, "
+                "  NULL AS c9_int3, "
+                "  NULL AS c10_big_int1, "
+                "  NULL AS c11_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))) + ") ";
@@ -885,10 +923,12 @@
                 "  value AS c3_string1, "
                 "  NULL AS c4_string2, "
                 "  NULL AS c5_string3, "
-                "  type AS c6_int1, "
-                "  NULL AS c7_int2, "
-                "  NULL AS c8_big_int1, "
-                "  NULL AS c9_big_int2 "
+                "  NULL AS c6_string4, "
+                "  type AS c7_int1, "
+                "  revision AS c8_int2, "
+                "  NULL AS c9_int3, "
+                "  NULL AS c10_big_int1, "
+                "  NULL AS c11_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -907,10 +947,12 @@
                "  childLevel.publicId AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  NULL AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  NULL AS c7_int1, "
+               "  NULL AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId ";
       }
@@ -926,10 +968,12 @@
                "  NULL AS c3_string1, "
                "  NULL AS c4_string2, "
                "  NULL AS c5_string3, "
-               "  COUNT(*) AS c6_int1, "
-               "  NULL AS c7_int2, "
-               "  NULL AS c8_big_int1, "
-               "  NULL AS c9_big_int2 "
+               "  NULL AS c6_string4, "
+               "  COUNT(*) AS c7_int1, "
+               "  NULL AS c8_int2, "
+               "  NULL AS c9_int3, "
+               "  NULL AS c10_big_int1, "
+               "  NULL AS c11_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId GROUP BY Lookup.internalId ";
       }
@@ -945,10 +989,12 @@
               "  grandChildLevel.publicId AS c3_string1, "
               "  NULL AS c4_string2, "
               "  NULL AS c5_string3, "
-              "  NULL AS c6_int1, "
-              "  NULL AS c7_int2, "
-              "  NULL AS c8_big_int1, "
-              "  NULL AS c9_big_int2 "
+              "  NULL AS c6_string4, "
+              "  NULL AS c7_int1, "
+              "  NULL AS c8_int2, "
+              "  NULL AS c9_int3, "
+              "  NULL AS c10_big_int1, "
+              "  NULL AS c11_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId ";
@@ -964,10 +1010,12 @@
               "  NULL AS c3_string1, "
               "  NULL AS c4_string2, "
               "  NULL AS c5_string3, "
-               "  COUNT(*) AS c6_int1, "
-              "  NULL AS c7_int2, "
-              "  NULL AS c8_big_int1, "
-              "  NULL AS c9_big_int2 "
+              "  NULL AS c6_string4, "
+              "  COUNT(*) AS c7_int1, "
+              "  NULL AS c8_int2, "
+              "  NULL AS c9_int3, "
+              "  NULL AS c10_big_int1, "
+              "  NULL AS c11_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId GROUP BY Lookup.internalId ";
@@ -983,10 +1031,12 @@
               "  grandGrandChildLevel.publicId AS c3_string1, "
               "  NULL AS c4_string2, "
               "  NULL AS c5_string3, "
-              "  NULL AS c6_int1, "
-              "  NULL AS c7_int2, "
-              "  NULL AS c8_big_int1, "
-              "  NULL AS c9_big_int2 "
+              "  NULL AS c6_string4, "
+              "  NULL AS c7_int1, "
+              "  NULL AS c8_int2, "
+              "  NULL AS c9_int3, "
+              "  NULL AS c10_big_int1, "
+              "  NULL AS c11_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
@@ -1002,10 +1052,12 @@
               "  NULL AS c3_string1, "
               "  NULL AS c4_string2, "
               "  NULL AS c5_string3, "
-               "  COUNT(*) AS c6_int1, "
-              "  NULL AS c7_int2, "
-              "  NULL AS c8_big_int1, "
-              "  NULL AS c9_big_int2 "
+              "  NULL AS c6_string4, "
+              "  COUNT(*) AS c7_int1, "
+              "  NULL AS c8_int2, "
+              "  NULL AS c9_int3, "
+              "  NULL AS c10_big_int1, "
+              "  NULL AS c11_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
@@ -1043,19 +1095,19 @@
           case QUERY_ATTACHMENTS:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
-            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)),
-                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2),
-                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
-                          s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3));
-            res.AddAttachment(file, 0 /* TODO - REVISIONS */);
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)),
+                          s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C8_INT_2)),
+                          s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3), s.ColumnString(C6_STRING_4));
+            res.AddAttachment(file, s.ColumnInt(C9_INT_3));
           }; break;
 
           case QUERY_MAIN_DICOM_TAGS:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddStringDicomTag(requestLevel, 
-                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
-                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C8_INT_2)),
                                   s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1063,8 +1115,8 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 1), 
-                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
-                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C8_INT_2)),
                                   s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1072,8 +1124,8 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 2), 
-                                  static_cast<uint16_t>(s.ColumnInt(C6_INT_1)),
-                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_2)),
+                                  static_cast<uint16_t>(s.ColumnInt(C7_INT_1)),
+                                  static_cast<uint16_t>(s.ColumnInt(C8_INT_2)),
                                   s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1081,7 +1133,7 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 1), 
-                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))),
+                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))),
                                              s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1089,7 +1141,7 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 2), 
-                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))),
+                                             DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))),
                                              s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1097,31 +1149,31 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddMetadata(static_cast<ResourceType>(requestLevel), 
-                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
-                            s.ColumnString(C3_STRING_1), 0 /* no support for revision */);
+                            static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
+                            s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2));
           }; break;
 
           case QUERY_PARENT_METADATA:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddMetadata(static_cast<ResourceType>(requestLevel - 1), 
-                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
-                            s.ColumnString(C3_STRING_1), 0 /* no support for revision */);
+                            static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
+                            s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2));
           }; break;
 
           case QUERY_GRAND_PARENT_METADATA:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddMetadata(static_cast<ResourceType>(requestLevel - 2), 
-                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
-                            s.ColumnString(C3_STRING_1), 0 /* no support for revision */);
+                            static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
+                            s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2));
           }; break;
 
           case QUERY_CHILDREN_METADATA:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 1), 
-                                         static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                                         static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
                                          s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1129,7 +1181,7 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 2), 
-                                         static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                                         static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
                                          s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1170,21 +1222,21 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 1),
-                                 static_cast<uint64_t>(s.ColumnInt64(C6_INT_1)));
+                                 static_cast<uint64_t>(s.ColumnInt64(C7_INT_1)));
           }; break;
 
           case QUERY_GRAND_CHILDREN_COUNT:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 2),
-                                 static_cast<uint64_t>(s.ColumnInt64(C6_INT_1)));
+                                 static_cast<uint64_t>(s.ColumnInt64(C7_INT_1)));
           }; break;
 
           case QUERY_GRAND_GRAND_CHILDREN_COUNT:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 3),
-                                 static_cast<uint64_t>(s.ColumnInt64(C6_INT_1)));
+                                 static_cast<uint64_t>(s.ColumnInt64(C7_INT_1)));
           }; break;
 
           case QUERY_ONE_INSTANCE_IDENTIFIER:
@@ -1196,16 +1248,16 @@
           case QUERY_ONE_INSTANCE_METADATA:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
-            res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), s.ColumnString(C3_STRING_1));
+            res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1));
           }; break;
 
           case QUERY_ONE_INSTANCE_ATTACHMENTS:
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
-            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)),
-                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2),
-                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
-                          s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3));
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)),
+                          s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C8_INT_2)),
+                          s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3), s.ColumnString(C6_STRING_4));
             res.AddOneInstanceAttachment(file);
           }; break;
 
@@ -1312,6 +1364,28 @@
       }
     }
 
+    void DeleteDeletedFile(const std::string& uuid)
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DeletedFiles WHERE uuid=?");
+      s.BindString(0, uuid);
+      s.Run();
+    }
+
+    void GetDeletedFileCustomData(std::string& customData, const std::string& uuid)
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                          "SELECT customData FROM DeletedFiles WHERE uuid=?");
+      s.BindString(0, uuid);
+    
+      if (s.Step())
+      { 
+        customData = s.ColumnString(0);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+    }
 
     virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
                                 int64_t id) ORTHANC_OVERRIDE
@@ -1687,7 +1761,7 @@
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE, 
                           "SELECT uuid, uncompressedSize, compressionType, compressedSize, "
-                          "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?");
+                          "uncompressedMD5, compressedMD5, revision, customData FROM AttachedFiles WHERE id=? AND fileType=?");
       s.BindInt64(0, id);
       s.BindInt(1, contentType);
 
@@ -1703,8 +1777,9 @@
                               s.ColumnString(4),
                               static_cast<CompressionType>(s.ColumnInt(2)),
                               s.ColumnInt64(3),
-                              s.ColumnString(5));
-        revision = 0;   // TODO - REVISIONS
+                              s.ColumnString(5),
+                              s.ColumnString(7));
+        revision = s.ColumnInt(6);
         return true;
       }
     }
@@ -1739,7 +1814,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 +1825,7 @@
       else
       {
         target = s.ColumnString(0);
-        revision = 0;   // TODO - REVISIONS
+        revision = s.ColumnInt(1);
         return true;
       }
     }
@@ -1922,11 +1997,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();
     }
 
@@ -2055,6 +2130,11 @@
     {
       if (sqlite_.activeTransaction_ != NULL)
       {
+        std::string id = context.GetStringValue(0);
+
+        std::string customData;
+        sqlite_.activeTransaction_->GetDeletedFileCustomData(customData, id);
+
         std::string uncompressedMD5, compressedMD5;
 
         if (!context.IsNullValue(5))
@@ -2073,9 +2153,11 @@
                       uncompressedMD5,
                       static_cast<CompressionType>(context.GetIntValue(3)),
                       static_cast<uint64_t>(context.GetInt64Value(4)),
-                      compressedMD5);
+                      compressedMD5,
+                      customData);
 
         sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info);
+        sqlite_.activeTransaction_->DeleteDeletedFile(id);
       }
     }
   };
@@ -2332,6 +2414,19 @@
         }
       }
 
+      // New in Orthanc 1.12.7
+      if (version_ >= 6)
+      {
+        if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasCustomDataAndRevision, true /* unused in SQLite */) 
+            || tmp != "1")
+        {
+          LOG(INFO) << "Upgrading SQLite schema to support revision and customData";
+          std::string query;
+          ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA);
+          db_.Execute(query);
+        }
+      }
+
       transaction->Commit(0);
     }
   }
@@ -2362,7 +2457,7 @@
   {
     boost::mutex::scoped_lock lock(mutex_);
 
-    if (targetVersion != 6)
+    if (targetVersion != 7)
     {
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
     }
@@ -2372,7 +2467,8 @@
     if (version_ != 3 &&
         version_ != 4 &&
         version_ != 5 &&
-        version_ != 6)
+        version_ != 6 &&
+        version_ != 7)
     {
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
     }
@@ -2413,6 +2509,7 @@
       
       version_ = 6;
     }
+
   }
 
 
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -452,7 +452,7 @@
   {
     // Anonymous namespace to avoid clashes between compilation modules
 
-    class FilesystemStorageWithoutDicom : public IStorageArea
+    class FilesystemStorageWithoutDicom : public ICoreStorageArea
     {
     private:
       FilesystemStorage storage_;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -2664,7 +2664,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/ServerContext.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -593,10 +593,11 @@
 
 
   void ServerContext::RemoveFile(const std::string& fileUuid,
-                                 FileContentType type)
+                                 FileContentType type,
+                                 const std::string& customData)
   {
     StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-    accessor.Remove(fileUuid, type);
+    accessor.Remove(fileUuid, type, customData);
   }
 
 
@@ -707,8 +708,11 @@
       // TODO Should we use "gzip" instead?
       CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
-      FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), 
-                                          FileContentType_Dicom, compression, storeMD5_);
+      std::string dicomCustomData;
+      std::string dicomUuid = Toolbox::GenerateUuid();
+
+      FileInfo dicomInfo = accessor.WriteInstance(dicomCustomData, dicom, dicom.GetBufferData(), dicom.GetBufferSize(), 
+                                          FileContentType_Dicom, compression, storeMD5_, dicomUuid);
 
       ServerIndex::Attachments attachments;
       attachments.push_back(dicomInfo);
@@ -718,8 +722,11 @@
           (!area_.HasReadRange() ||
            compressionEnabled_))
       {
-        dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, 
-                                             FileContentType_DicomUntilPixelData, compression, storeMD5_);
+        std::string dicomHeaderCustomData;
+        std::string dicomHeaderUuid = Toolbox::GenerateUuid();
+
+        dicomUntilPixelData = accessor.WriteInstance(dicomHeaderCustomData, dicom, dicom.GetBufferData(), pixelDataOffset, 
+                                             FileContentType_DicomUntilPixelData, compression, storeMD5_, dicomHeaderUuid);
         attachments.push_back(dicomUntilPixelData);
       }
 
@@ -1018,8 +1025,24 @@
     StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     accessor.Read(content, attachment);
 
-    FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(),
-                                       content.size(), attachmentType, compression, storeMD5_);
+    std::string newUuid = Toolbox::GenerateUuid();
+    std::string newCustomData;
+    FileInfo modified;
+
+    // if (attachmentType == FileContentType_Dicom || attachmentType == FileContentType_DicomUntilPixelData)
+    // {
+    //   // DicomInstanceToStore instance;
+    //   // TODO_CUSTOM_DATA: get the Instance such that we can call accessor.GetCustomData ...
+    //   // modified = accessor.WriteInstance(newCustomData, instance, content.empty() ? NULL : content.c_str(),
+    //   //                                 content.size(), attachmentType, compression, storeMD5_, newUuid);
+    // }
+    // else
+    {
+      modified = accessor.WriteAttachment(newCustomData, resourceId, level, content.empty() ? NULL : content.c_str(),
+                                          content.size(), attachmentType, compression, storeMD5_, newUuid);
+    }
+
+
 
     try
     {
@@ -1283,9 +1306,9 @@
                 compressionEnabled_)
             {
               int64_t newRevision;
-              AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData,
+              AddAttachment(newRevision, instancePublicId, ResourceType_Instance, FileContentType_DicomUntilPixelData,
                             dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset,
-                            false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */);
+                             false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */);
             }
           }
         }
@@ -1513,6 +1536,7 @@
 
   bool ServerContext::AddAttachment(int64_t& newRevision,
                                     const std::string& resourceId,
+                                    ResourceType resourceType,
                                     FileContentType attachmentType,
                                     const void* data,
                                     size_t size,
@@ -1526,7 +1550,13 @@
     CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
     StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
-    FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
+
+    std::string uuid = Toolbox::GenerateUuid();
+    std::string customData;
+
+    assert(attachmentType != FileContentType_Dicom && attachmentType != FileContentType_DicomUntilPixelData); // this method can not be used to store instances
+
+    FileInfo attachment = accessor.WriteAttachment(customData, resourceId, resourceType, data, size, attachmentType, compression, storeMD5_, uuid);
 
     try
     {
--- a/OrthancServer/Sources/ServerContext.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Mon Feb 24 09:19:12 2025 +0100
@@ -271,7 +271,8 @@
 
     // This method must only be called from "ServerIndex"!
     void RemoveFile(const std::string& fileUuid,
-                    FileContentType type);
+                    FileContentType type,
+                    const std::string& customData);
 
     // This DicomModification object is intended to be used as a
     // "rules engine" when de-identifying logs for C-Find, C-Get, and
@@ -344,6 +345,7 @@
 
     bool AddAttachment(int64_t& newRevision,
                        const std::string& resourceId,
+                       ResourceType resourceType,
                        FileContentType attachmentType,
                        const void* data,
                        size_t size,
--- a/OrthancServer/Sources/ServerEnumerations.h	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Sources/ServerEnumerations.h	Mon Feb 24 09:19:12 2025 +0100
@@ -171,6 +171,7 @@
     GlobalProperty_AnonymizationSequence = 3,
     GlobalProperty_JobsRegistry = 5,
     GlobalProperty_GetTotalSizeIsFast = 6,      // New in Orthanc 1.5.2
+    GlobalProperty_SQLiteHasCustomDataAndRevision = 7,     // New in Orthanc 1.12.7
     GlobalProperty_Modalities = 20,             // New in Orthanc 1.5.0
     GlobalProperty_Peers = 21,                  // New in Orthanc 1.5.0
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/Sources/ServerIndex.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -45,12 +45,14 @@
     struct FileToRemove
     {
     private:
-      std::string  uuid_;
-      FileContentType  type_;
+      std::string       uuid_;
+      std::string       customData_;
+      FileContentType   type_;
 
     public:
       explicit FileToRemove(const FileInfo& info) :
-        uuid_(info.GetUuid()), 
+        uuid_(info.GetUuid()),
+        customData_(info.GetCustomData()),
         type_(info.GetContentType())
       {
       }
@@ -60,6 +62,11 @@
         return uuid_;
       }
 
+      const std::string& GetCustomData() const
+      {
+        return customData_;
+      }
+
       FileContentType GetContentType() const 
       {
         return type_;
@@ -93,7 +100,7 @@
       {
         try
         {
-          context_.RemoveFile(it->GetUuid(), it->GetContentType());
+          context_.RemoveFile(it->GetUuid(), it->GetContentType(), it->GetCustomData());
         }
         catch (OrthancException& e)
         {
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Mon Feb 24 09:09:31 2025 +0100
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Mon Feb 24 09:19:12 2025 +0100
@@ -296,11 +296,10 @@
   ASSERT_EQ(0u, md.size());
 
   transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", 
-                                             CompressionType_ZlibWithSize, 21, "compressedMD5"), 42);
-  transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"), 43);
-  transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"), 44);
+                                             CompressionType_ZlibWithSize, 21, "compressedMD5", "customData"), 42);
+  transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5", "customData"), 43);
+  transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5", "customData"), 44);
   
-  // TODO - REVISIONS - "42" is revision number, that is not currently stored (*)
   transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42);
   
   transaction_->GetAllMetadata(md, a[4]);
@@ -339,17 +338,17 @@
 
   int64_t revision;
   ASSERT_TRUE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_RemoteAet));
-  ASSERT_EQ(0, revision);   // "0" instead of "42" because of (*)
+  ASSERT_EQ(42, revision);
   ASSERT_FALSE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_Instance_IndexInSeries));
-  ASSERT_EQ(0, revision);
+  ASSERT_EQ(42, revision);
   ASSERT_EQ("PINNACLE", s);
 
   std::string u;
   ASSERT_TRUE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_RemoteAet));
-  ASSERT_EQ(0, revision);
+  ASSERT_EQ(42, revision);
   ASSERT_EQ("PINNACLE", u);
   ASSERT_FALSE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_Instance_IndexInSeries));
-  ASSERT_EQ(0, revision);
+  ASSERT_EQ(42, revision);
 
   ASSERT_TRUE(transaction_->LookupGlobalProperty(s, GlobalProperty_FlushSleep, true));
   ASSERT_FALSE(transaction_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42), true));
@@ -357,7 +356,7 @@
 
   FileInfo att;
   ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson));
-  ASSERT_EQ(0, revision);  // "0" instead of "42" because of (*)
+  ASSERT_EQ(42, revision);
   ASSERT_EQ("my json file", att.GetUuid());
   ASSERT_EQ(21u, att.GetCompressedSize());
   ASSERT_EQ("md5", att.GetUncompressedMD5());
@@ -366,7 +365,7 @@
   ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType());
 
   ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom));
-  ASSERT_EQ(0, revision);  // "0" instead of "42" because of (*)
+  ASSERT_EQ(44, revision);
   ASSERT_EQ("world", att.GetUuid());
   ASSERT_EQ(44u, att.GetCompressedSize());
   ASSERT_EQ("md5", att.GetUncompressedMD5());
@@ -402,7 +401,7 @@
 
   CheckTableRecordCount(0, "Resources");
   CheckTableRecordCount(0, "AttachedFiles");
-  CheckTableRecordCount(3, "GlobalProperties");
+  CheckTableRecordCount(4, "GlobalProperties");
 
   std::string tmp;
   ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true));
@@ -478,7 +477,7 @@
     std::string p = "Patient " + boost::lexical_cast<std::string>(i);
     patients.push_back(transaction_->CreateResource(p, ResourceType_Patient));
     transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, 
-                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
+                                                      "md5-" + boost::lexical_cast<std::string>(i), "customData"), 42);
     ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
   }
 
@@ -539,7 +538,7 @@
     std::string p = "Patient " + boost::lexical_cast<std::string>(i);
     patients.push_back(transaction_->CreateResource(p, ResourceType_Patient));
     transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10,
-                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
+                                                      "md5-" + boost::lexical_cast<std::string>(i), "customData"), 42);
     ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
   }
 
@@ -782,7 +781,7 @@
 
   for (size_t i = 0; i < ids.size(); i++)
   {
-    FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5");
+    FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5", "customData");
     int64_t revision = -1;
     index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1, "");
     ASSERT_EQ(0, revision);
--- a/TODO	Mon Feb 24 09:09:31 2025 +0100
+++ b/TODO	Mon Feb 24 09:19:12 2025 +0100
@@ -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 ===
 =======================