changeset 5809:023a99146dd0 attach-custom-data

merged find-refactoring -> attach-custom-data
author Alain Mazy <am@orthanc.team>
date Tue, 24 Sep 2024 12:53:43 +0200
parents 63c025cf6958 (diff) 8a8756b2dd0b (current diff)
children 1f6642c04541
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancServer/CMakeLists.txt OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto OrthancServer/Resources/Configuration.json OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h OrthancServer/Sources/OrthancInitialization.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerEnumerations.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/UnitTestsSources/ServerIndexTests.cpp
diffstat 38 files changed, 2558 insertions(+), 365 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Sep 23 16:03:02 2024 +0200
+++ b/.hgignore	Tue Sep 24 12:53:43 2024 +0200
@@ -14,3 +14,4 @@
 .project
 Resources/Testing/Issue32/Java/bin
 Resources/Testing/Issue32/Java/target
+build/
--- a/NEWS	Mon Sep 23 16:03:02 2024 +0200
+++ b/NEWS	Tue Sep 24 12:53:43 2024 +0200
@@ -1,6 +1,21 @@
 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)
+
+Plugins
+-------
+
+* New database plugin SDK (v4) to handle customData for attachments.
+* New storage plugin SDK (v3) to handle customData for attachments,
+
 * TODO-FIND: complete the list of updated routes:
   - /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1')
   - /studies?since=x&limit=0 and sibbling routes: limit=0 now means "no limit" instead of "no results"
@@ -18,7 +33,6 @@
 * Introduced a new configuration "ReadOnly" to forbid an Orthanc instance to perform 
   any modifications in the Index DB or in the storage.
   
-
 REST API
 --------
 
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Sep 24 12:53:43 2024 +0200
@@ -33,7 +33,7 @@
 #   * Orthanc 0.4.0 -> Orthanc 0.7.2 = version 3
 #   * Orthanc 0.7.3 -> Orthanc 0.8.4 = version 4
 #   * Orthanc 0.8.5 -> Orthanc 0.9.4 = version 5
-#   * Orthanc 0.9.5 -> mainline      = version 6
+#   * Orthanc 0.9.5 -> Orthanc 1.11.X = version 6
 set(ORTHANC_DATABASE_VERSION 6)
 
 # Version of the Orthanc API, can be retrieved from "/system" URI in
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -42,7 +42,8 @@
   FileInfo::FileInfo(const std::string& uuid,
                      FileContentType contentType,
                      uint64_t size,
-                     const std::string& md5) :
+                     const std::string& md5,
+                     const std::string& customData) :
     valid_(true),
     uuid_(uuid),
     contentType_(contentType),
@@ -50,7 +51,8 @@
     uncompressedMD5_(md5),
     compressionType_(CompressionType_None),
     compressedSize_(size),
-    compressedMD5_(md5)
+    compressedMD5_(md5),
+    customData_(customData)
   {
   }
 
@@ -61,7 +63,8 @@
                      const std::string& uncompressedMD5,
                      CompressionType compressionType,
                      uint64_t compressedSize,
-                     const std::string& compressedMD5) :
+                     const std::string& compressedMD5,
+                     const std::string& customData) :
     valid_(true),
     uuid_(uuid),
     contentType_(contentType),
@@ -69,7 +72,8 @@
     uncompressedMD5_(uncompressedMD5),
     compressionType_(compressionType),
     compressedSize_(compressedSize),
-    compressedMD5_(compressedMD5)
+    compressedMD5_(compressedMD5),
+    customData_(customData)
   {
   }
 
@@ -169,4 +173,16 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
   }
+
+  const std::string& FileInfo::GetCustomData() const
+  {
+    if (valid_)
+    {
+      return customData_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
 }
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.h	Tue Sep 24 12:53:43 2024 +0200
@@ -42,6 +42,7 @@
     CompressionType  compressionType_;
     uint64_t         compressedSize_;
     std::string      compressedMD5_;
+    std::string      customData_;
 
   public:
     FileInfo();
@@ -52,7 +53,8 @@
     FileInfo(const std::string& uuid,
              FileContentType contentType,
              uint64_t size,
-             const std::string& md5);
+             const std::string& md5,
+             const std::string& customData);
 
     /**
      * Constructor for a compressed attachment.
@@ -63,7 +65,8 @@
              const std::string& uncompressedMD5,
              CompressionType compressionType,
              uint64_t compressedSize,
-             const std::string& compressedMD5);
+             const std::string& compressedMD5,
+             const std::string& customData);
 
     bool IsValid() const;
     
@@ -80,5 +83,7 @@
     const std::string& GetCompressedMD5() const;
 
     const std::string& GetUncompressedMD5() const;
+
+    const std::string& GetCustomData() const;
   };
 }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -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 Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Tue Sep 24 12:53:43 2024 +0200
@@ -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 Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h	Tue Sep 24 12:53:43 2024 +0200
@@ -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 Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Tue Sep 24 12:53:43 2024 +0200
@@ -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 Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -102,14 +102,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)
@@ -123,7 +124,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)
@@ -137,7 +138,7 @@
           cacheAccessor.Add(uuid, type, data, size);
         }
 
-        return FileInfo(uuid, type, size, md5);
+        return FileInfo(uuid, type, size, md5, customData);
       }
 
       case CompressionType_ZlibWithSize:
@@ -159,11 +160,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);
           }
         }
 
@@ -179,7 +180,7 @@
         }
 
         return FileInfo(uuid, type, size, md5,
-                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
+                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData);
       }
 
       default:
@@ -187,13 +188,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);
+    }
   }
 
 
@@ -238,7 +318,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)
@@ -259,7 +339,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)
@@ -318,7 +398,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)
@@ -331,7 +411,8 @@
 
 
   void StorageAccessor::Remove(const std::string& fileUuid,
-                               FileContentType type)
+                               FileContentType type,
+                               const std::string& customData)
   {
     if (cache_ != NULL)
     {
@@ -340,14 +421,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());
   }
 
 
@@ -417,7 +498,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);
     }
 
@@ -507,4 +588,21 @@
     output.AnswerStream(transcoder);
   }
 #endif
+
+  // bool StorageAccessor::HandlesCustomData()
+  // {
+  //   return area_.HandlesCustomData();
+  // }
+
+  // void StorageAccessor::GetCustomData(std::string& customData,
+  //                                     const std::string& uuid,
+  //                                     const DicomInstanceToStore* instance,
+  //                                     const void* content, 
+  //                                     size_t size,
+  //                                     FileContentType type,
+  //                                     bool compression)
+  // {
+  //   area_.GetCustomData(customData, uuid, instance, content, size, type, compression);
+  // }
+
 }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Tue Sep 24 12:53:43 2024 +0200
@@ -91,16 +91,47 @@
                     StorageCache& cache,
                     MetricsRegistry& metrics);
 
-    FileInfo Write(const void* data,
-                   size_t size,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
+    // FileInfo Write(const void* data,
+    //                size_t size,
+    //                FileContentType type,
+    //                CompressionType compression,
+    //                bool storeMd5,
+    //                const std::string& uuid,
+    //                const std::string& customData);
+
+    // FileInfo Write(const std::string& data,
+    //                FileContentType type,
+    //                CompressionType compression,
+    //                bool storeMd5,
+    //                const std::string& uuid,
+    //                const std::string& customData);
 
-    FileInfo Write(const std::string& data,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
+    FileInfo WriteInstance(std::string& customData,
+                           const DicomInstanceToStore& instance,
+                           const void* data,
+                           size_t size,
+                           FileContentType type,
+                           CompressionType compression,
+                           bool storeMd5,
+                           const std::string& uuid);
+
+    FileInfo WriteAttachment(std::string& customData,
+                             const std::string& resourceId,
+                             ResourceType resourceType,
+                             const void* data,
+                             size_t size,
+                             FileContentType type,
+                             CompressionType compression,
+                             bool storeMd5,
+                             const std::string& uuid);
+
+    // FileInfo Write(std::string& customData,
+    //                const std::string& data,
+    //                FileContentType type,
+    //                CompressionType compression,
+    //                bool storeMd5,
+    //                const std::string& uuid,
+    //                const std::string& customData);
 
     void Read(std::string& content,
               const FileInfo& info);
@@ -113,7 +144,8 @@
                         uint64_t end /* exclusive */);
 
     void Remove(const std::string& fileUuid,
-                FileContentType type);
+                FileContentType type,
+                const std::string& customData);
 
     void Remove(const FileInfo& info);
 
@@ -134,6 +166,17 @@
                     const FileInfo& info,
                     const std::string& mime);
 #endif
+
+    bool HandlesCustomData();
+
+    // void GetCustomData(std::string& customData,
+    //                    const std::string& uuid,
+    //                    const DicomInstanceToStore* instance,
+    //                    const void* content, 
+    //                    size_t size,
+    //                    FileContentType type,
+    //                    bool compression);
+
   private:
     void ReadStartRangeInternal(std::string& target,
                                 const FileInfo& info,
--- a/OrthancFramework/Sources/Toolbox.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -801,7 +801,6 @@
     return result;
   }
 
-
   void Toolbox::ComputeSHA1(std::string& result,
                             const void* data,
                             size_t size)
@@ -814,11 +813,13 @@
     }
 
     unsigned int digest[5];
-
     // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide
-    assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8)); 
+    assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8));
+    assert(sizeof(boost::uuids::detail::sha1::digest_type) == 20);
     
-    sha1.get_digest(digest);
+    // From Boost 1.86, digest_type is "unsigned char[20]" while it was "unsigned int[5]"" in previous versions.
+    // Always perform the cast even if it is useless for Boost < 1.86
+    sha1.get_digest(*(reinterpret_cast<boost::uuids::detail::sha1::digest_type*>(digest)));
 
     result.resize(8 * 5 + 4);
     sprintf(&result[0], "%08x-%08x-%08x-%08x-%08x",
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -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 Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Tue Sep 24 12:53:43 2024 +0200
@@ -62,6 +62,7 @@
 SET(BUILD_CONNECTIVITY_CHECKS ON CACHE BOOL "Whether to build the ConnectivityChecks plugin")
 SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin")
 SET(BUILD_DELAYED_DELETION ON CACHE BOOL "Whether to build the DelayedDeletion plugin")
+SET(BUILD_ADVANCED_STORAGE ON CACHE BOOL "Whether to build the AdvancedStorage plugin")
 SET(BUILD_MULTITENANT_DICOM ON CACHE BOOL "Whether to build the MultitenantDicom plugin")
 SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins")
 SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests")
@@ -238,15 +239,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)
@@ -491,7 +493,7 @@
 
 if (ENABLE_PLUGINS AND
     (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER OR
-      BUILD_DELAYED_DELETION OR BUILD_MULTITENANT_DICOM))
+      BUILD_DELAYED_DELETION OR BUILD_MULTITENANT_DICOM OR BUILD_ADVANCED_STORAGE))
   set(PLUGINS_DEPENDENCIES_SOURCES
     ${BOOST_SOURCES}
     ${JSONCPP_SOURCES}
@@ -803,6 +805,60 @@
 
 
 #####################################################################
+## Build the "AdvancedStorage" plugin
+#####################################################################
+
+if (ENABLE_PLUGINS AND BUILD_ADVANCED_STORAGE)
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    execute_process(
+      COMMAND 
+      ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py
+      ${ORTHANC_VERSION} AdvancedStorage AdvancedStorage.dll "Orthanc plugin to provide advanced file storage"
+      ERROR_VARIABLE Failure
+      OUTPUT_FILE ${AUTOGENERATED_DIR}/AdvancedStorage.rc
+      )
+
+    if (Failure)
+      message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+    endif()
+
+    list(APPEND ADVANCED_STORAGE_RESOURCES ${AUTOGENERATED_DIR}/AdvancedStorage.rc)
+  endif()
+
+  set_source_files_properties(
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/Plugin.cpp
+    PROPERTIES COMPILE_DEFINITIONS "ADVANCED_STORAGE_VERSION=\"${ORTHANC_VERSION}\""
+    )
+
+  add_library(AdvancedStorage SHARED 
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/Plugin.cpp
+    ${ORTHANC}
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/OrthancException.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/SystemToolbox.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Toolbox.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Logging.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/ChunkedBuffer.cpp
+    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Enumerations.cpp
+    )
+
+  DefineSourceBasenameForTarget(AdvancedStorage)
+
+  target_link_libraries(AdvancedStorage PluginsDependencies)
+  
+  set_target_properties(
+    AdvancedStorage PROPERTIES 
+    VERSION ${ORTHANC_VERSION} 
+    SOVERSION ${ORTHANC_VERSION}
+    )
+  
+  install(
+    TARGETS AdvancedStorage
+    RUNTIME DESTINATION lib    # Destination for Windows
+    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+    )
+endif()
+
+#####################################################################
 ## Build the "MultitenantDicom" plugin
 #####################################################################
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -83,13 +83,15 @@
     
     static FileInfo Convert(const OrthancPluginAttachment& attachment)
     {
+      std::string customData;
       return FileInfo(attachment.uuid,
                       static_cast<FileContentType>(attachment.contentType),
                       attachment.uncompressedSize,
                       attachment.uncompressedHash,
                       static_cast<CompressionType>(attachment.compressionType),
                       attachment.compressedSize,
-                      attachment.compressedHash);
+                      attachment.compressedHash,
+                      customData);
     }
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -62,13 +62,15 @@
 
     static FileInfo Convert(const OrthancPluginAttachment& attachment)
     {
+      std::string customData;
       return FileInfo(attachment.uuid,
                       static_cast<FileContentType>(attachment.contentType),
                       attachment.uncompressedSize,
                       attachment.uncompressedHash,
                       static_cast<CompressionType>(attachment.compressionType),
                       attachment.compressedSize,
-                      attachment.compressedHash);
+                      attachment.compressedHash,
+                      customData);
     }
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -107,7 +107,8 @@
                     source.uncompressed_hash(),
                     static_cast<CompressionType>(source.compression_type()),
                     source.compressed_size(),
-                    source.compressed_hash());
+                    source.compressed_hash(),
+                    source.custom_data());
   }
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -59,6 +59,7 @@
 #include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/OrthancConfiguration.h"
 #include "../../Sources/OrthancFindRequestHandler.h"
+#include "../../Sources/Search/DatabaseConstraint.h"
 #include "../../Sources/Search/HierarchicalMatcher.h"
 #include "../../Sources/ServerContext.h"
 #include "../../Sources/ServerToolbox.h"
@@ -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.0
+    class PluginStorageArea3 : public IStorageArea
+    {
+    private:
+      OrthancPluginStorageCreateInstance createInstance_;
+      OrthancPluginStorageCreateAttachment createAttachment_;
+      OrthancPluginStorageRemove2 remove2_;
+      OrthancPluginStorageReadWhole2  readWhole2_;
+      OrthancPluginStorageReadRange2  readRange2_;
+
+      PluginsErrorDictionary&    errorDictionary_;
+
+    protected:
+      PluginsErrorDictionary& GetErrorDictionary() const
+      {
+        return errorDictionary_;
+      }
+
+      IMemoryBuffer* RangeFromWhole(const std::string& uuid,
+                                    const std::string& customData,
+                                    FileContentType type,
+                                    uint64_t start /* inclusive */,
+                                    uint64_t end /* exclusive */)
+      {
+        if (start > end)
+        {
+          throw OrthancException(ErrorCode_BadRange);
+        }
+        else if (start == end)
+        {
+          return new StringMemoryBuffer;  // Empty
+        }
+        else
+        {
+          std::unique_ptr<IMemoryBuffer> whole(Read(uuid, type, customData));
+
+          if (start == 0 &&
+              end == whole->GetSize())
+          {
+            return whole.release();
+          }
+          else if (end > whole->GetSize())
+          {
+            throw OrthancException(ErrorCode_BadRange);
+          }
+          else
+          {
+            std::string range;
+            range.resize(end - start);
+            assert(!range.empty());
+            
+            memcpy(&range[0], reinterpret_cast<const char*>(whole->GetData()) + start, range.size());
+
+            whole.reset(NULL);
+            return StringMemoryBuffer::CreateFromSwap(range);
+          }
+        }
+      }      
+      
+    public:
+      PluginStorageArea3(const _OrthancPluginRegisterStorageArea3& callbacks,
+                      PluginsErrorDictionary&  errorDictionary) : 
+        createInstance_(callbacks.createInstance),
+        createAttachment_(callbacks.createAttachment),
+        remove2_(callbacks.remove),
+        readWhole2_(callbacks.readWhole),
+        readRange2_(callbacks.readRange),
+        errorDictionary_(errorDictionary)
+      {
+        if (createInstance_ == NULL ||
+            createAttachment_ == NULL ||
+            remove2_ == NULL ||
+            readWhole2_ == NULL)
+        {
+          throw OrthancException(ErrorCode_Plugin, "Storage area plugin doesn't implement all the required primitives (create, remove, readWhole");
+        }
+      }
+
+      virtual void CreateInstance(std::string& customData,
+                                const DicomInstanceToStore& instance,
+                                const std::string& uuid,
+                                const void* content,
+                                size_t size,
+                                FileContentType type,
+                                bool isCompressed) ORTHANC_OVERRIDE
+      {
+        OrthancPluginMemoryBuffer customDataBuffer;
+        Orthanc::OrthancPlugins::DicomInstanceFromCallback wrapped(instance);
+
+        OrthancPluginErrorCode error = createInstance_(&customDataBuffer,
+                                                       uuid.c_str(),
+                                                       reinterpret_cast<OrthancPluginDicomInstance*>(&wrapped),
+                                                       content, size, Plugins::Convert(type),
+                                                       isCompressed);
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+
+        if (customDataBuffer.size > 0)
+        {
+          customData.assign(reinterpret_cast<char*>(customDataBuffer.data), 
+                            static_cast<size_t>(customDataBuffer.size));
+        }
+      }
+
+      virtual void CreateAttachment(std::string& customData,
+                                    const std::string& resourceId,
+                                    ResourceType resourceLevel,
+                                    const std::string& uuid,
+                                    const void* content,
+                                    size_t size,
+                                    FileContentType type,
+                                    bool isCompressed) ORTHANC_OVERRIDE
+      {
+        OrthancPluginMemoryBuffer customDataBuffer;
+
+        OrthancPluginErrorCode error = createAttachment_(&customDataBuffer,
+                                                         uuid.c_str(),
+                                                         resourceId.c_str(),
+                                                         Plugins::Convert(resourceLevel),
+                                                         content, size, Plugins::Convert(type),
+                                                         isCompressed);
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+
+        if (customDataBuffer.size > 0)
+        {
+          customData.assign(reinterpret_cast<char*>(customDataBuffer.data), 
+                            static_cast<size_t>(customDataBuffer.size));
+        }
+      }
+
+      virtual void Remove(const std::string& uuid,
+                          FileContentType type,
+                          const std::string& customData) ORTHANC_OVERRIDE
+      {
+        OrthancPluginErrorCode error = remove2_
+          (uuid.c_str(), customData.c_str(), Plugins::Convert(type));
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+      }
+
+      virtual IMemoryBuffer* Read(const std::string& uuid,
+                                  FileContentType type,
+                                  const std::string& customData) ORTHANC_OVERRIDE
+      {
+        std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer);
+
+        OrthancPluginMemoryBuffer64 buffer;
+        buffer.size = 0;
+        buffer.data = NULL;
+        
+        OrthancPluginErrorCode error = readWhole2_(&buffer, uuid.c_str(), customData.c_str(), Plugins::Convert(type));
+
+        if (error == OrthancPluginErrorCode_Success)
+        {
+          result->Assign(buffer.data, buffer.size, ::free);
+          return result.release();
+        }
+        else
+        {
+          GetErrorDictionary().LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+      }
+
+      virtual IMemoryBuffer* ReadRange(const std::string& uuid,
+                                       FileContentType type,
+                                       uint64_t start /* inclusive */,
+                                       uint64_t end /* exclusive */,
+                                       const std::string& customData) ORTHANC_OVERRIDE
+      {
+        if (readRange2_ == NULL)
+        {
+          return RangeFromWhole(uuid, customData, type, start, end);
+        }
+        else
+        {
+          if (start > end)
+          {
+            throw OrthancException(ErrorCode_BadRange);
+          }
+          else if (start == end)
+          {
+            return new StringMemoryBuffer;
+          }
+          else
+          {
+            std::string range;
+            range.resize(end - start);
+            assert(!range.empty());
+
+            OrthancPluginMemoryBuffer64 buffer;
+            buffer.data = &range[0];
+            buffer.size = static_cast<uint64_t>(range.size());
+
+            OrthancPluginErrorCode error =
+              readRange2_(&buffer, uuid.c_str(), customData.c_str(), Plugins::Convert(type), start);
+
+            if (error == OrthancPluginErrorCode_Success)
+            {
+              return StringMemoryBuffer::CreateFromSwap(range);
+            }
+            else
+            {
+              GetErrorDictionary().LogError(error, true);
+              throw OrthancException(static_cast<ErrorCode>(error));
+            }
+          }
+        }
+      }
+
+      virtual bool HasReadRange() const ORTHANC_OVERRIDE
+      {
+        return (readRange2_ != NULL);
+      }
+
+    };
+
+
     class StorageAreaFactory : public boost::noncopyable
     {
     private:
       enum Version
       {
         Version1,
-        Version2
+        Version2,
+        Version3
       };
       
       SharedLibrary&                      sharedLibrary_;
       Version                             version_;
       _OrthancPluginRegisterStorageArea   callbacks_;
       _OrthancPluginRegisterStorageArea2  callbacks2_;
+      _OrthancPluginRegisterStorageArea3  callbacks3_;
       PluginsErrorDictionary&             errorDictionary_;
 
       static void WarnNoReadRange()
@@ -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);
         }
@@ -2493,125 +2863,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)
@@ -5021,7 +5272,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;
       }
@@ -5031,7 +5283,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;
       }
@@ -5041,7 +5294,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;
       }
 
@@ -5622,23 +5876,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);
@@ -5761,7 +6026,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 Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Tue Sep 24 12:53:43 2024 +0200
@@ -88,11 +88,14 @@
     class HttpClientChunkedAnswer;
     class HttpServerChunkedReader;
     class IDicomInstance;
-    class DicomInstanceFromCallback;
     class DicomInstanceFromBuffer;
     class DicomInstanceFromParsed;
     class WebDavCollection;
-    
+
+public:
+    class DicomInstanceFromCallback;
+
+private:
     void RegisterRestCallback(const void* parameters,
                               bool lock);
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Tue Sep 24 12:53:43 2024 +0200
@@ -63,6 +63,7 @@
     _OrthancPluginDatabaseAnswerType_DeletedAttachment = 1,
     _OrthancPluginDatabaseAnswerType_DeletedResource = 2,
     _OrthancPluginDatabaseAnswerType_RemainingAncestor = 3,
+    _OrthancPluginDatabaseAnswerType_DeletedAttachment2 = 4,
 
     /* Return value */
     _OrthancPluginDatabaseAnswerType_Attachment = 10,
@@ -75,6 +76,7 @@
     _OrthancPluginDatabaseAnswerType_String = 17,
     _OrthancPluginDatabaseAnswerType_MatchingResource = 18,  /* New in Orthanc 1.5.2 */
     _OrthancPluginDatabaseAnswerType_Metadata = 19,          /* New in Orthanc 1.5.4 */
+    _OrthancPluginDatabaseAnswerType_Attachment2 = 20,       /* New in Orthanc 1.12.0 */
 
     _OrthancPluginDatabaseAnswerType_INTERNAL = 0x7fffffff
   } _OrthancPluginDatabaseAnswerType;
@@ -93,6 +95,18 @@
 
   typedef struct
   {
+    const char* uuid;
+    int32_t     contentType;
+    uint64_t    uncompressedSize;
+    const char* uncompressedHash;
+    int32_t     compressionType;
+    uint64_t    compressedSize;
+    const char* compressedHash;
+    const char* customData;
+  } OrthancPluginAttachment2;
+
+  typedef struct
+  {
     uint16_t     group;
     uint16_t     element;
     const char*  value;
@@ -306,6 +320,19 @@
     context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
   }
 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerAttachment2(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginAttachment2* attachment)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Attachment2;
+    params.valueGeneric = attachment;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
   ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerResource(
     OrthancPluginContext*          context,
     OrthancPluginDatabaseContext*  database,
@@ -366,6 +393,19 @@
     context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
   }
 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment2(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginAttachment2* attachment)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_DeletedAttachment2;
+    params.valueGeneric = attachment;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
   ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedResource(
     OrthancPluginContext*          context,
     OrthancPluginDatabaseContext*  database,
@@ -1361,7 +1401,343 @@
 
     return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, &params);
   }
+
+
+  // typedef struct
+  // {
+  //   OrthancPluginDatabaseEventType type;
+
+  //   union
+  //   {
+  //     struct
+  //     {
+  //       /* For ""DeletedResource" and "RemainingAncestor" */
+  //       OrthancPluginResourceType  level;
+  //       const char*                publicId;
+  //     } resource;
+
+  //     /* For "DeletedAttachment" */
+  //     OrthancPluginAttachment2  attachment;
+      
+  //   } content;
+    
+  // } OrthancPluginDatabaseEvent2;
+
+
+  // typedef struct
+  // {
+  //   /**
+  //    * Functions to read the answers inside a transaction
+  //    **/
+    
+  //   OrthancPluginErrorCode (*readAnswersCount) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               uint32_t* target /* out */);
+
+  //   OrthancPluginErrorCode (*readAnswerAttachment2) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                    OrthancPluginAttachment2* target /* out */,          // new in v4
+  //                                                    uint32_t index);
+
+  //   OrthancPluginErrorCode (*readAnswerChange) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               OrthancPluginChange* target /* out */,
+  //                                               uint32_t index);
+
+  //   OrthancPluginErrorCode (*readAnswerDicomTag) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 uint16_t* group,
+  //                                                 uint16_t* element,
+  //                                                 const char** value,
+  //                                                 uint32_t index);
+
+  //   OrthancPluginErrorCode (*readAnswerExportedResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                         OrthancPluginExportedResource* target /* out */,
+  //                                                         uint32_t index);
+
+  //   OrthancPluginErrorCode (*readAnswerInt32) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              int32_t* target /* out */,
+  //                                              uint32_t index);
+
+  //   OrthancPluginErrorCode (*readAnswerInt64) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              int64_t* target /* out */,
+  //                                              uint32_t index);
+
+  //   OrthancPluginErrorCode (*readAnswerMatchingResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                         OrthancPluginMatchingResource* target /* out */,
+  //                                                         uint32_t index);
+    
+  //   OrthancPluginErrorCode (*readAnswerMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 int32_t* metadata /* out */,
+  //                                                 const char** value /* out */,
+  //                                                 uint32_t index);
+
+  //   OrthancPluginErrorCode (*readAnswerString) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               const char** target /* out */,
+  //                                               uint32_t index);
+    
+  //   OrthancPluginErrorCode (*readEventsCount) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              uint32_t* target /* out */);
+
+  //   OrthancPluginErrorCode (*readEvent2) (OrthancPluginDatabaseTransaction* transaction,
+  //                                         OrthancPluginDatabaseEvent2* event /* out */,                // new in v4
+  //                                         uint32_t index);
+
+    
+    
+  //   /**
+  //    * Functions to access the global database object
+  //    * (cf. "IDatabaseWrapper" class in Orthanc)
+  //    **/
+
+  //   OrthancPluginErrorCode (*open) (void* database);
+
+  //   OrthancPluginErrorCode (*close) (void* database);
+
+  //   OrthancPluginErrorCode (*destructDatabase) (void* database);
+
+  //   OrthancPluginErrorCode (*getDatabaseVersion) (void* database,
+  //                                                 uint32_t* target /* out */);
+
+  //   OrthancPluginErrorCode (*hasRevisionsSupport) (void* database,
+  //                                                  uint8_t* target /* out */);
+
+  //   OrthancPluginErrorCode (*hasAttachmentCustomDataSupport) (void* database,                           // new in v4
+  //                                                             uint8_t* target /* out */);
+
+  //   OrthancPluginErrorCode (*upgradeDatabase) (void* database,
+  //                                              OrthancPluginStorageArea* storageArea,
+  //                                              uint32_t targetVersion);
+
+  //   OrthancPluginErrorCode (*startTransaction) (void* database,
+  //                                               OrthancPluginDatabaseTransaction** target /* out */,
+  //                                               OrthancPluginDatabaseTransactionType type);
+
+  //   OrthancPluginErrorCode (*destructTransaction) (OrthancPluginDatabaseTransaction* transaction);
+
+
+  //   /**
+  //    * Functions to run operations within a database transaction
+  //    * (cf. "IDatabaseWrapper::ITransaction" class in Orthanc)
+  //    **/
+
+  //   OrthancPluginErrorCode (*rollback) (OrthancPluginDatabaseTransaction* transaction);
+    
+  //   OrthancPluginErrorCode (*commit) (OrthancPluginDatabaseTransaction* transaction,
+  //                                     int64_t fileSizeDelta);
+
+  //   /* A call to "addAttachment()" guarantees that this attachment is not already existing ("INSERT") */
+  //   OrthancPluginErrorCode (*addAttachment2) (OrthancPluginDatabaseTransaction* transaction,
+  //                                            int64_t id,
+  //                                            const OrthancPluginAttachment2* attachment,                      // new in v4
+  //                                            int64_t revision);
+
+  //   OrthancPluginErrorCode (*clearChanges) (OrthancPluginDatabaseTransaction* transaction);
+    
+  //   OrthancPluginErrorCode (*clearExportedResources) (OrthancPluginDatabaseTransaction* transaction);
+    
+  //   OrthancPluginErrorCode (*clearMainDicomTags) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 int64_t resourceId);
+
+  //   OrthancPluginErrorCode (*createInstance) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             OrthancPluginCreateInstanceResult* target /* out */,
+  //                                             const char* hashPatient,
+  //                                             const char* hashStudy,
+  //                                             const char* hashSeries,
+  //                                             const char* hashInstance);
+
+  //   OrthancPluginErrorCode (*deleteAttachment) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               int64_t id,
+  //                                               int32_t contentType);
+    
+  //   OrthancPluginErrorCode (*deleteMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             int64_t id,
+  //                                             int32_t metadataType);
+
+  //   OrthancPluginErrorCode (*deleteResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             int64_t id);
+
+  //   /* Answers are read using "readAnswerMetadata()" */
+  //   OrthancPluginErrorCode (*getAllMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             int64_t id);
+    
+  //   /* Answers are read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*getAllPublicIds) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              OrthancPluginResourceType resourceType);
+    
+  //   /* Answers are read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*getAllPublicIdsWithLimit) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                       OrthancPluginResourceType resourceType,
+  //                                                       uint64_t since,
+  //                                                       uint64_t limit);
+
+  //   /* Answers are read using "readAnswerChange()" */
+  //   OrthancPluginErrorCode (*getChanges) (OrthancPluginDatabaseTransaction* transaction,
+  //                                         uint8_t* targetDone /* out */,
+  //                                         int64_t since,
+  //                                         uint32_t maxResults);
+    
+  //   /* Answers are read using "readAnswerInt64()" */
+  //   OrthancPluginErrorCode (*getChildrenInternalId) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                    int64_t id);
+    
+  //   /* Answers are read using "readAnswerString()" */
+  //   OrthancPluginErrorCode  (*getChildrenMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                   int64_t resourceId,
+  //                                                   int32_t metadata);
+
+  //   /* Answers are read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*getChildrenPublicId) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                  int64_t id);
+
+  //   /* Answers are read using "readAnswerExportedResource()" */
+  //   OrthancPluginErrorCode (*getExportedResources) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                   uint8_t* targetDone /* out */,
+  //                                                   int64_t since,
+  //                                                   uint32_t maxResults);
+    
+  //   /* Answer is read using "readAnswerChange()" */
+  //   OrthancPluginErrorCode (*getLastChange) (OrthancPluginDatabaseTransaction* transaction);
+    
+  //   OrthancPluginErrorCode (*getLastChangeIndex) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 int64_t* target /* out */);
+    
+  //   /* Answer is read using "readAnswerExportedResource()" */
+  //   OrthancPluginErrorCode (*getLastExportedResource) (OrthancPluginDatabaseTransaction* transaction);
+    
+  //   /* Answers are read using "readAnswerDicomTag()" */
+  //   OrthancPluginErrorCode (*getMainDicomTags) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               int64_t id);
+    
+  //   /* Answer is read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*getPublicId) (OrthancPluginDatabaseTransaction* transaction,
+  //                                          int64_t internalId);
+    
+  //   OrthancPluginErrorCode (*getResourcesCount) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                uint64_t* target /* out */,
+  //                                                OrthancPluginResourceType resourceType);
+    
+  //   OrthancPluginErrorCode (*getResourceType) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              OrthancPluginResourceType* target /* out */,
+  //                                              uint64_t resourceId);
+    
+  //   OrthancPluginErrorCode (*getTotalCompressedSize) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                     uint64_t* target /* out */);
+    
+  //   OrthancPluginErrorCode (*getTotalUncompressedSize) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                       uint64_t* target /* out */);
+    
+  //   OrthancPluginErrorCode (*isDiskSizeAbove) (OrthancPluginDatabaseTransaction* transaction,
+  //                                              uint8_t* target /* out */,
+  //                                              uint64_t threshold);
+    
+  //   OrthancPluginErrorCode (*isExistingResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 uint8_t* target /* out */,
+  //                                                 int64_t resourceId);
+    
+  //   OrthancPluginErrorCode (*isProtectedPatient) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                 uint8_t* target /* out */,
+  //                                                 int64_t resourceId);
+    
+  //   /* Answers are read using "readAnswerInt32()" */
+  //   OrthancPluginErrorCode (*listAvailableAttachments) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                       int64_t internalId);
+
+  //   OrthancPluginErrorCode (*logChange) (OrthancPluginDatabaseTransaction* transaction,
+  //                                        int32_t changeType,
+  //                                        int64_t resourceId,
+  //                                        OrthancPluginResourceType resourceType,
+  //                                        const char* date);
+
+  //   OrthancPluginErrorCode (*logExportedResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                  OrthancPluginResourceType resourceType,
+  //                                                  const char* publicId,
+  //                                                  const char* modality,
+  //                                                  const char* date,
+  //                                                  const char* patientId,
+  //                                                  const char* studyInstanceUid,
+  //                                                  const char* seriesInstanceUid,
+  //                                                  const char* sopInstanceUid);
+
+  //   /* Answer is read using "readAnswerAttachment()" */
+  //   OrthancPluginErrorCode (*lookupAttachment) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               int64_t* revision /* out */,
+  //                                               int64_t resourceId,
+  //                                               int32_t contentType);
+
+  //   /* Answer is read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*lookupGlobalProperty) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                   const char* serverIdentifier,
+  //                                                   int32_t property);
+    
+  //   /* Answer is read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*lookupMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             int64_t* revision /* out */,
+  //                                             int64_t id,
+  //                                             int32_t metadata);
+    
+  //   OrthancPluginErrorCode (*lookupParent) (OrthancPluginDatabaseTransaction* transaction,
+  //                                           uint8_t* isExisting /* out */,
+  //                                           int64_t* parentId /* out */,
+  //                                           int64_t id);
+    
+  //   OrthancPluginErrorCode (*lookupResource) (OrthancPluginDatabaseTransaction* transaction,
+  //                                             uint8_t* isExisting /* out */,
+  //                                             int64_t* id /* out */,
+  //                                             OrthancPluginResourceType* type /* out */,
+  //                                             const char* publicId);
+    
+  //   /* Answers are read using "readAnswerMatchingResource()" */
+  //   OrthancPluginErrorCode  (*lookupResources) (OrthancPluginDatabaseTransaction* transaction,
+  //                                               uint32_t constraintsCount,
+  //                                               const OrthancPluginDatabaseConstraint* constraints,
+  //                                               OrthancPluginResourceType queryLevel,
+  //                                               uint32_t limit,
+  //                                               uint8_t requestSomeInstanceId);
+
+  //   /* The public ID of the parent resource is read using "readAnswerString()" */
+  //   OrthancPluginErrorCode (*lookupResourceAndParent) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                      uint8_t* isExisting /* out */,
+  //                                                      int64_t* id /* out */,
+  //                                                      OrthancPluginResourceType* type /* out */,
+  //                                                      const char* publicId);
+
+  //   OrthancPluginErrorCode (*selectPatientToRecycle) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                     uint8_t* patientAvailable /* out */,
+  //                                                     int64_t* patientId /* out */);
+    
+  //   OrthancPluginErrorCode (*selectPatientToRecycle2) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                      uint8_t* patientAvailable /* out */,
+  //                                                      int64_t* patientId /* out */,
+  //                                                      int64_t patientIdToAvoid);
+
+  //   OrthancPluginErrorCode (*setGlobalProperty) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                const char* serverIdentifier,
+  //                                                int32_t property,
+  //                                                const char* value);
+
+  //   /* In "setMetadata()", the metadata might already be existing ("INSERT OR REPLACE")  */
+  //   OrthancPluginErrorCode (*setMetadata) (OrthancPluginDatabaseTransaction* transaction,
+  //                                          int64_t id,
+  //                                          int32_t metadata,
+  //                                          const char* value,
+  //                                          int64_t revision);
+    
+  //   OrthancPluginErrorCode (*setProtectedPatient) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                  int64_t id,
+  //                                                  uint8_t isProtected);
+
+  //   OrthancPluginErrorCode  (*setResourcesContent) (OrthancPluginDatabaseTransaction* transaction,
+  //                                                   uint32_t countIdentifierTags,
+  //                                                   const OrthancPluginResourcesContentTags* identifierTags,
+  //                                                   uint32_t countMainDicomTags,
+  //                                                   const OrthancPluginResourcesContentTags* mainDicomTags,
+  //                                                   uint32_t countMetadata,
+  //                                                   const OrthancPluginResourcesContentMetadata* metadata);
+    
+
+  // } OrthancPluginDatabaseBackendV4;
+
+/*<! @endcond */
   
+
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Sep 24 12:53:43 2024 +0200
@@ -491,6 +491,7 @@
     _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017,  /* New in Orthanc 1.10.0 */
     _OrthancPluginService_RegisterReceivedInstanceCallback = 1018,  /* New in Orthanc 1.10.0 */
     _OrthancPluginService_RegisterWebDavCollection = 1019,     /* New in Orthanc 1.10.1 */
+    _OrthancPluginService_RegisterStorageArea3 = 1020,         /* New in Orthanc 1.12.0 */
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -559,7 +560,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,
@@ -1364,7 +1365,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
@@ -1439,6 +1440,121 @@
 
 
 
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
+   *
+   * @param customData The custom data of the attachment (out)
+   * @param uuid The UUID of the file.
+   * @param instance The DICOM instance being stored.
+   * @param content The content of the file (might be compressed data, hence the need for the DICOM instance arg to access tags).
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreateInstance) (
+    OrthancPluginMemoryBuffer* customData,
+    const char* uuid,
+    const OrthancPluginDicomInstance*  instance,
+    const void* content,
+    int64_t size,
+    OrthancPluginContentType type,
+    bool isCompressed);
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
+   *
+   * @param customData The custom data of the attachment (out)
+   * @param uuid The UUID of the file.
+   * @param resourceId The resource ID the file is attached to.
+   * @param resourceType The resource Type the file is attached to.
+   * @param content The content of the file (might be compressed data, hence the need for the DICOM instance arg to access tags).
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreateAttachment) (
+    OrthancPluginMemoryBuffer* customData,
+    const char* uuid,
+    const char* resourceId,
+    OrthancPluginResourceType resourceType,
+    const void* content,
+    int64_t size,
+    OrthancPluginContentType type,
+    bool isCompressed);
+
+
+
+  /**
+   * @brief Callback for reading a whole file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * reads a whole file from the storage area.
+   *
+   * @param target Memory buffer where to store the content of the file. It must be allocated by the
+   * plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it.
+   * @param uuid The UUID of the file of interest.
+   * @param customData The custom data of the file to be removed.
+   * @param type The content type corresponding to this file. 
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageReadWhole2) (
+    OrthancPluginMemoryBuffer64* target,
+    const char* uuid,
+    const char* customData,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for reading a range of a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * reads a portion of a file from the storage area. Orthanc
+   * indicates the start position and the length of the range.
+   *
+   * @param target Memory buffer where to store the content of the range.
+   * The memory buffer is allocated and freed by Orthanc. The length of the range
+   * of interest corresponds to the size of this buffer.
+   * @param uuid The UUID of the file of interest.
+   * @param customData The custom data of the file to be removed.
+   * @param type The content type corresponding to this file. 
+   * @param rangeStart Start position of the requested range in the file.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) (
+    OrthancPluginMemoryBuffer64* target,
+    const char* uuid,
+    const char* customData,
+    OrthancPluginContentType type,
+    uint64_t rangeStart);
+
+
+
+  /**
+   * @brief Callback for removing a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area.
+   *
+   * @param uuid The UUID of the file to be removed.
+   * @param customData The custom data of the file to be removed.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) (
+    const char* uuid,
+    const char* customData,
+    OrthancPluginContentType type);
+
+
   /**
    * @brief Callback to handle the C-Find SCP requests for worklists.
    *
@@ -9361,6 +9477,50 @@
   }
 
 
+  typedef struct
+  {
+    OrthancPluginStorageCreateInstance       createInstance;
+    OrthancPluginStorageCreateAttachment     createAttachment;
+    OrthancPluginStorageReadWhole2    readWhole;
+    OrthancPluginStorageReadRange2    readRange;
+    OrthancPluginStorageRemove2       remove;
+  } _OrthancPluginRegisterStorageArea3;
+
+  /**
+   * @brief Register a custom storage area, with support for custom data.
+   *
+   * This function registers a custom storage area, to replace the
+   * built-in way Orthanc stores its files on the filesystem. This
+   * function must be called during the initialization of the plugin,
+   * i.e. inside the OrthancPluginInitialize() public function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param create The callback function to store a file on the custom storage area.
+   * @param readWhole The callback function to read a whole file from the custom storage area.
+   * @param readRange The callback function to read some range of a file from the custom storage area.
+   * If this feature is not supported by the plugin, this value can be set to NULL.
+   * @param remove The callback function to remove a file from the custom storage area.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3(
+    OrthancPluginContext*             context,
+    // OrthancPluginStorageGetCustomData getCustomData,
+    OrthancPluginStorageCreateInstance         createInstance,
+    OrthancPluginStorageCreateAttachment       createAttachement,
+    OrthancPluginStorageReadWhole2    readWhole,
+    OrthancPluginStorageReadRange2    readRange,
+    OrthancPluginStorageRemove2       remove)
+  {
+    _OrthancPluginRegisterStorageArea3 params;
+    // params.getCustomData = getCustomData;
+    params.createAttachment = createAttachement;
+    params.createInstance = createInstance;
+    params.readWhole = readWhole;
+    params.readRange = readRange;
+    params.remove = remove;
+    context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, &params);
+  }
+
   /**
    * @brief Signature of a callback function that is triggered when
    * the Orthanc core requests an operation from the database plugin.
@@ -9567,7 +9727,6 @@
     context->InvokeService(context, _OrthancPluginService_LogMessage, &m);
   }
 
-
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Tue Sep 24 12:53:43 2024 +0200
@@ -55,6 +55,7 @@
   int32   compression_type = 5;  // opaque "CompressionType" in Orthanc
   uint64  compressed_size = 6;
   string  compressed_hash = 7;
+  string  custom_data = 8;       // added in v 1.12.5
 }
 
 enum ResourceType {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/AdvancedStorage/CMakeLists.txt	Tue Sep 24 12:53:43 2024 +0200
@@ -0,0 +1,81 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2022 Osimis S.A., Belgium
+# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+cmake_minimum_required(VERSION 2.8)
+cmake_policy(SET CMP0058 NEW)
+
+project(AdvancedStorage)
+
+SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin")
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake)
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+include(${CMAKE_CURRENT_LIST_DIR}/../Common/OrthancPluginsExports.cmake)
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/WindowsResources.py
+    ${PLUGIN_VERSION} AdvancedStorage AdvancedStorage.dll "Orthanc plugin to extend Orthanc Storage"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/AdvancedStorage.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  list(APPEND ADDITIONAL_RESOURCES ${AUTOGENERATED_DIR}/AdvancedStorage.rc)
+endif()  
+
+add_definitions(
+  -DHAS_ORTHANC_EXCEPTION=1
+  -DORTHANC_PLUGIN_VERSION="${PLUGIN_VERSION}"
+  )
+
+include_directories(
+  ${CMAKE_SOURCE_DIR}/../../Include/
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/
+  )
+
+add_library(AdvancedStorage SHARED
+  ${ADDITIONAL_RESOURCES}
+  ${AUTOGENERATED_SOURCES}
+  ${ORTHANC_CORE_SOURCES_DEPENDENCIES}
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Enumerations.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Logging.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/OrthancException.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SystemToolbox.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Toolbox.cpp
+  ${CMAKE_SOURCE_DIR}/../../../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
+  Plugin.cpp
+  )
+
+set_target_properties(
+  AdvancedStorage PROPERTIES 
+  VERSION ${PLUGIN_VERSION} 
+  SOVERSION ${PLUGIN_VERSION}
+  )
+
+install(
+  TARGETS AdvancedStorage
+  DESTINATION .
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -0,0 +1,701 @@
+/**
+ * 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/>.
+ **/
+
+#define ORTHANC_PLUGIN_NAME "advanced-storage"
+
+#include "../../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/SystemToolbox.h"
+#include "../../../../OrthancFramework/Sources/Toolbox.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/iostreams/device/file_descriptor.hpp>
+#include <boost/iostreams/stream.hpp>
+
+
+#include <json/value.h>
+#include <json/writer.h>
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+#include <map>
+#include <list>
+#include <time.h>
+
+namespace fs = boost::filesystem;
+
+fs::path rootPath_;
+bool multipleStoragesEnabled_ = false;
+std::map<std::string, fs::path> rootPaths_;
+std::string currentStorageId_;
+std::string namingScheme_;
+bool fsyncOnWrite_ = true;
+size_t maxPathLength_ = 256;
+size_t legacyPathLength = 39; // ex "/00/f7/00f7fd8b-47bd8c3a-ff917804-d180cdbc-40cf9527"
+
+fs::path GetRootPath()
+{
+  if (multipleStoragesEnabled_)
+  {
+    return rootPaths_[currentStorageId_];
+  }
+
+  return rootPath_;
+}
+
+fs::path GetRootPath(const std::string& storageId)
+{
+  if (multipleStoragesEnabled_)
+  {
+    if (rootPaths_.find(storageId) == rootPaths_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Advanced Storage - storage '" + storageId + "' is not defined in configuration"));
+    }
+    return rootPaths_[storageId];
+  }
+
+  return rootPath_;
+}
+
+
+fs::path GetLegacyRelativePath(const std::string& uuid)
+{
+  if (!Orthanc::Toolbox::IsUuid(uuid))
+  {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+
+  fs::path path;
+
+  path /= std::string(&uuid[0], &uuid[2]);
+  path /= std::string(&uuid[2], &uuid[4]);
+  path /= uuid;
+
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+  path.make_preferred();
+#endif
+
+  return path;
+}
+
+fs::path GetPath(const std::string& uuid, const std::string& customDataString)
+{
+  fs::path path;
+
+  if (!customDataString.empty())
+  {
+    Json::Value customData;
+    Orthanc::Toolbox::ReadJson(customData, customDataString);
+
+    if (customData["Version"].asInt() == 1)
+    {
+      if (customData.isMember("StorageId"))
+      {
+        path = GetRootPath(customData["StorageId"].asString());
+      }
+      else
+      {
+        path = GetRootPath();
+      }
+      
+      if (customData.isMember("Path"))
+      {
+        path /= customData["Path"].asString();
+      }
+      else
+      { // we are in "legacy mode" for the path part
+        path /= GetLegacyRelativePath(uuid);
+      }
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Advanced Storage - unknown version for custom data '" + boost::lexical_cast<std::string>(customData["Version"].asInt()) + "'"));
+    }
+  }
+  else // we are in "legacy mode"
+  {
+    path = GetRootPath();
+    path /= GetLegacyRelativePath(uuid);
+  }
+
+  path.make_preferred();
+  return path;
+}
+
+void GetCustomData(std::string& output, const fs::path& path)
+{
+  // if we use defaults, non need to store anything in the metadata, the plugin has the same behavior as the core of Orthanc
+  if (namingScheme_ == "OrthancDefault" && !multipleStoragesEnabled_)
+  {
+    return;
+  }
+
+  Json::Value customDataJson;
+  customDataJson["Version"] = 1;
+
+  // no need to store the path since if we are in the default mode
+  if (namingScheme_ != "OrthancDefault")
+  { 
+    customDataJson["Path"] = path.string();
+  }
+
+  if (multipleStoragesEnabled_)
+  {
+    customDataJson["StorageId"] = currentStorageId_;
+  }
+
+  return Orthanc::Toolbox::WriteFastJson(output, customDataJson);
+}
+
+void AddSplitDateDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
+{
+  if (tags.isMember(tagName) && tags[tagName].asString().size() == 8)
+  {
+    std::string date = tags[tagName].asString();
+    path /= date.substr(0, 4);
+    path /= date.substr(4, 2);
+    path /= date.substr(6, 2);
+  }
+  else if (defaultValue != NULL)
+  {
+    path /= defaultValue;
+  }
+}
+
+void AddStringDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
+{
+  if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0)
+  {
+    path /= tags[tagName].asString();
+  }
+  else if (defaultValue != NULL)
+  {
+    path /= defaultValue;
+  }
+}
+
+void AddIntDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, size_t zeroPaddingWidth = 0, const char* defaultValue = NULL)
+{
+  if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0)
+  {
+    std::string tagValue = tags[tagName].asString();
+    if (zeroPaddingWidth > 0 && tagValue.size() < zeroPaddingWidth)
+    {
+      std::string padding(zeroPaddingWidth - tagValue.size(), '0');
+      path /= padding + tagValue; 
+    }
+    else
+    {
+      path /= tagValue;
+    }
+  }
+  else if (defaultValue != NULL)
+  {
+    path /= defaultValue;
+  }
+}
+
+std::string GetExtension(OrthancPluginContentType type, bool isCompressed)
+{
+  std::string extension;
+
+  switch (type)
+  {
+    case OrthancPluginContentType_Dicom:
+      extension = ".dcm";
+      break;
+    case OrthancPluginContentType_DicomUntilPixelData:
+      extension = ".dcm.head";
+      break;
+    default:
+      extension = ".unk";
+  }
+  if (isCompressed)
+  {
+    extension = extension + ".cmp"; // compression is zlib + size -> we can not use the .zip extension
+  }
+  
+  return extension;
+}
+
+fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed)
+{
+  fs::path path;
+
+  if (!tags.isNull())
+  { 
+    if (namingScheme_ == "Preset1-StudyDatePatientID")
+    {
+      if (!tags.isMember("StudyDate"))
+      {
+        LOG(WARNING) << "AdvancedStorage - No 'StudyDate' in attachment " << uuid << ".  Attachment will be stored in NO_STUDY_DATE folder";
+      }
+
+      AddSplitDateDicomTagToPath(path, tags, "StudyDate", "NO_STUDY_DATE");
+      AddStringDicomTagToPath(path, tags, "PatientID");  // no default value, tag is always present if the instance is accepted by Orthanc  
+      
+      if (tags.isMember("PatientName") && tags["PatientName"].isString() && !tags["PatientName"].asString().empty())
+      {
+        path += std::string(" - ") + tags["PatientName"].asString();
+      }
+
+      AddStringDicomTagToPath(path, tags, "StudyDescription");
+      AddStringDicomTagToPath(path, tags, "SeriesInstanceUID");
+
+      path /= uuid;
+      path += GetExtension(type, isCompressed);
+      return path;
+    }
+  }
+
+  return GetLegacyRelativePath(uuid);
+}
+
+
+OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData,
+                                             const char* uuid,
+                                             const Json::Value& tags,
+                                             const void* content,
+                                             int64_t size,
+                                             OrthancPluginContentType type,
+                                             bool isCompressed)
+{
+  fs::path relativePath = GetRelativePathFromTags(tags, uuid, type, isCompressed);
+  std::string customDataString;
+  GetCustomData(customDataString, relativePath);
+
+  fs::path rootPath = GetRootPath();
+  fs::path path = rootPath / relativePath;
+
+  LOG(INFO) << "Advanced Storage - creating attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path.string() + ")";
+
+  // check that the final path is not 'above' the root path (this could happen if e.g., a PatientName is ../../../../toto)
+  // fs::canonical() can not be used for that since the file needs to exist
+  // so far, we'll just forbid path containing '..' since they might be suspicious
+  if (path.string().find("..") != std::string::npos)
+  {
+    fs::path legacyPath = rootPath / GetLegacyRelativePath(uuid);
+    LOG(WARNING) << "Advanced Storage - WAS02 - Path is suspicious since it contains '..': '" << path.string() << "' will be stored in '" << legacyPath << "'";
+    path = legacyPath;
+  }
+
+  // check path length !!!!!, if too long, go back to legacy path and issue a warning
+  if (path.string().size() > maxPathLength_)
+  {
+    fs::path legacyPath = rootPath / GetLegacyRelativePath(uuid);
+    LOG(WARNING) << "Advanced Storage - WAS01 - Path is too long: '" << path.string() << "' will be stored in '" << legacyPath << "'";
+    path = legacyPath;
+  }
+
+  if (fs::exists(path))
+  {
+    // Extremely unlikely case if uuid is included in the path: This Uuid has already been created
+    // in the past.
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Advanced Storage - path already exists");
+
+    // TODO for the future: handle duplicates path (e.g: there's no uuid in the path and we are uploading the same file again)
+  }
+
+  if (fs::exists(path.parent_path()))
+  {
+    if (!fs::is_directory(path.parent_path()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_DirectoryOverFile);
+    }
+  }
+  else
+  {
+    if (!fs::create_directories(path.parent_path()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_FileStorageCannotWrite);
+    }
+  }
+
+  Orthanc::SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_);
+
+  OrthancPluginCreateMemoryBuffer(OrthancPlugins::GetGlobalContext(), customData, customDataString.size());
+  memcpy(customData->data, customDataString.data(), customDataString.size());
+
+  return OrthancPluginErrorCode_Success;
+
+}
+
+OrthancPluginErrorCode StorageCreateInstance(OrthancPluginMemoryBuffer* customData,
+                                             const char* uuid,
+                                             const OrthancPluginDicomInstance*  instance,
+                                             const void* content,
+                                             int64_t size,
+                                             OrthancPluginContentType type,
+                                             bool isCompressed)
+{
+  try
+  {
+    OrthancPlugins::DicomInstance dicomInstance(instance);
+    Json::Value tags;
+    dicomInstance.GetSimplifiedJson(tags);
+
+    return StorageCreate(customData, uuid, tags, content, size, type, isCompressed);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+  }
+  catch (...)
+  {
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode StorageCreateAttachment(OrthancPluginMemoryBuffer* customData,
+                                               const char* uuid,
+                                               const char* resourceId,
+                                               OrthancPluginResourceType resourceType,
+                                               const void* content,
+                                               int64_t size,
+                                               OrthancPluginContentType type,
+                                               bool isCompressed)
+{
+  try
+  {
+    LOG(INFO) << "Creating attachment \"" << uuid << "\"";
+
+    //TODO_CUSTOM_DATA: get tags from the Rest API...
+    Json::Value tags;
+
+    return StorageCreate(customData, uuid, tags, content, size, type, isCompressed);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+  }
+  catch (...)
+  {
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target,
+                                        const char* uuid,
+                                        const char* customData,
+                                        OrthancPluginContentType type)
+{
+  std::string path = GetPath(uuid, customData).string();
+
+  LOG(INFO) << "Advanced Storage - Reading whole attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path + ")";
+
+  if (!Orthanc::SystemToolbox::IsRegularFile(path))
+  {
+    LOG(ERROR) << "The path does not point to a regular file: " << path;
+    return OrthancPluginErrorCode_InexistentFile;
+  }
+
+  try
+  {
+    fs::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      LOG(ERROR) << "The path does not point to a regular file: " << path;
+      return OrthancPluginErrorCode_InexistentFile;
+    }
+
+    // get file size
+    f.seekg(0, std::ios::end);
+    std::streamsize fileSize = f.tellg();
+    f.seekg(0, std::ios::beg);
+
+    // The ReadWhole must allocate the buffer itself
+    if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, fileSize) != OrthancPluginErrorCode_Success)
+    {
+      LOG(ERROR) << "Unable to allocate memory to read file: " << path;
+      return OrthancPluginErrorCode_NotEnoughMemory;
+    }
+
+    if (fileSize != 0)
+    {
+      f.read(reinterpret_cast<char*>(target->data), fileSize);
+    }
+
+    f.close();
+  }
+  catch (...)
+  {
+    LOG(ERROR) << "Unexpected error while reading: " << path;
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode StorageReadRange (OrthancPluginMemoryBuffer64* target,
+                                         const char* uuid,
+                                         const char* customData,
+                                         OrthancPluginContentType type,
+                                         uint64_t rangeStart)
+{
+  std::string path = GetPath(uuid, customData).string();
+
+  LOG(INFO) << "Advanced Storage - Reading range of attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path + ")";
+
+  if (!Orthanc::SystemToolbox::IsRegularFile(path))
+  {
+    LOG(ERROR) << "The path does not point to a regular file: " << path;
+    return OrthancPluginErrorCode_InexistentFile;
+  }
+
+  try
+  {
+    fs::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      LOG(ERROR) << "The path does not point to a regular file: " << path;
+      return OrthancPluginErrorCode_InexistentFile;
+    }
+
+    f.seekg(rangeStart, std::ios::beg);
+
+    // The ReadRange uses a target that has already been allocated by orthanc
+    f.read(reinterpret_cast<char*>(target->data), target->size);
+
+    f.close();
+  }
+  catch (...)
+  {
+    LOG(ERROR) << "Unexpected error while reading: " << path;
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode StorageRemove (const char* uuid,
+                                      const char* customData,
+                                      OrthancPluginContentType type)
+{
+  fs::path path = GetPath(uuid, customData);
+
+  LOG(INFO) << "Advanced Storage - Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path.string() + ")";
+
+  try
+  {
+    fs::remove(path);
+  }
+  catch (...)
+  {
+    // Ignore the error
+  }
+
+  // Remove the empty parent directories, (ignoring the error code if these directories are not empty)
+
+  try
+  {
+    fs::path parent = path.parent_path();
+
+    while (parent != GetRootPath())
+    {
+      fs::remove(parent);
+      parent = parent.parent_path();
+    }
+  }
+  catch (...)
+  {
+    // Ignore the error
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+extern "C"
+{
+
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    OrthancPlugins::SetGlobalContext(context);
+    Orthanc::Logging::InitializePluginContext(context);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(context) == 0)
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      return -1;
+    }
+
+    LOG(WARNING) << "AdvancedStorage plugin is initializing";
+    OrthancPluginSetDescription2(context, ORTHANC_PLUGIN_NAME, "Provides alternative layout for your storage.");
+
+    OrthancPlugins::OrthancConfiguration orthancConfiguration;
+
+    OrthancPlugins::OrthancConfiguration advancedStorage;
+    orthancConfiguration.GetSection(advancedStorage, "AdvancedStorage");
+
+    bool enabled = advancedStorage.GetBooleanValue("Enable", false);
+    if (enabled)
+    {
+      /*
+        {
+          "AdvancedStorage": {
+            
+            // Enables/disables the plugin
+            "Enable": false,
+
+            // Enables/disables support for multiple StorageDirectories
+            "MultipleStorages" : {
+              "Storages" : {
+                // The storgae ids below may never change since they are stored in DB
+                // The storage path may change in case you move your data from one place to the other
+                "1" : "/var/lib/orthanc/db",
+                "2" : "/mnt/disk2/orthanc"
+              },
+
+              // the storage on which new data is stored.
+              // There's currently no automatic changes of disks
+              "CurrentStorage" : "2",
+            },
+
+            // Defines the storage structure and file namings.  Right now, 
+            // only the "OrthancDefault" value shall be used in a production environment.  
+            // All other values are currently experimental
+            // "OrthancDefault" = same structure and file naming as default orthanc, 
+            // "Preset1-StudyDatePatientID" = split(StudyDate)/PatientID - PatientName/StudyDescription/SeriesInstanceUID/uuid.ext
+            "NamingScheme" : "OrthancDefault",
+
+            // Defines the maximum length for path used in the storage.  If a file is longer
+            // than this limit, it is stored with the default orthanc naming scheme
+            // (and a warning is issued).
+            // Note, on Windows, the maximum path length is 260 bytes by default but can be increased
+            // through a configuration.
+            "MaxPathLength" : 256
+          }
+        }
+      */
+
+      fsyncOnWrite_ = orthancConfiguration.GetBooleanValue("SyncStorageArea", true);
+
+      const Json::Value& pluginJson = advancedStorage.GetJson();
+
+      namingScheme_ = advancedStorage.GetStringValue("NamingScheme", "OrthancDefault");
+
+      // if we have enabled multiple storage after files have been saved without this plugin, we still need the default StorageDirectory
+      rootPath_ = fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage"));
+      LOG(WARNING) << "AdvancedStorage - Path to the default storage area: " << rootPath_.string();
+
+      maxPathLength_ = orthancConfiguration.GetIntegerValue("MaxPathLength", 256);
+      LOG(WARNING) << "AdvancedStorage - Maximum path length: " << maxPathLength_;
+
+      if (!rootPath_.is_absolute())
+      {
+        LOG(ERROR) << "AdvancedStorage - Path to the default storage area should be an absolute path '" << rootPath_ << "'";
+        return -1;
+      }
+
+      if (rootPath_.size() > (maxPathLength_ - legacyPathLength))
+      {
+        LOG(ERROR) << "AdvancedStorage - Path to the default storage is too long";
+        return -1;
+      }
+
+      if (pluginJson.isMember("MultipleStorages"))
+      {
+        multipleStoragesEnabled_ = true;
+        const Json::Value& multipleStoragesJson = pluginJson["MultipleStorages"];
+        
+        if (multipleStoragesJson.isMember("Storages") && multipleStoragesJson.isObject() && multipleStoragesJson.isMember("CurrentStorage") && multipleStoragesJson["CurrentStorage"].isString())
+        {
+          const Json::Value& storagesJson = multipleStoragesJson["Storages"];
+          Json::Value::Members storageIds = storagesJson.getMemberNames();
+    
+          for (Json::Value::Members::const_iterator it = storageIds.begin(); it != storageIds.end(); ++it)
+          {
+            const Json::Value& storagePath = storagesJson[*it];
+            if (!storagePath.isString())
+            {
+              LOG(ERROR) << "AdvancedStorage - Storage path is not a string " << *it;
+              return -1;
+            }
+
+            rootPaths_[*it] = storagePath.asString();
+
+            if (!rootPaths_[*it].is_absolute())
+            {
+              LOG(ERROR) << "AdvancedStorage - Storage path shall be absolute path '" << storagePath.asString() << "'";
+              return -1;
+            }
+
+            if (storagePath.asString().size() > (maxPathLength_ - legacyPathLength))
+            {
+              LOG(ERROR) << "AdvancedStorage - Storage path is too long '" << storagePath.asString() << "'";
+              return -1;
+            }
+          }
+
+          currentStorageId_ = multipleStoragesJson["CurrentStorage"].asString();
+
+          if (rootPaths_.find(currentStorageId_) == rootPaths_.end())
+          {
+            LOG(ERROR) << "AdvancedStorage - CurrentStorage is not defined in Storages list: " << currentStorageId_;
+            return -1;
+          }
+
+          LOG(WARNING) << "AdvancedStorage - multiple storages enabled.  Current storage : " << rootPaths_[currentStorageId_].string();
+        }
+      }
+
+      OrthancPluginRegisterStorageArea3(context, StorageCreateInstance, StorageCreateAttachment, StorageReadWhole, StorageReadRange, StorageRemove);
+    }
+    else
+    {
+      LOG(WARNING) << "AdvancedStorage plugin is disabled by the configuration file";
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    LOG(WARNING) << "AdvancedStorage plugin is finalizing";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return ORTHANC_PLUGIN_NAME;
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ADVANCED_STORAGE_VERSION;
+  }
+}
--- a/OrthancServer/Resources/Configuration.json	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Resources/Configuration.json	Tue Sep 24 12:53:43 2024 +0200
@@ -15,11 +15,15 @@
   // Path to the directory that holds the heavyweight files (i.e. the
   // raw DICOM instances). Backslashes must be either escaped by
   // doubling them, or replaced by forward slashes "/".
+  // If a relative path is provided, it is relative to the configuration
+  // file path.  It is advised to provide an absolute path.
   "StorageDirectory" : "OrthancStorage",
 
   // Path to the directory that holds the SQLite index (if unset, the
   // value of StorageDirectory is used). This index could be stored on
   // a RAM-drive or a SSD device for performance reasons.
+  // If a relative path is provided, it is relative to the configuration
+  // file path.  It is advised to provide an absolute path.
   "IndexDirectory" : "OrthancStorage",
 
   // Path to the directory where Orthanc stores its large temporary
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/DowngradeFrom7to6.sql	Tue Sep 24 12:53:43 2024 +0200
@@ -0,0 +1,45 @@
+-- Orthanc - A Lightweight, RESTful DICOM Store
+-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+-- Department, University Hospital of Liege, Belgium
+-- Copyright (C) 2017-2022 Osimis S.A., Belgium
+-- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+--
+-- This program is free software: you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License as
+-- published by the Free Software Foundation, either version 3 of the
+-- License, or (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+--
+-- This SQLite script updates the version of the Orthanc database from 6 to 7.
+--
+
+-- Add a new column to AttachedFiles
+
+ALTER TABLE AttachedFiles DROP COLUMN metadata;
+
+-- update the triggers (back to v6)
+DROP TRIGGER AttachedFileDeleted
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           old.uncompressedMD5, old.compressedMD5);
+END;
+
+
+-- Change the database version
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+
+UPDATE GlobalProperties SET value="6" WHERE property=1;
+
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Tue Sep 24 12:53:43 2024 +0200
@@ -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	Tue Sep 24 12:53:43 2024 +0200
@@ -0,0 +1,66 @@
+-- Orthanc - A Lightweight, RESTful DICOM Store
+-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+-- Department, University Hospital of Liege, Belgium
+-- Copyright (C) 2017-2022 Osimis S.A., Belgium
+-- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+--
+-- This program is free software: you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License as
+-- published by the Free Software Foundation, either version 3 of the
+-- License, or (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+--
+-- This SQLite script installs revision and customData without changing the Orthanc database version
+--
+
+-- Add new columns for revision
+ALTER TABLE Metadata ADD COLUMN revision INTEGER;
+ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER;
+
+-- Add new column for customData
+ALTER TABLE AttachedFiles ADD COLUMN customData TEXT;
+
+
+-- add another AttachedFileDeleted trigger 
+-- We want to keep backward compatibility and avoid changing the database version number (which would force
+-- users to upgrade the DB).  By keeping backward compatibility, we mean "allow a user to run a previous Orthanc
+-- version after it has run this update script".
+-- We must keep the signature of the initial trigger (it is impossible to have 2 triggers on the same event).
+-- We tried adding a trigger on "BEFORE DELETE" but then it is being called when running the previous Orthanc
+-- which makes it fail.
+-- But, we need the customData in the C++ function that is called when a AttachedFiles is deleted.
+-- The trick is then to save the customData in a DeletedFiles table.
+-- The SignalFileDeleted C++ function will then get the customData from this table and delete the entry.
+-- Drawback: if you downgrade Orthanc, the DeletedFiles table will remain and will be populated by the trigger
+-- but not consumed by the C++ function -> we consider this is an acceptable drawback for a few people compared 
+-- to the burden of upgrading the DB.
+
+CREATE TABLE DeletedFiles(
+       uuid TEXT NOT NULL,        -- 0
+       customData TEXT            -- 1
+);
+
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  INSERT INTO DeletedFiles VALUES(old.uuid, old.customData);
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           old.uncompressedMD5, old.compressedMD5
+                           );
+END;
+
+-- Record that this upgrade has been performed
+
+INSERT INTO GlobalProperties VALUES (7, 1);  -- GlobalProperty_SQLiteHasCustomDataAndRevision
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Sources/Database/PrepareDatabase.sql	Tue Sep 24 12:53:43 2024 +0200
@@ -52,6 +52,7 @@
        id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
        type INTEGER,
        value TEXT,
+       -- revision INTEGER,      -- New in Orthanc 1.12.0 (added in InstallRevisionAndCustomData.sql)
        PRIMARY KEY(id, type)
        );
 
@@ -64,6 +65,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.0 (added in InstallRevisionAndCustomData.sql)
+       -- customData TEXT,       -- New in Orthanc 1.12.0 (added in InstallRevisionAndCustomData.sql)
        PRIMARY KEY(id, fileType)
        );              
 
@@ -126,7 +129,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 Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -41,6 +41,8 @@
 #include <stdio.h>
 #include <boost/lexical_cast.hpp>
 
+static std::map<std::string, std::string> filesToDeleteCustomData;
+
 namespace Orthanc
 {  
   static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec)
@@ -410,8 +412,9 @@
                                const FileInfo& attachment,
                                int64_t revision) ORTHANC_OVERRIDE
     {
-      // TODO - REVISIONS
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5) VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+        "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5, revision, customData) "
+        "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
       s.BindInt64(0, id);
       s.BindInt(1, attachment.GetContentType());
       s.BindString(2, attachment.GetUuid());
@@ -420,10 +423,11 @@
       s.BindInt(5, attachment.GetCompressionType());
       s.BindString(6, attachment.GetUncompressedMD5());
       s.BindString(7, attachment.GetCompressedMD5());
+      s.BindInt(8, revision);
+      s.BindString(9, attachment.GetCustomData());
       s.Run();
     }
 
-
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
                                       const DatabaseConstraints& lookup,
@@ -473,10 +477,11 @@
 #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_BIG_INT_1 9
+#define C10_BIG_INT_2 10
 
 #define QUERY_LOOKUP 1
 #define QUERY_MAIN_DICOM_TAGS 2
@@ -566,10 +571,11 @@
              "  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_big_int1, "
+             "  NULL AS c10_big_int2 "
              "  FROM Lookup ";
 
       // need one instance info ? (part 2: execute the queries)
@@ -583,10 +589,11 @@
                "    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, "
+               "    instanceInternalId AS c9_big_int1, "
+               "    NULL AS c10_big_int2 "
                "   FROM OneInstance ";
 
         sql += "   UNION SELECT"
@@ -596,10 +603,11 @@
                "    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_big_int1, "
+               "    NULL AS c10_big_int2 "
                "   FROM OneInstance "
                "   INNER JOIN Metadata ON Metadata.id = OneInstance.instanceInternalId ";
               
@@ -610,10 +618,11 @@
                "    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, "
+               "    compressedSize AS c9_big_int1, "
+               "    uncompressedSize AS c10_big_int2 "
                "   FROM OneInstance "
                "   INNER JOIN AttachedFiles ON AttachedFiles.id = OneInstance.instanceInternalId ";
 
@@ -629,10 +638,11 @@
                "  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_big_int1, "
+               "  NULL AS c10_big_int2 "
                "FROM Lookup "
                "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId ";
       }
@@ -647,10 +657,11 @@
                "  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, "
+               "  NULL AS c8_int2, "
+               "  NULL AS c9_big_int1, "
+               "  NULL AS c10_big_int2 "
                "FROM Lookup "
                "INNER JOIN Metadata ON Metadata.id = Lookup.internalId ";
       }
@@ -665,10 +676,11 @@
                "  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, "
+               "  compressedSize AS c9_big_int1, "
+               "  uncompressedSize AS c10_big_int2 "
                "FROM Lookup "
                "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId ";
       }
@@ -684,10 +696,11 @@
                "  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_big_int1, "
+               "  NULL AS c10_big_int2 "
                "FROM Lookup "
                "INNER JOIN Labels ON Labels.id = Lookup.internalId ";
       }
@@ -704,10 +717,11 @@
                  "  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_big_int1, "
+                 "  NULL AS c10_big_int2 "
                  "FROM Lookup "
                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                  "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId ";
@@ -723,10 +737,11 @@
                  "  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, "
+                 "  NULL AS c8_int2, "
+                 "  NULL AS c9_big_int1, "
+                 "  NULL AS c10_big_int2 "
                  "FROM Lookup "
                  "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                  "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId ";        
@@ -744,10 +759,11 @@
                   "  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_big_int1, "
+                  "  NULL AS c10_big_int2 "
                   "FROM Lookup "
                   "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                   "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -764,10 +780,11 @@
                   "  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, "
+                  "  NULL AS c8_int2, "
+                  "  NULL AS c9_big_int1, "
+                  "  NULL AS c10_big_int2 "
                   "FROM Lookup "
                   "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId "
                   "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "
@@ -786,10 +803,11 @@
                "  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_big_int1, "
+               "  NULL AS c10_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))); 
@@ -805,10 +823,11 @@
                 "  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_big_int1, "
+                "  NULL AS c10_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -825,10 +844,11 @@
                "  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_big_int1, "
+               "  NULL AS c10_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId "
                "  INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId ";
@@ -844,10 +864,11 @@
                 "  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, "
+                "  NULL AS c8_int2, "
+                "  NULL AS c9_big_int1, "
+                "  NULL AS c10_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))) + ") ";
@@ -863,10 +884,11 @@
                 "  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, "
+                "  NULL AS c8_int2, "
+                "  NULL AS c9_big_int1, "
+                "  NULL AS c10_big_int2 "
                 "FROM Lookup "
                 "  INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId "
                 "  INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId "
@@ -885,10 +907,11 @@
                "  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_big_int1, "
+               "  NULL AS c10_big_int2 "
                "FROM Lookup "
                "  INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId ";
       }
@@ -904,10 +927,11 @@
               "  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_big_int1, "
+              "  NULL AS c10_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId ";
@@ -923,10 +947,11 @@
               "  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_big_int1, "
+              "  NULL AS c10_big_int2 "
               "FROM Lookup "
               "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "
               "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "
@@ -964,10 +989,10 @@
           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(C8_BIG_INT_1), s.ColumnString(C4_STRING_2),
-                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
-                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C5_STRING_3));
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)), 
+                          s.ColumnInt64(C9_BIG_INT_1), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C8_INT_2)),
+                          s.ColumnInt64(C10_BIG_INT_2), s.ColumnString(C5_STRING_3), s.ColumnString(C6_STRING_4));
             res.AddAttachment(file);
           }; break;
 
@@ -975,8 +1000,8 @@
           {
             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;
 
@@ -984,8 +1009,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;
 
@@ -993,8 +1018,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;
 
@@ -1002,7 +1027,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;
 
@@ -1010,7 +1035,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;
 
@@ -1018,7 +1043,7 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddMetadata(static_cast<ResourceType>(requestLevel), 
-                            static_cast<MetadataType>(s.ColumnInt(C6_INT_1)),
+                            static_cast<MetadataType>(s.ColumnInt(C7_INT_1)),
                             s.ColumnString(C3_STRING_1));
           }; break;
 
@@ -1026,7 +1051,7 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddMetadata(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;
 
@@ -1034,7 +1059,7 @@
           {
             FindResponse::Resource& res = response.GetResourceByInternalId(internalId);
             res.AddMetadata(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;
 
@@ -1042,7 +1067,7 @@
           {
             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;
 
@@ -1050,7 +1075,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;
 
@@ -1090,16 +1115,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(C8_BIG_INT_1), s.ColumnString(C4_STRING_2),
-                          static_cast<CompressionType>(s.ColumnInt(C7_INT_2)),
-                          s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C5_STRING_3));
+            FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)), 
+                          s.ColumnInt64(C9_BIG_INT_1), s.ColumnString(C4_STRING_2),
+                          static_cast<CompressionType>(s.ColumnInt(C8_INT_2)),
+                          s.ColumnInt64(C10_BIG_INT_2), s.ColumnString(C5_STRING_3), s.ColumnString(C6_STRING_4));
             res.AddOneInstanceAttachment(file);
           }; break;
 
@@ -1206,6 +1231,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
@@ -1581,7 +1628,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);
 
@@ -1597,8 +1644,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;
       }
     }
@@ -1633,7 +1681,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);
 
@@ -1644,7 +1692,7 @@
       else
       {
         target = s.ColumnString(0);
-        revision = 0;   // TODO - REVISIONS
+        revision = s.ColumnInt(1);
         return true;
       }
     }
@@ -1816,11 +1864,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();
     }
 
@@ -1949,6 +1997,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))
@@ -1967,9 +2020,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);
       }
     }
   };
@@ -2226,6 +2281,19 @@
         }
       }
 
+      // New in Orthanc 1.12.5
+      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);
     }
   }
@@ -2256,7 +2324,7 @@
   {
     boost::mutex::scoped_lock lock(mutex_);
 
-    if (targetVersion != 6)
+    if (targetVersion != 7)
     {
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
     }
@@ -2266,7 +2334,8 @@
     if (version_ != 3 &&
         version_ != 4 &&
         version_ != 5 &&
-        version_ != 6)
+        version_ != 6 &&
+        version_ != 7)
     {
       throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
     }
@@ -2307,6 +2376,7 @@
       
       version_ = 6;
     }
+
   }
 
 
--- a/OrthancServer/Sources/OrthancInitialization.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -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 Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -2701,6 +2701,7 @@
  
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
+    ResourceType resourceType = StringToResourceType(call.GetFullUri()[0].c_str());
 
     FileContentType contentType = StringToContentType(name);
     if (IsUserContentType(contentType) ||  // It is forbidden to modify internal attachments...
@@ -2725,7 +2726,7 @@
       }
 
       int64_t newRevision;
-      context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(),
+      context.AddAttachment(newRevision, publicId, resourceType, StringToContentType(name), call.GetBodyData(),
                             call.GetBodySize(), hasOldRevision, oldRevision, oldMD5);
 
       SetBufferContentETag(call.GetOutput(), newRevision, call.GetBodyData(), call.GetBodySize());  // New in Orthanc 1.9.2
@@ -2846,9 +2847,10 @@
 
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
+    ResourceType resourceType = StringToResourceType(call.GetFullUri()[0].c_str());
     FileContentType contentType = StringToContentType(name);
 
-    OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression);
+    OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, resourceType, contentType, compression);
     call.GetOutput().AnswerBuffer("{}", MimeType_Json);
   }
 
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -582,10 +582,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);
   }
 
 
@@ -696,8 +697,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);
@@ -707,8 +711,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);
       }
 
@@ -979,6 +986,7 @@
 
 
   void ServerContext::ChangeAttachmentCompression(const std::string& resourceId,
+                                                  ResourceType resourceType,
                                                   FileContentType attachmentType,
                                                   CompressionType compression)
   {
@@ -1005,8 +1013,25 @@
     StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     accessor.Read(content, attachment);
 
-    FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(),
-                                       content.size(), attachmentType, compression, storeMD5_);
+    std::string newUuid = Toolbox::GenerateUuid();
+    std::string newCustomData;
+    FileInfo modified;
+
+    // if (attachmentType == FileContentType_Dicom || attachmentType == FileContentType_DicomUntilPixelData)
+    // {
+    //   // DicomInstanceToStore instance;
+    //   // TODO_CUSTOM_DATA: get the Instance such that we can call accessor.GetCustomData ...
+    //   // modified = accessor.WriteInstance(newCustomData, instance, content.empty() ? NULL : content.c_str(),
+    //   //                                 content.size(), attachmentType, compression, storeMD5_, newUuid);
+    // }
+    // else
+    {
+      ResourceType resourceType = ResourceType_Instance; //TODO_CUSTOM_DATA: get it from above in the stack
+      modified = accessor.WriteAttachment(newCustomData, resourceId, resourceType, content.empty() ? NULL : content.c_str(),
+                                       content.size(), attachmentType, compression, storeMD5_, newUuid);
+    }
+
+
 
     try
     {
@@ -1270,9 +1295,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 */);
             }
           }
         }
@@ -1495,6 +1520,7 @@
 
   bool ServerContext::AddAttachment(int64_t& newRevision,
                                     const std::string& resourceId,
+                                    ResourceType resourceType,
                                     FileContentType attachmentType,
                                     const void* data,
                                     size_t size,
@@ -1508,7 +1534,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 Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Tue Sep 24 12:53:43 2024 +0200
@@ -286,7 +286,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
@@ -358,6 +359,7 @@
 
     bool AddAttachment(int64_t& newRevision,
                        const std::string& resourceId,
+                       ResourceType resourceType,
                        FileContentType attachmentType,
                        const void* data,
                        size_t size,
@@ -379,6 +381,7 @@
                           FileContentType content);
 
     void ChangeAttachmentCompression(const std::string& resourceId,
+                                     ResourceType resourceType,
                                      FileContentType attachmentType,
                                      CompressionType compression);
 
--- a/OrthancServer/Sources/ServerEnumerations.h	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Tue Sep 24 12:53:43 2024 +0200
@@ -135,6 +135,7 @@
     GlobalProperty_AnonymizationSequence = 3,
     GlobalProperty_JobsRegistry = 5,
     GlobalProperty_GetTotalSizeIsFast = 6,      // New in Orthanc 1.5.2
+    GlobalProperty_SQLiteHasCustomDataAndRevision = 7,     // New in Orthanc 1.12.0
     GlobalProperty_Modalities = 20,             // New in Orthanc 1.5.0
     GlobalProperty_Peers = 21,                  // New in Orthanc 1.5.0
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -45,12 +45,14 @@
     struct FileToRemove
     {
     private:
-      std::string  uuid_;
-      FileContentType  type_;
+      std::string       uuid_;
+      std::string       customData_;
+      FileContentType   type_;
 
     public:
       explicit FileToRemove(const FileInfo& info) :
-        uuid_(info.GetUuid()), 
+        uuid_(info.GetUuid()),
+        customData_(info.GetCustomData()),
         type_(info.GetContentType())
       {
       }
@@ -60,6 +62,11 @@
         return uuid_;
       }
 
+      const std::string& GetCustomData() const
+      {
+        return customData_;
+      }
+
       FileContentType GetContentType() const 
       {
         return type_;
@@ -93,7 +100,7 @@
       {
         try
         {
-          context_.RemoveFile(it->GetUuid(), it->GetContentType());
+          context_.RemoveFile(it->GetUuid(), it->GetContentType(), it->GetCustomData());
         }
         catch (OrthancException& e)
         {
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Mon Sep 23 16:03:02 2024 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Tue Sep 24 12:53:43 2024 +0200
@@ -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 Sep 23 16:03:02 2024 +0200
+++ b/TODO	Tue Sep 24 12:53:43 2024 +0200
@@ -1,3 +1,36 @@
+TODO_CUSTOM_DATA branch
+- add REVISIONS in AttachedFiles + Metadata in SQLite since we change the DB schema
+- add revisions and customData in all Database plugins
+- check if we can play with GlobalProperty_DatabasePatchLevel instead of upgrading the DB version !
+- upgrade DB automatically such that it does not need a specific launch with --update , add --downgrade + --no-auto-upgrade command lines
+- refuse to instantiate a PluginStorage3 if a DBv4 is not instantiated !
+- handle all TODO_CUSTOM_DATA
+- check /attachments/...  routes for path returned
+- AdvancedStoragePlugin 
+  - show warning if a tag is missing when generating the path from tags (+ option to disable this warning)
+  - generate path from tags from resource (CreateAttachment)
+  - add an instanceId or parentSeriesId arg in CreateInstance ? 
+  - implement a 'legacy' root path to group all files with missing tags or path too long
+  - avoid error AdvancedStorage - Path to the default storage area should be an absolute path '"OrthancStorage"' when using PG and no StorageDirectory has been defined
+  - document that, once you have used the AdvancedStoragePlugin and stored DICOM files, you can not downgrade Orthanc to a previous Orthanc
+    without loosing access to the DICOM files
+  - write integration test for advanced-storage: 
+    - launch 1.11.2
+    - upload 1 file
+    - launch 1.12.0 with advanced-storage plugin with a non default namingScheme
+    - upload 1 file
+    - access + delete initial file
+
+- write integration test for transitions from one DB to the other (for each DB plugin):
+  - launch 1.11.2, 
+  - upload 2 files, 
+  - launch 1.12.0, 
+  - access + delete one file, 
+  - upload one file, 
+  - launch 1.11.2 again, 
+  - access + delete last 2 files
+
+
 =======================
 === Orthanc Roadmap ===
 =======================