changeset 4640:66109d24d26e

"ETag" headers for metadata and attachments now allow strong comparison (MD5 is included)
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 26 Apr 2021 15:22:44 +0200
parents c638dd444de0
children b02dc8303cf6
files NEWS OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/ServerIndex.h OrthancServer/Sources/ServerToolbox.cpp OrthancServer/Sources/main.cpp OrthancServer/UnitTestsSources/ServerIndexTests.cpp
diffstat 11 files changed, 206 insertions(+), 97 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Apr 22 13:27:57 2021 +0200
+++ b/NEWS	Mon Apr 26 15:22:44 2021 +0200
@@ -1,6 +1,8 @@
 Pending changes in the mainline
 ===============================
 
+* "ETag" headers for metadata and attachments now allow strong comparison (MD5 is included)
+
 
 Version 1.9.2 (2021-04-22)
 ==========================
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Apr 26 15:22:44 2021 +0200
@@ -2199,7 +2199,8 @@
                                                 MetadataType type,
                                                 const std::string& value,
                                                 bool hasOldRevision,
-                                                int64_t oldRevision)
+                                                int64_t oldRevision,
+                                                const std::string& oldMD5)
   {
     class Operations : public IReadWriteOperations
     {
@@ -2210,6 +2211,7 @@
       const std::string&  value_;
       bool                hasOldRevision_;
       int64_t             oldRevision_;
+      const std::string&  oldMD5_;
 
     public:
       Operations(int64_t& newRevision,
@@ -2217,13 +2219,15 @@
                  MetadataType type,
                  const std::string& value,
                  bool hasOldRevision,
-                 int64_t oldRevision) :
+                 int64_t oldRevision,
+                 const std::string& oldMD5) :
         newRevision_(newRevision),
         publicId_(publicId),
         type_(type),
         value_(value),
         hasOldRevision_(hasOldRevision),
-        oldRevision_(oldRevision)
+        oldRevision_(oldRevision),
+        oldMD5_(oldMD5)
       {
       }
 
@@ -2241,15 +2245,19 @@
           int64_t expectedRevision;
           if (transaction.LookupMetadata(oldValue, expectedRevision, id, type_))
           {
-            if (hasOldRevision_ &&
-                expectedRevision != oldRevision_)
+            if (hasOldRevision_)
             {
-              throw OrthancException(ErrorCode_Revision);
+              std::string expectedMD5;
+              Toolbox::ComputeMD5(expectedMD5, oldValue);
+
+              if (expectedRevision != oldRevision_ ||
+                  expectedMD5 != oldMD5_)
+              {
+                throw OrthancException(ErrorCode_Revision);
+              }              
             }
-            else
-            {
-              newRevision_ = expectedRevision + 1;
-            }
+            
+            newRevision_ = expectedRevision + 1;
           }
           else
           {
@@ -2268,7 +2276,7 @@
       }
     };
 
-    Operations operations(newRevision, publicId, type, value, hasOldRevision, oldRevision);
+    Operations operations(newRevision, publicId, type, value, hasOldRevision, oldRevision, oldMD5);
     Apply(operations);
   }
 
@@ -2278,14 +2286,15 @@
                                                       const std::string& value)
   {
     int64_t newRevision;  // Unused
-    SetMetadata(newRevision, publicId, type, value, false /* no old revision */, -1 /* dummy */);
+    SetMetadata(newRevision, publicId, type, value, false /* no old revision */, -1 /* dummy */, "" /* dummy */);
   }
 
 
   bool StatelessDatabaseOperations::DeleteMetadata(const std::string& publicId,
                                                    MetadataType type,
                                                    bool hasRevision,
-                                                   int64_t revision)
+                                                   int64_t revision,
+                                                   const std::string& md5)
   {
     class Operations : public IReadWriteOperations
     {
@@ -2294,17 +2303,20 @@
       MetadataType        type_;
       bool                hasRevision_;
       int64_t             revision_;
+      const std::string&  md5_;
       bool                found_;
 
     public:
       Operations(const std::string& publicId,
                  MetadataType type,
                  bool hasRevision,
-                 int64_t revision) :
+                 int64_t revision,
+                 const std::string& md5) :
         publicId_(publicId),
         type_(type),
         hasRevision_(hasRevision),
         revision_(revision),
+        md5_(md5),
         found_(false)
       {
       }
@@ -2324,19 +2336,25 @@
         }
         else
         {
-          std::string s;
+          std::string value;
           int64_t expectedRevision;
-          if (transaction.LookupMetadata(s, expectedRevision, id, type_))
+          if (transaction.LookupMetadata(value, expectedRevision, id, type_))
           {
-            if (hasRevision_ &&
-                expectedRevision != revision_)
+            if (hasRevision_)
             {
-              throw OrthancException(ErrorCode_Revision);
+              std::string expectedMD5;
+              Toolbox::ComputeMD5(expectedMD5, value);
+
+              if (expectedRevision != revision_ ||
+                  expectedMD5 != md5_)
+              {
+                throw OrthancException(ErrorCode_Revision);
+              }
             }
             
             found_ = true;
             transaction.DeleteMetadata(id, type_);
-
+            
             if (IsUserMetadata(type_))
             {
               transaction.LogChange(id, ChangeType_UpdatedMetadata, resourceType, publicId_);
@@ -2350,7 +2368,7 @@
       }
     };
 
-    Operations operations(publicId, type, hasRevision, revision);
+    Operations operations(publicId, type, hasRevision, revision, md5);
     Apply(operations);
     return operations.HasFound();
   }
@@ -2485,7 +2503,8 @@
   bool StatelessDatabaseOperations::DeleteAttachment(const std::string& publicId,
                                                      FileContentType type,
                                                      bool hasRevision,
-                                                     int64_t revision)
+                                                     int64_t revision,
+                                                     const std::string& md5)
   {
     class Operations : public IReadWriteOperations
     {
@@ -2494,17 +2513,20 @@
       FileContentType     type_;
       bool                hasRevision_;
       int64_t             revision_;
+      const std::string&  md5_;
       bool                found_;
 
     public:
       Operations(const std::string& publicId,
                  FileContentType type,
                  bool hasRevision,
-                 int64_t revision) :
+                 int64_t revision,
+                 const std::string& md5) :
         publicId_(publicId),
         type_(type),
         hasRevision_(hasRevision),
         revision_(revision),
+        md5_(md5),
         found_(false)
       {
       }
@@ -2529,7 +2551,8 @@
           if (transaction.LookupAttachment(info, expectedRevision, id, type_))
           {
             if (hasRevision_ &&
-                expectedRevision != revision_)
+                (expectedRevision != revision_ ||
+                 info.GetUncompressedMD5() != md5_))
             {
               throw OrthancException(ErrorCode_Revision);
             }
@@ -2550,7 +2573,7 @@
       }
     };
 
-    Operations operations(publicId, type, hasRevision, revision);
+    Operations operations(publicId, type, hasRevision, revision, md5);
     Apply(operations);
     return operations.HasFound();
   }
@@ -3261,7 +3284,8 @@
                                                          uint64_t maximumStorageSize,
                                                          unsigned int maximumPatients,
                                                          bool hasOldRevision,
-                                                         int64_t oldRevision)
+                                                         int64_t oldRevision,
+                                                         const std::string& oldMD5)
   {
     class Operations : public IReadWriteOperations
     {
@@ -3274,6 +3298,7 @@
       unsigned int        maximumPatientCount_;
       bool                hasOldRevision_;
       int64_t             oldRevision_;
+      const std::string&  oldMD5_;
 
     public:
       Operations(int64_t& newRevision,
@@ -3282,7 +3307,8 @@
                  uint64_t maximumStorageSize,
                  unsigned int maximumPatientCount,
                  bool hasOldRevision,
-                 int64_t oldRevision) :
+                 int64_t oldRevision,
+                 const std::string& oldMD5) :
         newRevision_(newRevision),
         status_(StoreStatus_Failure),
         attachment_(attachment),
@@ -3290,7 +3316,8 @@
         maximumStorageSize_(maximumStorageSize),
         maximumPatientCount_(maximumPatientCount),
         hasOldRevision_(hasOldRevision),
-        oldRevision_(oldRevision)
+        oldRevision_(oldRevision),
+        oldMD5_(oldMD5)
       {
       }
 
@@ -3316,7 +3343,8 @@
             if (transaction.LookupAttachment(oldFile, expectedRevision, resourceId, attachment_.GetContentType()))
             {
               if (hasOldRevision_ &&
-                  expectedRevision != oldRevision_)
+                  (expectedRevision != oldRevision_ ||
+                   oldFile.GetUncompressedMD5() != oldMD5_))
               {
                 throw OrthancException(ErrorCode_Revision);
               }
@@ -3371,7 +3399,8 @@
     };
 
 
-    Operations operations(newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision);
+    Operations operations(newRevision, attachment, publicId, maximumStorageSize, maximumPatients,
+                          hasOldRevision, oldRevision, oldMD5);
     Apply(operations);
     return operations.GetStatus();
   }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Apr 26 15:22:44 2021 +0200
@@ -580,7 +580,8 @@
                      MetadataType type,
                      const std::string& value,
                      bool hasOldRevision,
-                     int64_t oldRevision);
+                     int64_t oldRevision,
+                     const std::string& oldMD5);
 
     // Same as "SetMetadata()", but doesn't care about revisions
     void OverwriteMetadata(const std::string& publicId,
@@ -590,7 +591,8 @@
     bool DeleteMetadata(const std::string& publicId,
                         MetadataType type,
                         bool hasRevision,
-                        int64_t revision);
+                        int64_t revision,
+                        const std::string& md5);
 
     uint64_t IncrementGlobalSequence(GlobalProperty sequence,
                                      bool shared);
@@ -606,7 +608,8 @@
     bool DeleteAttachment(const std::string& publicId,
                           FileContentType type,
                           bool hasRevision,
-                          int64_t revision);
+                          int64_t revision,
+                          const std::string& md5);
 
     void LogChange(int64_t internalId,
                    ChangeType changeType,
@@ -634,6 +637,7 @@
                               uint64_t maximumStorageSize,
                               unsigned int maximumPatients,
                               bool hasOldRevision,
-                              int64_t oldRevision);
+                              int64_t oldRevision,
+                              const std::string& oldMd5);
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Apr 26 15:22:44 2021 +0200
@@ -1450,7 +1450,49 @@
   }
 
 
+  static void SetStringContentETag(RestApiOutput& output,
+                              int64_t revision,
+                              const std::string& value)
+  {
+    std::string md5;
+    Toolbox::ComputeMD5(md5, value);
+    const std::string etag = "\"" + boost::lexical_cast<std::string>(revision) + "-" + md5 + "\"";
+    output.GetLowLevelOutput().AddHeader("ETag", etag);
+  }
+  
+
+  static void SetBufferContentETag(RestApiOutput& output,
+                                   int64_t revision,
+                                   const void* data,
+                                   size_t size)
+  {
+    std::string md5;
+    Toolbox::ComputeMD5(md5, data, size);
+    const std::string etag = "\"" + boost::lexical_cast<std::string>(revision) + "-" + md5 + "\"";
+    output.GetLowLevelOutput().AddHeader("ETag", etag);
+  }
+  
+
+  static void SetAttachmentETag(RestApiOutput& output,
+                                int64_t revision,
+                                const FileInfo& info)
+  {
+    const std::string etag = ("\"" + boost::lexical_cast<std::string>(revision) + "-" +
+                              info.GetUncompressedMD5() + "\"");
+    output.GetLowLevelOutput().AddHeader("ETag", etag);
+  }
+
+
+  static std::string GetMD5(const std::string& value)
+  {
+    std::string md5;
+    Toolbox::ComputeMD5(md5, value);
+    return md5;
+  }
+
+
   static bool GetRevisionHeader(int64_t& revision /* out */,
+                                std::string& md5 /* out */,
                                 const RestApiCall& call,
                                 const std::string& header)
   {
@@ -1469,14 +1511,20 @@
 
       try
       {
-        revision = boost::lexical_cast<int64_t>(value);
-        return true;
+        size_t comma = value.find('-');
+        if (comma != std::string::npos)
+        {
+          revision = boost::lexical_cast<int64_t>(value.substr(0, comma));
+          md5 = value.substr(comma + 1);
+          return true;
+        }        
       }
       catch (boost::bad_lexical_cast&)
       {
-        throw OrthancException(ErrorCode_ParameterOutOfRange, "The \"" + header +
-                               "\" HTTP header should contain the revision as an integer, but found: " + value);
       }
+
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "The \"" + header +
+                             "\" HTTP header should contain the ETag (revision followed by MD5 hash), but found: " + value);
     }
   }
 
@@ -1510,12 +1558,13 @@
     int64_t revision;
     if (OrthancRestApi::GetIndex(call).LookupMetadata(value, revision, publicId, level, metadata))
     {
-      call.GetOutput().GetLowLevelOutput().
-        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\"");  // New in Orthanc 1.9.2
+      SetStringContentETag(call.GetOutput(), revision, value);  // New in Orthanc 1.9.2
 
       int64_t userRevision;
-      if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
-          revision == userRevision)
+      std::string userMD5;
+      if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") &&
+          userRevision == revision &&
+          userMD5 == GetMD5(value))
       {
         call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
       }
@@ -1555,9 +1604,10 @@
     {
       bool found;
       int64_t revision;
-      if (GetRevisionHeader(revision, call, "if-match"))
+      std::string md5;
+      if (GetRevisionHeader(revision, md5, call, "if-match"))
       {
-        found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, true, revision);
+        found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, true, revision, md5);
       }
       else
       {
@@ -1569,7 +1619,7 @@
         }
         else
         {
-          found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, false, -1 /* dummy value */);
+          found = OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata, false, -1 /* dummy value */, "");
         }
       }
 
@@ -1619,7 +1669,8 @@
     if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
     {
       int64_t oldRevision;
-      bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match");
+      std::string oldMD5;
+      bool hasOldRevision = GetRevisionHeader(oldRevision, oldMD5, call, "if-match");
 
       if (!hasOldRevision)
       {
@@ -1631,15 +1682,15 @@
           // inexistent as expected
           hasOldRevision = true;
           oldRevision = -1;  // dummy value
+          oldMD5.clear();  // dummy value
         }
       }
 
       int64_t newRevision;
-      OrthancRestApi::GetIndex(call).SetMetadata(newRevision, publicId, metadata, value, hasOldRevision, oldRevision);
-
-      call.GetOutput().GetLowLevelOutput().
-        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(newRevision) + "\"");  // New in Orthanc 1.9.2
-      
+      OrthancRestApi::GetIndex(call).SetMetadata(newRevision, publicId, metadata, value,
+                                                 hasOldRevision, oldRevision, oldMD5);
+
+      SetStringContentETag(call.GetOutput(), newRevision, value);  // New in Orthanc 1.9.2
       call.GetOutput().AnswerBuffer("", MimeType_PlainText);
     }
     else
@@ -1709,12 +1760,13 @@
     int64_t revision;
     if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType))
     {
-      call.GetOutput().GetLowLevelOutput().
-        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\"");  // New in Orthanc 1.9.2
+      SetAttachmentETag(call.GetOutput(), revision, info);  // New in Orthanc 1.9.2
 
       int64_t userRevision;
-      if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
-          revision == userRevision)
+      std::string userMD5;
+      if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") &&
+          revision == userRevision &&
+          info.GetUncompressedMD5() == userMD5)
       {
         call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
         return false;
@@ -1810,33 +1862,34 @@
     std::string publicId = call.GetUriComponent("id", "");
     FileContentType type = StringToContentType(call.GetUriComponent("name", ""));
 
-    if (uncompress)
+    FileInfo info;
+    if (GetAttachmentInfo(info, call))
     {
-      FileInfo info;
-      if (GetAttachmentInfo(info, call))
+      // NB: "SetAttachmentETag()" is already invoked by "GetAttachmentInfo()"
+
+      if (uncompress)
       {
         context.AnswerAttachment(call.GetOutput(), publicId, type);
       }
-    }
-    else
-    {
-      // Return the raw data (possibly compressed), as stored on the filesystem
-      std::string content;
-      int64_t revision;
-      context.ReadAttachment(content, revision, publicId, type, false);
-
-      call.GetOutput().GetLowLevelOutput().
-        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\"");  // New in Orthanc 1.9.2
-
-      int64_t userRevision;
-      if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
-          revision == userRevision)
-      {
-        call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
-      }
       else
       {
-        call.GetOutput().AnswerBuffer(content, MimeType_Binary);
+        // Return the raw data (possibly compressed), as stored on the filesystem
+        std::string content;
+        int64_t revision;
+        context.ReadAttachment(content, revision, publicId, type, false);
+
+        int64_t userRevision;
+        std::string userMD5;
+        if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") &&
+            revision == userRevision &&
+            info.GetUncompressedMD5() == userMD5)
+        {
+          call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
+        }
+        else
+        {
+          call.GetOutput().AnswerBuffer(content, MimeType_Binary);
+        }
       }
     }
   }
@@ -2037,7 +2090,8 @@
     if (IsUserContentType(contentType))  // It is forbidden to modify internal attachments
     {
       int64_t oldRevision;
-      bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match");
+      std::string oldMD5;
+      bool hasOldRevision = GetRevisionHeader(oldRevision, oldMD5, call, "if-match");
 
       if (!hasOldRevision)
       {
@@ -2049,16 +2103,15 @@
           // inexistent as expected
           hasOldRevision = true;
           oldRevision = -1;  // dummy value
+          oldMD5.clear();  // dummy value
         }
       }
 
       int64_t newRevision;
       context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(),
-                            call.GetBodySize(), hasOldRevision, oldRevision);
-
-      call.GetOutput().GetLowLevelOutput().
-        AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(newRevision) + "\"");  // New in Orthanc 1.9.2
-      
+                            call.GetBodySize(), hasOldRevision, oldRevision, oldMD5);
+
+      SetBufferContentETag(call.GetOutput(), newRevision, call.GetBodyData(), call.GetBodySize());  // New in Orthanc 1.9.2
       call.GetOutput().AnswerBuffer("{}", MimeType_Json);
     }
     else
@@ -2119,9 +2172,10 @@
     {
       bool found;
       int64_t revision;
-      if (GetRevisionHeader(revision, call, "if-match"))
+      std::string md5;
+      if (GetRevisionHeader(revision, md5, call, "if-match"))
       {
-        found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, true, revision);
+        found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, true, revision, md5);
       }
       else
       {
@@ -2133,7 +2187,8 @@
         }
         else
         {
-          found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, false, -1 /* dummy value */);
+          found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType,
+                                                                  false, -1 /* dummy value */, "" /* dummy value */);
         }
       }
 
@@ -2986,7 +3041,8 @@
       for (std::list<std::string>::const_iterator 
              instance = instances.begin(); instance != instances.end(); ++instance)
       {
-        index.DeleteAttachment(*instance, FileContentType_DicomAsJson, false /* no revision checks */, -1 /* dummy */);
+        index.DeleteAttachment(*instance, FileContentType_DicomAsJson,
+                               false /* no revision checks */, -1 /* dummy */, "" /* dummy */);
       }
     }
 
--- a/OrthancServer/Sources/ServerContext.cpp	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Mon Apr 26 15:22:44 2021 +0200
@@ -799,7 +799,8 @@
     try
     {
       int64_t newRevision;  // ignored
-      StoreStatus status = index_.AddAttachment(newRevision, modified, resourceId, true, revision);
+      StoreStatus status = index_.AddAttachment(newRevision, modified, resourceId,
+                                                true, revision, modified.GetUncompressedMD5());
       if (status != StoreStatus_Success)
       {
         accessor.Remove(modified);
@@ -986,7 +987,7 @@
               int64_t newRevision;
               AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData,
                             dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset,
-                            false /* no old revision */, -1 /* dummy revision */);
+                            false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */);
             }
           }
         }
@@ -1162,7 +1163,8 @@
                                     const void* data,
                                     size_t size,
                                     bool hasOldRevision,
-                                    int64_t oldRevision)
+                                    int64_t oldRevision,
+                                    const std::string& oldMD5)
   {
     LOG(INFO) << "Adding attachment " << EnumerationToString(attachmentType) << " to resource " << resourceId;
     
@@ -1172,7 +1174,8 @@
     StorageAccessor accessor(area_, GetMetricsRegistry());
     FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
 
-    StoreStatus status = index_.AddAttachment(newRevision, attachment, resourceId, hasOldRevision, oldRevision);
+    StoreStatus status = index_.AddAttachment(
+      newRevision, attachment, resourceId, hasOldRevision, oldRevision, oldMD5);
     if (status != StoreStatus_Success)
     {
       accessor.Remove(attachment);
--- a/OrthancServer/Sources/ServerContext.h	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Mon Apr 26 15:22:44 2021 +0200
@@ -301,7 +301,8 @@
                        const void* data,
                        size_t size,
                        bool hasOldRevision,
-                       int64_t oldRevision);
+                       int64_t oldRevision,
+                       const std::string& oldMD5);
 
     StoreStatus Store(std::string& resultPublicId,
                       DicomInstanceToStore& dicom,
--- a/OrthancServer/Sources/ServerIndex.cpp	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Mon Apr 26 15:22:44 2021 +0200
@@ -550,7 +550,8 @@
                                          const FileInfo& attachment,
                                          const std::string& publicId,
                                          bool hasOldRevision,
-                                         int64_t oldRevision)
+                                         int64_t oldRevision,
+                                         const std::string& oldMD5)
   {
     uint64_t maximumStorageSize;
     unsigned int maximumPatients;
@@ -562,6 +563,7 @@
     }
 
     return StatelessDatabaseOperations::AddAttachment(
-      newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision);
+      newRevision, attachment, publicId, maximumStorageSize, maximumPatients,
+      hasOldRevision, oldRevision, oldMD5);
   }
 }
--- a/OrthancServer/Sources/ServerIndex.h	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Mon Apr 26 15:22:44 2021 +0200
@@ -101,6 +101,7 @@
                               const FileInfo& attachment,
                               const std::string& publicId,
                               bool hasOldRevision,
-                              int64_t oldRevision);
+                              int64_t oldRevision,
+                              const std::string& oldMD5);
   };
 }
--- a/OrthancServer/Sources/ServerToolbox.cpp	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Mon Apr 26 15:22:44 2021 +0200
@@ -295,8 +295,8 @@
         ServerContext::DicomCacheLocker locker(context, *it);
 
         // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext"
-        context.GetIndex().DeleteAttachment(
-          *it, FileContentType_DicomAsJson, false /* no revision */, -1 /* dummy revision */);
+        context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson, false /* no revision */,
+                                            -1 /* dummy revision */, "" /* dummy MD5 */);
         
         context.GetIndex().ReconstructInstance(locker.GetDicom());
       }
--- a/OrthancServer/Sources/main.cpp	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/Sources/main.cpp	Mon Apr 26 15:22:44 2021 +0200
@@ -1493,6 +1493,7 @@
     static const char* const CHECK_REVISIONS = "CheckRevisions";
     
     OrthancConfiguration::ReaderLock lock;
+    
     if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
     {
       if (database.HasRevisionsSupport())
@@ -1505,6 +1506,16 @@
         LOG(WARNING) << "The custom database back-end has *no* support for revisions of metadata and attachments, "
                      << "but configuration option \"" << CHECK_REVISIONS << "\" is set to \"true\"";
       }
+      
+      static const char* const STORE_MD5 = "StoreMD5ForAttachments";
+
+      if (!lock.GetConfiguration().GetBooleanParameter(STORE_MD5, true))
+      {
+        throw OrthancException(
+          ErrorCode_ParameterOutOfRange,
+          "The revision system is enabled by configuration option \"" + std::string(CHECK_REVISIONS) +
+          "\", but won't work properly for attachments if \"" + std::string(STORE_MD5) + "\" is set to \"false\"");
+      }
     }
   }
 
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Thu Apr 22 13:27:57 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Mon Apr 26 15:22:44 2021 +0200
@@ -786,7 +786,7 @@
   {
     FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5");
     int64_t revision = -1;
-    index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1);
+    index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1, "");
     ASSERT_EQ(0, revision);
 
     index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients,