changeset 4627:f7d5372b59b3 db-changes

handling revisions of attachments
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 20 Apr 2021 15:11:59 +0200
parents 686f189a903d
children 5fabef29c4ff
files NEWS OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/ServerIndex.h OrthancServer/Sources/ServerJobs/ArchiveJob.cpp OrthancServer/Sources/ServerToolbox.cpp OrthancServer/UnitTestsSources/ServerIndexTests.cpp
diffstat 17 files changed, 361 insertions(+), 123 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Apr 19 10:28:43 2021 +0200
+++ b/NEWS	Tue Apr 20 15:11:59 2021 +0200
@@ -12,8 +12,8 @@
 --------
 
 * API version upgraded to 12
-* "/.../{id}/metadata/{name}" URIs handle the HTTP headers "If-Match", "If-None-Match" and
-  "ETag" to cope with revisions
+* "/.../{id}/metadata/{name}" and "/.../{id}/attachments/{name}/..." URIs handle the
+  HTTP headers "If-Match", "If-None-Match" and "ETag" to cope with revisions
 
 Plugins
 -------
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -651,8 +651,10 @@
     
 
     virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment) ORTHANC_OVERRIDE
+                               const FileInfo& attachment,
+                               int64_t revision) ORTHANC_OVERRIDE
     {
+      // "revision" is not used, as it was added in Orthanc 1.9.2
       OrthancPluginAttachment tmp;
       tmp.uuid = attachment.GetUuid().c_str();
       tmp.contentType = static_cast<int32_t>(attachment.GetContentType());
@@ -1104,6 +1106,7 @@
 
     
     virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t& revision,
                                   int64_t id,
                                   FileContentType contentType) ORTHANC_OVERRIDE
     {
@@ -1111,6 +1114,8 @@
 
       CheckSuccess(that_.backend_.lookupAttachment
                    (that_.GetContext(), that_.payload_, id, static_cast<int32_t>(contentType)));
+      
+      revision = 0;  // Dummy value, as revisions were added in Orthanc 1.9.2
 
       if (type_ == _OrthancPluginDatabaseAnswerType_None)
       {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -305,7 +305,8 @@
 
     
     virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment) ORTHANC_OVERRIDE
+                               const FileInfo& attachment,
+                               int64_t revision) ORTHANC_OVERRIDE
     {
       OrthancPluginAttachment tmp;
       tmp.uuid = attachment.GetUuid().c_str();
@@ -316,7 +317,7 @@
       tmp.compressedSize = attachment.GetCompressedSize();
       tmp.compressedHash = attachment.GetCompressedMD5().c_str();
 
-      CheckSuccess(that_.backend_.addAttachment(transaction_, id, &tmp));
+      CheckSuccess(that_.backend_.addAttachment(transaction_, id, &tmp, revision));
       CheckNoEvent();
     }
 
@@ -666,10 +667,11 @@
 
     
     virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t& revision,
                                   int64_t id,
                                   FileContentType contentType) ORTHANC_OVERRIDE
     {
-      CheckSuccess(that_.backend_.lookupAttachment(transaction_, id, static_cast<int32_t>(contentType)));
+      CheckSuccess(that_.backend_.lookupAttachment(transaction_, &revision, id, static_cast<int32_t>(contentType)));
       CheckNoEvent();
 
       uint32_t count;
@@ -1127,8 +1129,8 @@
     CHECK_FUNCTION_EXISTS(backend_, getLastExportedResource);
     CHECK_FUNCTION_EXISTS(backend_, getMainDicomTags);
     CHECK_FUNCTION_EXISTS(backend_, getPublicId);
+    CHECK_FUNCTION_EXISTS(backend_, getResourceType);
     CHECK_FUNCTION_EXISTS(backend_, getResourcesCount);
-    CHECK_FUNCTION_EXISTS(backend_, getResourceType);
     CHECK_FUNCTION_EXISTS(backend_, getTotalCompressedSize);
     CHECK_FUNCTION_EXISTS(backend_, getTotalUncompressedSize);
     CHECK_FUNCTION_EXISTS(backend_, isDiskSizeAbove);
@@ -1142,8 +1144,8 @@
     CHECK_FUNCTION_EXISTS(backend_, lookupMetadata);
     CHECK_FUNCTION_EXISTS(backend_, lookupParent);
     CHECK_FUNCTION_EXISTS(backend_, lookupResource);
+    CHECK_FUNCTION_EXISTS(backend_, lookupResourceAndParent);
     CHECK_FUNCTION_EXISTS(backend_, lookupResources);
-    CHECK_FUNCTION_EXISTS(backend_, lookupResourceAndParent);
     CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle);
     CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle2);
     CHECK_FUNCTION_EXISTS(backend_, setGlobalProperty);
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Tue Apr 20 15:11:59 2021 +0200
@@ -1109,10 +1109,12 @@
     
     OrthancPluginErrorCode (*commit) (OrthancPluginDatabaseTransaction* transaction,
                                       int64_t fileSizeDelta);
-    
+
+    /* A call to "addAttachment()" guarantees that this attachment is not already existing ("INSERT") */
     OrthancPluginErrorCode (*addAttachment) (OrthancPluginDatabaseTransaction* transaction,
                                              int64_t id,
-                                             const OrthancPluginAttachment* attachment);
+                                             const OrthancPluginAttachment* attachment,
+                                             int64_t revision);
 
     OrthancPluginErrorCode (*clearChanges) (OrthancPluginDatabaseTransaction* transaction);
     
@@ -1243,6 +1245,7 @@
 
     /* Answer is read using "readAnswerAttachment()" */
     OrthancPluginErrorCode (*lookupAttachment) (OrthancPluginDatabaseTransaction* transaction,
+                                                int64_t* revision /* out */,
                                                 int64_t resourceId,
                                                 int32_t contentType);
 
@@ -1297,6 +1300,7 @@
                                                  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,
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Tue Apr 20 15:11:59 2021 +0200
@@ -78,8 +78,12 @@
       // attachments (cf. "fastGetTotalSize_")
       virtual void Commit(int64_t fileSizeDelta) = 0;
 
+      // A call to "AddAttachment()" guarantees that this attachment
+      // is not already existing. This is different from
+      // "SetMetadata()" that might have to replace an older value.
       virtual void AddAttachment(int64_t id,
-                                 const FileInfo& attachment) = 0;
+                                 const FileInfo& attachment,
+                                 int64_t revision) = 0;
 
       virtual void ClearChanges() = 0;
 
@@ -150,6 +154,7 @@
       virtual void LogExportedResource(const ExportedResource& resource) = 0;
     
       virtual bool LookupAttachment(FileInfo& attachment,
+                                    int64_t& revision,
                                     int64_t id,
                                     FileContentType contentType) = 0;
 
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -326,8 +326,10 @@
 
     
     virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment) ORTHANC_OVERRIDE
+                               const FileInfo& attachment,
+                               int64_t revision) ORTHANC_OVERRIDE
     {
+      // TODO - REVISIONS
       SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
       s.BindInt64(0, id);
       s.BindInt(1, attachment.GetContentType());
@@ -799,6 +801,7 @@
 
 
     virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t& revision,
                                   int64_t id,
                                   FileContentType contentType) ORTHANC_OVERRIDE
     {
@@ -821,6 +824,7 @@
                               static_cast<CompressionType>(s.ColumnInt(2)),
                               s.ColumnInt64(3),
                               s.ColumnString(5));
+        revision = 0;   // TODO - REVISIONS
         return true;
       }
     }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -920,7 +920,8 @@
               target["Type"] = "Instance";
 
               FileInfo attachment;
-              if (!transaction.LookupAttachment(attachment, internalId, FileContentType_Dicom))
+              int64_t revision;  // ignored
+              if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom))
               {
                 throw OrthancException(ErrorCode_InternalError);
               }
@@ -1015,10 +1016,11 @@
 
 
   bool StatelessDatabaseOperations::LookupAttachment(FileInfo& attachment,
+                                                     int64_t& revision,
                                                      const std::string& instancePublicId,
                                                      FileContentType contentType)
   {
-    class Operations : public ReadOnlyOperationsT4<bool&, FileInfo&, const std::string&, FileContentType>
+    class Operations : public ReadOnlyOperationsT5<bool&, FileInfo&, int64_t&, const std::string&, FileContentType>
     {
     public:
       virtual void ApplyTuple(ReadOnlyTransaction& transaction,
@@ -1026,13 +1028,13 @@
       {
         int64_t internalId;
         ResourceType type;
-        if (!transaction.LookupResource(internalId, type, tuple.get<2>()))
+        if (!transaction.LookupResource(internalId, type, tuple.get<3>()))
         {
           throw OrthancException(ErrorCode_UnknownResource);
         }
-        else if (transaction.LookupAttachment(tuple.get<1>(), internalId, tuple.get<3>()))
+        else if (transaction.LookupAttachment(tuple.get<1>(), tuple.get<2>(), internalId, tuple.get<4>()))
         {
-          assert(tuple.get<1>().GetContentType() == tuple.get<3>());
+          assert(tuple.get<1>().GetContentType() == tuple.get<4>());
           tuple.get<0>() = true;
         }
         else
@@ -1044,7 +1046,7 @@
 
     bool found;
     Operations operations;
-    operations.Apply(*this, found, attachment, instancePublicId, contentType);
+    operations.Apply(*this, found, attachment, revision, instancePublicId, contentType);
     return found;
   }
 
@@ -1547,7 +1549,8 @@
                    it = f.begin(); it != f.end(); ++it)
             {
               FileInfo attachment;
-              if (transaction.LookupAttachment(attachment, resource, *it))
+              int64_t revision;  // ignored
+              if (transaction.LookupAttachment(attachment, revision, resource, *it))
               {
                 if (attachment.GetContentType() == FileContentType_Dicom)
                 {
@@ -2244,7 +2247,7 @@
             }
             else
             {
-              newRevision_ = oldRevision_ + 1;
+              newRevision_ = expectedRevision + 1;
             }
           }
           else
@@ -2478,23 +2481,38 @@
   }
 
 
-  void StatelessDatabaseOperations::DeleteAttachment(const std::string& publicId,
-                                                     FileContentType type)
+  bool StatelessDatabaseOperations::DeleteAttachment(const std::string& publicId,
+                                                     FileContentType type,
+                                                     bool hasRevision,
+                                                     int64_t revision)
   {
     class Operations : public IReadWriteOperations
     {
     private:
       const std::string&  publicId_;
       FileContentType     type_;
-      
+      bool                hasRevision_;
+      int64_t             revision_;
+      bool                found_;
+
     public:
       Operations(const std::string& publicId,
-                 FileContentType type) :
+                 FileContentType type,
+                 bool hasRevision,
+                 int64_t revision) :
         publicId_(publicId),
-        type_(type)
+        type_(type),
+        hasRevision_(hasRevision),
+        revision_(revision),
+        found_(false)
       {
       }
         
+      bool HasFound() const
+      {
+        return found_;
+      }
+      
       virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
       {
         ResourceType resourceType;
@@ -2505,18 +2523,35 @@
         }
         else
         {
-          transaction.DeleteAttachment(id, type_);
+          FileInfo info;
+          int64_t expectedRevision;
+          if (transaction.LookupAttachment(info, expectedRevision, id, type_))
+          {
+            if (hasRevision_ &&
+                expectedRevision != revision_)
+            {
+              throw OrthancException(ErrorCode_Revision);
+            }
+            
+            found_ = true;
+            transaction.DeleteAttachment(id, type_);
           
-          if (IsUserContentType(type_))
+            if (IsUserContentType(type_))
+            {
+              transaction.LogChange(id, ChangeType_UpdatedAttachment, resourceType, publicId_);
+            }
+          }
+          else
           {
-            transaction.LogChange(id, ChangeType_UpdatedAttachment, resourceType, publicId_);
+            found_ = false;
           }
         }
       }
     };
 
-    Operations operations(publicId, type);
+    Operations operations(publicId, type, hasRevision, revision);
     Apply(operations);
+    return operations.HasFound();
   }
 
 
@@ -3018,7 +3053,7 @@
           for (Attachments::const_iterator it = attachments_.begin();
                it != attachments_.end(); ++it)
           {
-            transaction.AddAttachment(instanceId, *it);
+            transaction.AddAttachment(instanceId, *it, 0 /* this is the first revision */);
           }
 
       
@@ -3219,30 +3254,42 @@
   }
 
 
-  StoreStatus StatelessDatabaseOperations::AddAttachment(const FileInfo& attachment,
+  StoreStatus StatelessDatabaseOperations::AddAttachment(int64_t& newRevision,
+                                                         const FileInfo& attachment,
                                                          const std::string& publicId,
                                                          uint64_t maximumStorageSize,
-                                                         unsigned int maximumPatients)
+                                                         unsigned int maximumPatients,
+                                                         bool hasOldRevision,
+                                                         int64_t oldRevision)
   {
     class Operations : public IReadWriteOperations
     {
     private:
+      int64_t&            newRevision_;
       StoreStatus         status_;
       const FileInfo&     attachment_;
       const std::string&  publicId_;
       uint64_t            maximumStorageSize_;
       unsigned int        maximumPatientCount_;
-      
+      bool                hasOldRevision_;
+      int64_t             oldRevision_;
+
     public:
-      Operations(const FileInfo& attachment,
+      Operations(int64_t& newRevision,
+                 const FileInfo& attachment,
                  const std::string& publicId,
                  uint64_t maximumStorageSize,
-                 unsigned int maximumPatientCount) :
+                 unsigned int maximumPatientCount,
+                 bool hasOldRevision,
+                 int64_t oldRevision) :
+        newRevision_(newRevision),
         status_(StoreStatus_Failure),
         attachment_(attachment),
         publicId_(publicId),
         maximumStorageSize_(maximumStorageSize),
-        maximumPatientCount_(maximumPatientCount)
+        maximumPatientCount_(maximumPatientCount),
+        hasOldRevision_(hasOldRevision),
+        oldRevision_(oldRevision)
       {
       }
 
@@ -3261,8 +3308,30 @@
         }
         else
         {
-          // Remove possible previous attachment
-          transaction.DeleteAttachment(resourceId, attachment_.GetContentType());
+          // Possibly remove previous attachment
+          {
+            FileInfo oldFile;
+            int64_t expectedRevision;
+            if (transaction.LookupAttachment(oldFile, expectedRevision, resourceId, attachment_.GetContentType()))
+            {
+              if (hasOldRevision_ &&
+                  expectedRevision != oldRevision_)
+              {
+                throw OrthancException(ErrorCode_Revision);
+              }
+              else
+              {
+                newRevision_ = expectedRevision + 1;
+                transaction.DeleteAttachment(resourceId, attachment_.GetContentType());
+              }
+            }
+            else
+            {
+              // The attachment is not existing yet: Ignore "oldRevision"
+              // and initialize a new sequence of revisions
+              newRevision_ = 0;
+            }
+          }
 
           // Locate the patient of the target resource
           int64_t patientId = resourceId;
@@ -3286,7 +3355,7 @@
           transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
                               attachment_.GetCompressedSize(), transaction.GetPublicId(patientId));
 
-          transaction.AddAttachment(resourceId, attachment_);
+          transaction.AddAttachment(resourceId, attachment_, newRevision_);
 
           if (IsUserContentType(attachment_.GetContentType()))
           {
@@ -3301,7 +3370,7 @@
     };
 
 
-    Operations operations(attachment, publicId, maximumStorageSize, maximumPatients);
+    Operations operations(newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision);
     Apply(operations);
     return operations.GetStatus();
   }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Apr 20 15:11:59 2021 +0200
@@ -240,10 +240,11 @@
       }
 
       bool LookupAttachment(FileInfo& attachment,
+                            int64_t& revision,
                             int64_t id,
                             FileContentType contentType)
       {
-        return transaction_.LookupAttachment(attachment, id, contentType);
+        return transaction_.LookupAttachment(attachment, revision, id, contentType);
       }
       
       bool LookupGlobalProperty(std::string& target,
@@ -294,9 +295,10 @@
       }
 
       void AddAttachment(int64_t id,
-                         const FileInfo& attachment)
+                         const FileInfo& attachment,
+                         int64_t revision)
       {
-        transaction_.AddAttachment(id, attachment);
+        transaction_.AddAttachment(id, attachment, revision);
       }
       
       void ClearChanges()
@@ -481,6 +483,7 @@
                              /* out */ uint64_t& countInstances);
 
     bool LookupAttachment(FileInfo& attachment,
+                          int64_t& revision,
                           const std::string& instancePublicId,
                           FileContentType contentType);
 
@@ -600,8 +603,10 @@
                            bool shared,
                            const std::string& value);
 
-    void DeleteAttachment(const std::string& publicId,
-                          FileContentType type);
+    bool DeleteAttachment(const std::string& publicId,
+                          FileContentType type,
+                          bool hasRevision,
+                          int64_t revision);
 
     void LogChange(int64_t internalId,
                    ChangeType changeType,
@@ -623,9 +628,12 @@
                       uint64_t maximumStorageSize,
                       unsigned int maximumPatients);
 
-    StoreStatus AddAttachment(const FileInfo& attachment,
+    StoreStatus AddAttachment(int64_t& newRevision /*out*/,
+                              const FileInfo& attachment,
                               const std::string& publicId,
                               uint64_t maximumStorageSize,
-                              unsigned int maximumPatients);
+                              unsigned int maximumPatients,
+                              bool hasOldRevision,
+                              int64_t oldRevision);
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -1541,7 +1541,7 @@
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)")
         .SetHttpHeader("If-Match", "Revision of the metadata, to check if its content has not changed and can "
-                       "be deleted. This option is mandatory if `CheckRevision` option is `true`.");
+                       "be deleted. This header is mandatory if `CheckRevision` option is `true`.");
       return;
     }
 
@@ -1686,15 +1686,48 @@
   }
 
 
-  static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call)
+  static void AddAttachmentDocumentation(RestApiGetCall& call,
+                                         const std::string& resourceType)
+  {
+    call.GetDocumentation()
+      .SetUriArgument("id", "Orthanc identifier of the " + resourceType + " of interest")
+      .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
+      .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations")
+      .SetHttpHeader("If-None-Match", "Optional revision of the attachment, to check if its content has changed");
+  }
+
+  
+  static bool GetAttachmentInfo(FileInfo& info,
+                                RestApiGetCall& call)
   {
     CheckValidResourceType(call);
  
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
+    const std::string publicId = call.GetUriComponent("id", "");
+    const std::string name = call.GetUriComponent("name", "");
     FileContentType contentType = StringToContentType(name);
 
-    return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType);
+    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
+
+      int64_t userRevision;
+      if (GetRevisionHeader(userRevision, call, "If-None-Match") &&
+          revision == userRevision)
+      {
+        call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified);
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
   }
 
 
@@ -1704,12 +1737,11 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag("Other")
         .SetSummary("List operations on attachments")
         .SetDescription("Get the list of the operations that are available for attachments associated with the given " + r)
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_Json, "List of the available operations")
         .SetHttpGetSample("https://demo.orthanc-server.com/instances/d94d9a03-3003b047-a4affc69-322313b2-680530a2/attachments/dicom", true);
       return;
@@ -1765,7 +1797,9 @@
                         std::string(uncompress ? "" : ". The attachment will not be decompressed if `StorageCompression` is `true`."))
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
-        .AddAnswerType(MimeType_Binary, "The attachment");
+        .AddAnswerType(MimeType_Binary, "The attachment")
+        .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations")
+        .SetHttpHeader("If-None-Match", "Optional revision of the metadata, to check if its content has changed");
       return;
     }
 
@@ -1778,14 +1812,32 @@
 
     if (uncompress)
     {
-      context.AnswerAttachment(call.GetOutput(), publicId, type);
+      FileInfo info;
+      if (GetAttachmentInfo(info, call))
+      {
+        context.AnswerAttachment(call.GetOutput(), publicId, type);
+      }
     }
     else
     {
       // Return the raw data (possibly compressed), as stored on the filesystem
       std::string content;
-      context.ReadAttachment(content, publicId, type, false);
-      call.GetOutput().AnswerBuffer(content, MimeType_Binary);
+      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);
+      }
     }
   }
 
@@ -1796,12 +1848,11 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Get size of attachment")
         .SetDescription("Get the size of one attachment associated with the given " + r)
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "The size of the attachment");
       return;
     }
@@ -1820,13 +1871,12 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Get size of attachment on disk")
         .SetDescription("Get the size of one attachment associated with the given " + r + ", as stored on the disk. "
                         "This is different from `.../size` iff `EnableStorage` is `true`.")
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "The size of the attachment, as stored on the disk");
       return;
     }
@@ -1845,12 +1895,11 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Get MD5 of attachment")
         .SetDescription("Get the MD5 hash of one attachment associated with the given " + r)
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment");
       return;
     }
@@ -1870,13 +1919,12 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Get MD5 of attachment on disk")
         .SetDescription("Get the MD5 hash of one attachment associated with the given " + r + ", as stored on the disk. "
                         "This is different from `.../md5` iff `EnableStorage` is `true`.")
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment, as stored on the disk");
       return;
     }
@@ -1911,9 +1959,11 @@
 
     std::string publicId = call.GetUriComponent("id", "");
     std::string name = call.GetUriComponent("name", "");
+    FileContentType contentType = StringToContentType(name);
 
     FileInfo info;
-    if (!GetAttachmentInfo(info, call) ||
+    int64_t revision;  // Ignored
+    if (!OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType) ||
         info.GetCompressedMD5() == "" ||
         info.GetUncompressedMD5() == "")
     {
@@ -1925,7 +1975,7 @@
 
     // First check whether the compressed data is correctly stored in the disk
     std::string data;
-    context.ReadAttachment(data, publicId, StringToContentType(name), false);
+    context.ReadAttachment(data, revision, publicId, StringToContentType(name), false);
 
     std::string actualMD5;
     Toolbox::ComputeMD5(actualMD5, data);
@@ -1940,7 +1990,7 @@
       }
       else
       {
-        context.ReadAttachment(data, publicId, StringToContentType(name), true);        
+        context.ReadAttachment(data, revision, publicId, StringToContentType(name), true);        
         Toolbox::ComputeMD5(actualMD5, data);
         ok = (actualMD5 == info.GetUncompressedMD5());
       }
@@ -1972,7 +2022,8 @@
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
         .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddRequestType(MimeType_Binary, "Binary data containing the attachment")
-        .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success");
+        .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success")
+        .SetHttpHeader("If-Match", "Revision of the attachment, if this is not the first time this attachment is set.");
       return;
     }
 
@@ -1983,9 +2034,31 @@
     std::string name = call.GetUriComponent("name", "");
 
     FileContentType contentType = StringToContentType(name);
-    if (IsUserContentType(contentType) &&  // It is forbidden to modify internal attachments
-        context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize()))
+    if (IsUserContentType(contentType))  // It is forbidden to modify internal attachments
     {
+      int64_t oldRevision;
+      bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match");
+
+      if (!hasOldRevision)
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
+        {
+          // "StatelessDatabaseOperations::AddAttachment()" will ignore
+          // the actual value of "oldRevision" if the metadata is
+          // inexistent as expected
+          hasOldRevision = true;
+          oldRevision = -1;  // 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.GetOutput().AnswerBuffer("{}", MimeType_Json);
     }
     else
@@ -2007,7 +2080,9 @@
         .SetDescription("Delete an attachment associated with the given DICOM " + r +
                         ". This call will fail if trying to delete a system attachment (i.e. whose index is < 1024).")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)");
+        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
+        .SetHttpHeader("If-Match", "Revision of the attachment, to check if its content has not changed and can "
+                       "be deleted. This header is mandatory if `CheckRevision` option is `true`.");
       return;
     }
 
@@ -2042,8 +2117,34 @@
 
     if (allowed) 
     {
-      OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType);
-      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+      bool found;
+      int64_t revision;
+      if (GetRevisionHeader(revision, call, "if-match"))
+      {
+        found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, true, revision);
+      }
+      else
+      {
+        OrthancConfiguration::ReaderLock lock;
+        if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
+        {
+          throw OrthancException(ErrorCode_Revision,
+                                 "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\"");
+        }
+        else
+        {
+          found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, false, -1 /* dummy value */);
+        }
+      }
+
+      if (found)
+      {
+        call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
     }
     else
     {
@@ -2085,12 +2186,11 @@
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
       std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
+      AddAttachmentDocumentation(call, r);
       call.GetDocumentation()
         .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
         .SetSummary("Is attachment compressed?")
         .SetDescription("Test whether the attachment has been stored as a compressed file on the disk.")
-        .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)")
         .AddAnswerType(MimeType_PlainText, "`0` if the attachment was stored uncompressed, `1` if it was compressed");
       return;
     }
@@ -2886,7 +2986,7 @@
       for (std::list<std::string>::const_iterator 
              instance = instances.begin(); instance != instances.end(); ++instance)
       {
-        index.DeleteAttachment(*instance, FileContentType_DicomAsJson);
+        index.DeleteAttachment(*instance, FileContentType_DicomAsJson, false /* no revision checks */, -1 /* dummy */);
       }
     }
 
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -153,7 +153,8 @@
         if (level_ == ResourceType_Instance)
         {
           FileInfo info;
-          if (context_.GetIndex().LookupAttachment(info, publicId, FileContentType_Dicom))
+          int64_t revision;  // Ignored
+          if (context_.GetIndex().LookupAttachment(info, revision, publicId, FileContentType_Dicom))
           {
             std::unique_ptr<File> f(new File(s + ".dcm"));
             f->SetMimeType(MimeType_Dicom);
@@ -508,7 +509,8 @@
           LookupTime(time, context_, *it, ResourceType_Instance, MetadataType_Instance_ReceptionDate);
 
           FileInfo info;
-          if (context_.GetIndex().LookupAttachment(info, *it, FileContentType_Dicom))
+          int64_t revision;  // Ignored
+          if (context_.GetIndex().LookupAttachment(info, revision, *it, FileContentType_Dicom))
           {
             std::unique_ptr<File> resource(new File(*it + ".dcm"));
             resource->SetMimeType(MimeType_Dicom);
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -753,13 +753,16 @@
                                        FileContentType content)
   {
     FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, resourceId, content))
+    int64_t revision;
+    if (!index_.LookupAttachment(attachment, revision, resourceId, content))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
-
-    StorageAccessor accessor(area_, GetMetricsRegistry());
-    accessor.AnswerFile(output, attachment, GetFileContentMime(content));
+    else
+    {
+      StorageAccessor accessor(area_, GetMetricsRegistry());
+      accessor.AnswerFile(output, attachment, GetFileContentMime(content));
+    }
   }
 
 
@@ -773,7 +776,8 @@
               << compression; 
 
     FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, resourceId, attachmentType))
+    int64_t revision;
+    if (!index_.LookupAttachment(attachment, revision, resourceId, attachmentType))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
@@ -794,7 +798,8 @@
 
     try
     {
-      StoreStatus status = index_.AddAttachment(modified, resourceId);
+      int64_t newRevision;  // ignored
+      StoreStatus status = index_.AddAttachment(newRevision, modified, resourceId, true, revision);
       if (status != StoreStatus_Success)
       {
         accessor.Remove(modified);
@@ -833,8 +838,9 @@
      **/
     
     FileInfo attachment;
+    int64_t revision;
 
-    if (index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomUntilPixelData))
+    if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData))
     {
       std::string dicom;
 
@@ -893,7 +899,7 @@
 
       if (hasPixelDataOffset &&
           area_.HasReadRange() &&
-          index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom) &&
+          index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom) &&
           attachment.GetCompressionType() == CompressionType_None)
       {
         /**
@@ -922,7 +928,7 @@
         }
       }
       else if (ignoreTagLength.empty() &&
-               index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomAsJson))
+               index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomAsJson))
       {
         /**
          * CASE 3: This instance was created using Orthanc <=
@@ -978,8 +984,10 @@
             if (!area_.HasReadRange() ||
                 compressionEnabled_)
             {
-              AddAttachment(instancePublicId, FileContentType_DicomUntilPixelData,
-                            dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset);
+              int64_t newRevision;
+              AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData,
+                            dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset,
+                            false /* no old revision */, -1 /* dummy revision */);
             }
           }
         }
@@ -999,7 +1007,8 @@
   void ServerContext::ReadDicom(std::string& dicom,
                                 const std::string& instancePublicId)
   {
-    ReadAttachment(dicom, instancePublicId, FileContentType_Dicom, true /* uncompress */);
+    int64_t revision;
+    ReadAttachment(dicom, revision, instancePublicId, FileContentType_Dicom, true /* uncompress */);
   }
     
 
@@ -1012,14 +1021,14 @@
     }
     
     FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom))
+    int64_t revision;  // Ignored
+    if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom))
     {
       throw OrthancException(ErrorCode_InternalError,
                              "Unable to read the DICOM file of instance " + instancePublicId);
     }
 
     std::string s;
-    int64_t revision;  // Ignored
 
     if (attachment.GetCompressionType() == CompressionType_None &&
         index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance,
@@ -1046,12 +1055,13 @@
   
 
   void ServerContext::ReadAttachment(std::string& result,
+                                     int64_t& revision,
                                      const std::string& instancePublicId,
                                      FileContentType content,
                                      bool uncompressIfNeeded)
   {
     FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, instancePublicId, content))
+    if (!index_.LookupAttachment(attachment, revision, instancePublicId, content))
     {
       throw OrthancException(ErrorCode_InternalError,
                              "Unable to read attachment " + EnumerationToString(content) +
@@ -1147,10 +1157,13 @@
   }
 
 
-  bool ServerContext::AddAttachment(const std::string& resourceId,
+  bool ServerContext::AddAttachment(int64_t& newRevision,
+                                    const std::string& resourceId,
                                     FileContentType attachmentType,
                                     const void* data,
-                                    size_t size)
+                                    size_t size,
+                                    bool hasOldRevision,
+                                    int64_t oldRevision)
   {
     LOG(INFO) << "Adding attachment " << EnumerationToString(attachmentType) << " to resource " << resourceId;
     
@@ -1160,7 +1173,7 @@
     StorageAccessor accessor(area_, GetMetricsRegistry());
     FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
 
-    StoreStatus status = index_.AddAttachment(attachment, resourceId);
+    StoreStatus status = index_.AddAttachment(newRevision, attachment, resourceId, hasOldRevision, oldRevision);
     if (status != StoreStatus_Success)
     {
       accessor.Remove(attachment);
--- a/OrthancServer/Sources/ServerContext.h	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Tue Apr 20 15:11:59 2021 +0200
@@ -295,10 +295,13 @@
       return compressionEnabled_;
     }
 
-    bool AddAttachment(const std::string& resourceId,
+    bool AddAttachment(int64_t& newRevision,
+                       const std::string& resourceId,
                        FileContentType attachmentType,
                        const void* data,
-                       size_t size);
+                       size_t size,
+                       bool hasOldRevision,
+                       int64_t oldRevision);
 
     StoreStatus Store(std::string& resultPublicId,
                       DicomInstanceToStore& dicom,
@@ -327,6 +330,7 @@
 
     // This method is for low-level operations on "/instances/.../attachments/..."
     void ReadAttachment(std::string& result,
+                        int64_t& revision,
                         const std::string& instancePublicId,
                         FileContentType content,
                         bool uncompressIfNeeded);
--- a/OrthancServer/Sources/ServerIndex.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -546,8 +546,11 @@
   }
 
   
-  StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment,
-                                         const std::string& publicId)
+  StoreStatus ServerIndex::AddAttachment(int64_t& newRevision,
+                                         const FileInfo& attachment,
+                                         const std::string& publicId,
+                                         bool hasOldRevision,
+                                         int64_t oldRevision)
   {
     uint64_t maximumStorageSize;
     unsigned int maximumPatients;
@@ -559,6 +562,6 @@
     }
 
     return StatelessDatabaseOperations::AddAttachment(
-      attachment, publicId, maximumStorageSize, maximumPatients);
+      newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision);
   }
 }
--- a/OrthancServer/Sources/ServerIndex.h	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Tue Apr 20 15:11:59 2021 +0200
@@ -97,7 +97,10 @@
                       bool hasPixelDataOffset,
                       uint64_t pixelDataOffset);
 
-    StoreStatus AddAttachment(const FileInfo& attachment,
-                              const std::string& publicId);
+    StoreStatus AddAttachment(int64_t& newRevision /*out*/,
+                              const FileInfo& attachment,
+                              const std::string& publicId,
+                              bool hasOldRevision,
+                              int64_t oldRevision);
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -229,7 +229,8 @@
       if (level_ == ResourceType_Instance)
       {
         FileInfo tmp;
-        if (index.LookupAttachment(tmp, id, FileContentType_Dicom))
+        int64_t revision;  // ignored
+        if (index.LookupAttachment(tmp, revision, id, FileContentType_Dicom))
         {
           instances_.push_back(Instance(id, tmp.GetUncompressedSize()));
         }
--- a/OrthancServer/Sources/ServerToolbox.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -165,7 +165,8 @@
 
         // Get the DICOM file attached to some instances in the resource
         FileInfo attachment;
-        if (!transaction.LookupAttachment(attachment, instance, FileContentType_Dicom))
+        int64_t revision;
+        if (!transaction.LookupAttachment(attachment, revision, instance, FileContentType_Dicom))
         {
           throw OrthancException(ErrorCode_InternalError,
                                  "Cannot retrieve the DICOM file associated with instance " +
@@ -294,7 +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);
+        context.GetIndex().DeleteAttachment(
+          *it, FileContentType_DicomAsJson, false /* no revision */, -1 /* dummy revision */);
         
         context.GetIndex().ReconstructInstance(locker.GetDicom());
       }
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Mon Apr 19 10:28:43 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Tue Apr 20 15:11:59 2021 +0200
@@ -302,10 +302,10 @@
   ASSERT_EQ(0u, md.size());
 
   transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", 
-                                       CompressionType_ZlibWithSize, 21, "compressedMD5"));
-  transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"));
-  transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "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);
+  
   // TODO - REVISIONS - "42" is revision number, that is not currently stored (*)
   transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42);
   
@@ -362,7 +362,8 @@
   ASSERT_EQ("World", s);
 
   FileInfo att;
-  ASSERT_TRUE(transaction_->LookupAttachment(att, a[4], FileContentType_DicomAsJson));
+  ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson));
+  ASSERT_EQ(0, revision);  // "0" instead of "42" because of (*)
   ASSERT_EQ("my json file", att.GetUuid());
   ASSERT_EQ(21u, att.GetCompressedSize());
   ASSERT_EQ("md5", att.GetUncompressedMD5());
@@ -370,7 +371,8 @@
   ASSERT_EQ(42u, att.GetUncompressedSize());
   ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType());
 
-  ASSERT_TRUE(transaction_->LookupAttachment(att, a[6], FileContentType_Dicom));
+  ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom));
+  ASSERT_EQ(0, revision);  // "0" instead of "42" because of (*)
   ASSERT_EQ("world", att.GetUuid());
   ASSERT_EQ(44u, att.GetCompressedSize());
   ASSERT_EQ("md5", att.GetUncompressedMD5());
@@ -482,7 +484,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)));
+                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
     ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
   }
 
@@ -543,7 +545,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)));
+                                                      "md5-" + boost::lexical_cast<std::string>(i)), 42);
     ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i]));
   }
 
@@ -783,7 +785,9 @@
   for (size_t i = 0; i < ids.size(); i++)
   {
     FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5");
-    index.AddAttachment(info, ids[i]);
+    int64_t revision = -1;
+    index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1);
+    ASSERT_EQ(0, revision);
 
     index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                               countStudies, countSeries, countInstances);
@@ -861,12 +865,17 @@
 
     {
       FileInfo nope;
-      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson));
+      int64_t revision;
+      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson));
     }
 
     FileInfo dicom1, pixelData1;
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, id, FileContentType_Dicom));
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, id, FileContentType_DicomUntilPixelData));
+    int64_t revision;
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, revision, id, FileContentType_Dicom));
+    ASSERT_EQ(0, revision);
+    revision = -1;
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, revision, id, FileContentType_DicomUntilPixelData));
+    ASSERT_EQ(0, revision);
 
     context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                            countStudies, countSeries, countInstances);
@@ -906,12 +915,16 @@
 
     {
       FileInfo nope;
-      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson));
+      int64_t revision;
+      ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson));
     }
 
     FileInfo dicom2, pixelData2;
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, id, FileContentType_Dicom));
-    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, id, FileContentType_DicomUntilPixelData));
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, revision, id, FileContentType_Dicom));
+    ASSERT_EQ(0, revision);
+    revision = -1;
+    ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, revision, id, FileContentType_DicomUntilPixelData));
+    ASSERT_EQ(0, revision);
 
     context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
                                            countStudies, countSeries, countInstances);