changeset 4796:94616af363ec filter-store-instance

added ReceivedCStoreInstanceFilter lua callback + OrthancPluginRegisterIncomingCStoreInstanceFilter in sdk
author Alain Mazy <am@osimis.io>
date Fri, 01 Oct 2021 18:36:45 +0200
parents 22d5b611dea7
children 0bd98c52474b
files NEWS OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp OrthancFramework/Sources/Lua/LuaFunctionCall.cpp OrthancFramework/Sources/Lua/LuaFunctionCall.h OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Sources/IServerListener.h OrthancServer/Sources/LuaScripting.cpp OrthancServer/Sources/LuaScripting.h OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp OrthancServer/Sources/main.cpp OrthancServer/UnitTestsSources/ServerIndexTests.cpp OrthancServer/UnitTestsSources/ServerJobsTests.cpp
diffstat 22 files changed, 300 insertions(+), 58 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Sep 30 17:52:07 2021 +0200
+++ b/NEWS	Fri Oct 01 18:36:45 2021 +0200
@@ -1,11 +1,23 @@
 Pending changes in the mainline
 ===============================
 
+
 * Fix handling of option "DeidentifyLogs", notably for tags (0010,0010) and (0010,0020)
 
 * New configuration options:
   - "DicomThreadsCount" to set the number of threads in the embedded DICOM server
 
+Lua
+---
+
+* New "ReceivedCStoreInstanceFilter" Lua callback to filter instances received
+  through C-Store and return a specific C-Store status code.
+
+
+Plugins
+-------
+
+* New function in the SDK: OrthancPluginRegisterIncomingCStoreInstanceFilter()
 
 Version 1.9.7 (2021-08-31)
 ==========================
--- a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Fri Oct 01 18:36:45 2021 +0200
@@ -39,9 +39,9 @@
     {
     }
 
-    virtual void Handle(DcmDataset& dicom,
-                        const std::string& remoteIp,
-                        const std::string& remoteAet,
-                        const std::string& calledAet) = 0;
+    virtual uint16_t Handle(DcmDataset& dicom,
+                            const std::string& remoteIp,
+                            const std::string& remoteAet,
+                            const std::string& calledAet) = 0;
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -161,14 +161,14 @@
             // which SOP class and SOP instance ?
 	    
 #if DCMTK_VERSION_NUMBER >= 364
-	    if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sizeof(sopClass),
-						     sopInstance, sizeof(sopInstance), /*opt_correctUIDPadding*/ OFFalse))
+	            if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sizeof(sopClass),
+						      sopInstance, sizeof(sopInstance), /*opt_correctUIDPadding*/ OFFalse))
 #else
               if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse))
 #endif
               {
-		//LOG4CPP_ERROR(Internals::GetLogger(), "bad DICOM file: " << fileName);
-		rsp->DimseStatus = STATUS_STORE_Error_CannotUnderstand;
+		            //LOG4CPP_ERROR(Internals::GetLogger(), "bad DICOM file: " << fileName);
+		            rsp->DimseStatus = STATUS_STORE_Error_CannotUnderstand;
               }
               else if (strcmp(sopClass, req->AffectedSOPClassUID) != 0)
               {
@@ -182,7 +182,7 @@
               {
                 try
                 {
-                  cbdata->handler->Handle(**imageDataSet, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET);
+                  rsp->DimseStatus = cbdata->handler->Handle(**imageDataSet, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET);
                 }
                 catch (OrthancException& e)
                 {
--- a/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -146,6 +146,20 @@
     }
   }
 
+  void LuaFunctionCall::ExecuteToInt(int& result)
+  {
+    ExecuteInternal(1);
+    
+    int top = lua_gettop(context_.lua_);
+    if (lua_isnumber(context_.lua_, top))
+    {
+      result = static_cast<int>(lua_tointeger(context_.lua_, top));
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_LuaReturnsNoString);
+    }
+  }
 
   void LuaFunctionCall::PushStringMap(const std::map<std::string, std::string>& value)
   {
--- a/OrthancFramework/Sources/Lua/LuaFunctionCall.h	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.h	Fri Oct 01 18:36:45 2021 +0200
@@ -78,6 +78,8 @@
 
     void ExecuteToString(std::string& result);
 
+    void ExecuteToInt(int& result);
+
 #if ORTHANC_ENABLE_DCMTK == 1
     void ExecuteToDicom(DicomMap& target);
 #endif
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -79,6 +79,7 @@
 #include <boost/regex.hpp>
 #include <dcmtk/dcmdata/dcdict.h>
 #include <dcmtk/dcmdata/dcdicent.h>
+#include <dcmtk/dcmnet/dimse.h>
 
 #define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc API is necessary"
 
@@ -1165,6 +1166,7 @@
     typedef std::list<OrthancPluginIncomingHttpRequestFilter>  IncomingHttpRequestFilters;
     typedef std::list<OrthancPluginIncomingHttpRequestFilter2>  IncomingHttpRequestFilters2;
     typedef std::list<OrthancPluginIncomingDicomInstanceFilter>  IncomingDicomInstanceFilters;
+    typedef std::list<OrthancPluginIncomingCStoreInstanceFilter>  IncomingCStoreInstanceFilters;
     typedef std::list<OrthancPluginDecodeImageCallback>  DecodeImageCallbacks;
     typedef std::list<OrthancPluginTranscoderCallback>  TranscoderCallbacks;
     typedef std::list<OrthancPluginJobsUnserializer>  JobsUnserializers;
@@ -1187,6 +1189,7 @@
     IncomingHttpRequestFilters  incomingHttpRequestFilters_;
     IncomingHttpRequestFilters2 incomingHttpRequestFilters2_;
     IncomingDicomInstanceFilters  incomingDicomInstanceFilters_;
+    IncomingCStoreInstanceFilters  incomingCStoreInstanceFilters_;  // New in Orthanc 1.9.8
     RefreshMetricsCallbacks refreshMetricsCallbacks_;
     StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_;
     std::unique_ptr<StorageAreaFactory>  storageArea_;
@@ -2261,7 +2264,36 @@
     return true;
   }
 
-  
+
+
+  uint16_t OrthancPlugins::FilterIncomingCStoreInstance(const DicomInstanceToStore& instance,
+                                                        const Json::Value& simplified)
+  {
+    DicomInstanceFromCallback wrapped(instance);
+    
+    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
+    
+    for (PImpl::IncomingCStoreInstanceFilters::const_iterator
+           filter = pimpl_->incomingCStoreInstanceFilters_.begin();
+         filter != pimpl_->incomingCStoreInstanceFilters_.end(); ++filter)
+    {
+      int32_t filterResult = (*filter) (reinterpret_cast<const OrthancPluginDicomInstance*>(&wrapped));
+
+      if (filterResult >= 0 && filterResult <= 0xFFFF)
+      {
+        return static_cast<uint16_t>(filterResult);
+      }
+      else
+      {
+        // The callback is only allowed to answer uint16_t
+        throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+
+    return STATUS_Success;
+  }
+
+
   void OrthancPlugins::SignalChangeInternal(OrthancPluginChangeType changeType,
                                             OrthancPluginResourceType resourceType,
                                             const char* resource)
@@ -2479,6 +2511,18 @@
   }
 
 
+  void OrthancPlugins::RegisterIncomingCStoreInstanceFilter(const void* parameters)
+  {
+    const _OrthancPluginIncomingCStoreInstanceFilter& p = 
+      *reinterpret_cast<const _OrthancPluginIncomingCStoreInstanceFilter*>(parameters);
+
+    CLOG(INFO, PLUGINS) << "Plugin has registered a callback to filter incoming C-Store DICOM instances";
+    pimpl_->incomingCStoreInstanceFilters_.push_back(p.callback);
+  }
+
+
+    void RegisterIncomingCStoreInstanceFilter(const void* parameters);
+
   void OrthancPlugins::RegisterRefreshMetricsCallback(const void* parameters)
   {
     const _OrthancPluginRegisterRefreshMetricsCallback& p = 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Fri Oct 01 18:36:45 2021 +0200
@@ -133,6 +133,8 @@
 
     void RegisterIncomingDicomInstanceFilter(const void* parameters);
 
+    void RegisterIncomingCStoreInstanceFilter(const void* parameters);
+
     void RegisterRefreshMetricsCallback(const void* parameters);
 
     void RegisterStorageCommitmentScpCallback(const void* parameters);
@@ -279,6 +281,9 @@
     virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
                                         const Json::Value& simplified) ORTHANC_OVERRIDE;
 
+    virtual uint16_t FilterIncomingCStoreInstance(const DicomInstanceToStore& instance,
+                                                  const Json::Value& simplified) ORTHANC_OVERRIDE;
+
     bool HasStorageArea() const;
 
     IStorageArea* CreateStorageArea();  // To be freed after use
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Oct 01 18:36:45 2021 +0200
@@ -461,7 +461,8 @@
     _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014,
     _OrthancPluginService_RegisterTranscoderCallback = 1015,   /* New in Orthanc 1.7.0 */
     _OrthancPluginService_RegisterStorageArea2 = 1016,         /* New in Orthanc 1.9.0 */
-    
+    _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017,  /* New in Orthanc 1.9.8 */
+
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
     _OrthancPluginService_CompressAndAnswerPngImage = 2001,  /* Unused as of Orthanc 0.9.4 */
@@ -7764,6 +7765,63 @@
 
 
   /**
+   * @brief Callback to filter incoming DICOM instances received by 
+   * Orthanc through C-Store.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a new DICOM instance (through DICOM protocol), 
+   * and that answers whether this DICOM instance should be accepted 
+   * or discarded by Orthanc.  If the instance is discarded, the callback
+   * can specify the C-Store error code.
+   *
+   * Note that the metadata information is not available
+   * (i.e. GetInstanceMetadata() should not be used on "instance").
+   *
+   * @param instance The received DICOM instance.
+   * @return 0 to accept the instance, any valid C-Store error code
+   * to reject the instance, -1 if error.
+   * @ingroup Callback
+   **/
+  typedef int32_t (*OrthancPluginIncomingCStoreInstanceFilter) (
+    const OrthancPluginDicomInstance* instance);
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingCStoreInstanceFilter callback;
+  } _OrthancPluginIncomingCStoreInstanceFilter;
+
+  /**
+   * @brief Register a callback to filter incoming DICOM instances
+   * received by Orthanc through C-Store.
+   *
+   *
+   * @warning Your callback function will be called synchronously with
+   * the core of Orthanc. This implies that deadlocks might emerge if
+   * you call other core primitives of Orthanc in your callback (such
+   * deadlocks are particular visible in the presence of other plugins
+   * or Lua scripts). It is thus strongly advised to avoid any call to
+   * the REST API of Orthanc in the callback. If you have to call
+   * other primitives of Orthanc, you should make these calls in a
+   * separate thread, passing the pending events to be processed
+   * through a message queue.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingCStoreInstanceFilter(
+    OrthancPluginContext*                     context,
+    OrthancPluginIncomingCStoreInstanceFilter  callback)
+  {
+    _OrthancPluginIncomingCStoreInstanceFilter params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingCStoreInstanceFilter, &params);
+  }
+
+  /**
    * @brief Get the transfer syntax of a DICOM file.
    *
    * This function returns a pointer to a newly created string that
--- a/OrthancServer/Sources/IServerListener.h	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/IServerListener.h	Fri Oct 01 18:36:45 2021 +0200
@@ -55,5 +55,9 @@
 
     virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
                                         const Json::Value& simplified) = 0;
+
+    virtual uint16_t FilterIncomingCStoreInstance(const DicomInstanceToStore& instance,
+                                                  const Json::Value& simplified) = 0;
+
   };
 }
--- a/OrthancServer/Sources/LuaScripting.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/LuaScripting.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -43,6 +43,8 @@
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h"
 
+#include <dcmtk/dcmnet/dimse.h>
+
 #include <OrthancServerResources.h>
 
 
@@ -945,6 +947,41 @@
     return true;
   }
 
+  uint16_t LuaScripting::FilterIncomingCStoreInstance(const DicomInstanceToStore& instance,
+                                                      const Json::Value& simplified)
+  {
+    static const char* NAME = "ReceivedCStoreInstanceFilter";
+
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    if (lua_.IsExistingFunction(NAME))
+    {
+      LuaFunctionCall call(lua_, NAME);
+      call.PushJson(simplified);
+
+      Json::Value origin;
+      instance.GetOrigin().Format(origin);
+      call.PushJson(origin);
+
+      Json::Value info = Json::objectValue;
+      info["HasPixelData"] = instance.HasPixelData();
+
+      DicomTransferSyntax s;
+      if (instance.LookupTransferSyntax(s))
+      {
+        info["TransferSyntaxUID"] = GetTransferSyntaxUid(s);
+      }
+
+      call.PushJson(info);
+
+      int result;
+      call.ExecuteToInt(result);
+      return static_cast<uint16_t>(result);
+    }
+
+    return STATUS_Success;
+  }
+
 
   void LuaScripting::Execute(const std::string& command)
   {
--- a/OrthancServer/Sources/LuaScripting.h	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/LuaScripting.h	Fri Oct 01 18:36:45 2021 +0200
@@ -129,6 +129,9 @@
     bool FilterIncomingInstance(const DicomInstanceToStore& instance,
                                 const Json::Value& simplifiedTags);
 
+    uint16_t FilterIncomingCStoreInstance(const DicomInstanceToStore& instance,
+                                          const Json::Value& simplified);
+
     void Execute(const std::string& command);
 
     void SignalJobSubmitted(const std::string& jobId);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -554,16 +554,16 @@
     toStore->SetOrigin(DicomInstanceOrigin::FromRest(call));
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-    StoreStatus status = context.Store(id, *toStore, StoreInstanceMode_Default);
+    ServerContext::StoreResult result = context.Store(id, *toStore, StoreInstanceMode_Default);
 
-    if (status == StoreStatus_Failure)
+    if (result.GetStatus() == StoreStatus_Failure)
     {
       throw OrthancException(ErrorCode_CannotStoreInstance);
     }
 
     if (sendAnswer)
     {
-      OrthancRestApi::GetApi(call).AnswerStoredInstance(call, *toStore, status, id);
+      OrthancRestApi::GetApi(call).AnswerStoredInstance(call, *toStore, result.GetStatus(), id);
     }
   }
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -199,10 +199,10 @@
 
           try
           {
-            StoreStatus status = context.Store(publicId, *toStore, StoreInstanceMode_Default);
+            ServerContext::StoreResult result = context.Store(publicId, *toStore, StoreInstanceMode_Default);
 
             Json::Value info;
-            SetupResourceAnswer(info, *toStore, status, publicId);
+            SetupResourceAnswer(info, *toStore, result.GetStatus(), publicId);
             answer.append(info);
           }
           catch (OrthancException& e)
@@ -252,9 +252,9 @@
       toStore->SetOrigin(DicomInstanceOrigin::FromRest(call));
 
       std::string publicId;
-      StoreStatus status = context.Store(publicId, *toStore, StoreInstanceMode_Default);
+      ServerContext::StoreResult result = context.Store(publicId, *toStore, StoreInstanceMode_Default);
 
-      OrthancRestApi::GetApi(call).AnswerStoredInstance(call, *toStore, status, publicId);
+      OrthancRestApi::GetApi(call).AnswerStoredInstance(call, *toStore, result.GetStatus(), publicId);
     }
   }
 
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -1313,9 +1313,9 @@
         try
         {
           std::string publicId;
-          StoreStatus status = context_.Store(publicId, *instance, StoreInstanceMode_Default);
-          if (status == StoreStatus_Success ||
-              status == StoreStatus_AlreadyStored)
+          ServerContext::StoreResult result = context_.Store(publicId, *instance, StoreInstanceMode_Default);
+          if (result.GetStatus() == StoreStatus_Success ||
+              result.GetStatus() == StoreStatus_AlreadyStored)
           {
             LOG(INFO) << "Successfully imported DICOM instance from WebDAV: "
                       << path << " (Orthanc ID: " << publicId << ")";
--- a/OrthancServer/Sources/ServerContext.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -57,6 +57,7 @@
 #include "StorageCommitmentReports.h"
 
 #include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmnet/dimse.h>
 
 
 static size_t DICOM_CACHE_SIZE = 128 * 1024 * 1024;  // 128 MB
@@ -100,6 +101,13 @@
       transferSyntax != DicomTransferSyntax_XML);
   }
 
+
+  ServerContext::StoreResult::StoreResult() :
+    status_(StoreStatus_Failure),
+    cstoreStatusCode_(0)
+  {
+  }
+
   
   void ServerContext::ChangeThread(ServerContext* that,
                                    unsigned int sleepDelay)
@@ -489,9 +497,9 @@
   }
 
 
-  StoreStatus ServerContext::StoreAfterTranscoding(std::string& resultPublicId,
-                                                   DicomInstanceToStore& dicom,
-                                                   StoreInstanceMode mode)
+  ServerContext::StoreResult ServerContext::StoreAfterTranscoding(std::string& resultPublicId,
+                                                                  DicomInstanceToStore& dicom,
+                                                                  StoreInstanceMode mode)
   {
     bool overwrite;
     switch (mode)
@@ -538,7 +546,7 @@
       Toolbox::SimplifyDicomAsJson(simplifiedTags, dicomAsJson, DicomToJsonFormat_Human);
 
       // Test if the instance must be filtered out
-      bool accepted = true;
+      StoreResult result;
 
       {
         boost::shared_lock<boost::shared_mutex> lock(listenersMutex_);
@@ -549,9 +557,22 @@
           {
             if (!it->GetListener().FilterIncomingInstance(dicom, simplifiedTags))
             {
-              accepted = false;
+              result.SetStatus(StoreStatus_FilteredOut);
+              result.SetCStoreStatusCode(STATUS_Success); // to keep backward compatibility, we still return 'success'
               break;
             }
+
+            if (dicom.GetOrigin().GetRequestOrigin() == Orthanc::RequestOrigin_DicomProtocol)
+            {
+              uint16_t filterResult = it->GetListener().FilterIncomingCStoreInstance(dicom, simplifiedTags);
+              if (it->GetListener().FilterIncomingCStoreInstance(dicom, simplifiedTags) != 0x0000)
+              {
+                result.SetStatus(StoreStatus_FilteredOut);
+                result.SetCStoreStatusCode(filterResult);
+                break;
+              }
+            }
+            
           }
           catch (OrthancException& e)
           {
@@ -563,10 +584,10 @@
         }
       }
 
-      if (!accepted)
+      if (result.GetStatus() == StoreStatus_FilteredOut)
       {
         LOG(INFO) << "An incoming instance has been discarded by the filter";
-        return StoreStatus_FilteredOut;
+        return result;
       }
 
       // Remove the file from the DicomCache (useful if
@@ -595,9 +616,9 @@
 
       typedef std::map<MetadataType, std::string>  InstanceMetadata;
       InstanceMetadata  instanceMetadata;
-      StoreStatus status = index_.Store(
+      result.SetStatus(index_.Store(
         instanceMetadata, summary, attachments, dicom.GetMetadata(), dicom.GetOrigin(), overwrite,
-        hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset);
+        hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset));
 
       // Only keep the metadata for the "instance" level
       dicom.ClearMetadata();
@@ -608,7 +629,7 @@
         dicom.AddMetadata(ResourceType_Instance, it->first, it->second);
       }
             
-      if (status != StoreStatus_Success)
+      if (result.GetStatus() != StoreStatus_Success)
       {
         accessor.Remove(dicomInfo);
 
@@ -618,7 +639,7 @@
         }
       }
 
-      switch (status)
+      switch (result.GetStatus())
       {
         case StoreStatus_Success:
           LOG(INFO) << "New instance stored";
@@ -637,8 +658,8 @@
           break;
       }
 
-      if (status == StoreStatus_Success ||
-          status == StoreStatus_AlreadyStored)
+      if (result.GetStatus() == StoreStatus_Success ||
+          result.GetStatus() == StoreStatus_AlreadyStored)
       {
         boost::shared_lock<boost::shared_mutex> lock(listenersMutex_);
 
@@ -657,7 +678,7 @@
         }
       }
 
-      return status;
+      return result;
     }
     catch (OrthancException& e)
     {
@@ -671,9 +692,9 @@
   }
 
 
-  StoreStatus ServerContext::Store(std::string& resultPublicId,
-                                   DicomInstanceToStore& dicom,
-                                   StoreInstanceMode mode)
+  ServerContext::StoreResult ServerContext::Store(std::string& resultPublicId,
+                                                  DicomInstanceToStore& dicom,
+                                                  StoreInstanceMode mode)
   {
     if (!isIngestTranscoding_)
     {
@@ -733,10 +754,10 @@
           std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*tmp));
           toStore->SetOrigin(dicom.GetOrigin());
 
-          StoreStatus ok = StoreAfterTranscoding(resultPublicId, *toStore, mode);
+          StoreResult result = StoreAfterTranscoding(resultPublicId, *toStore, mode);
           assert(resultPublicId == tmp->GetHasher().HashInstance());
 
-          return ok;
+          return result;
         }
         else
         {
--- a/OrthancServer/Sources/ServerContext.h	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Fri Oct 01 18:36:45 2021 +0200
@@ -93,6 +93,36 @@
                          const Json::Value* dicomAsJson) = 0;
     };
     
+    struct StoreResult
+    {
+    private:
+      StoreStatus  status_;
+      uint16_t     cstoreStatusCode_;
+      // uint16_t     httpStatusCode_; // for future use
+
+    public:
+      StoreResult();
+
+      void SetStatus(StoreStatus status)
+      {
+        status_ = status;
+      }
+
+      StoreStatus GetStatus()
+      {
+        return status_;
+      }
+
+      void SetCStoreStatusCode(uint16_t statusCode)
+      {
+        cstoreStatusCode_ = statusCode;
+      }
+
+      uint16_t GetCStoreStatusCode()
+      {
+        return cstoreStatusCode_;
+      }
+    };
     
   private:
     class LuaServerListener : public IServerListener
@@ -123,6 +153,12 @@
       {
         return context_.filterLua_.FilterIncomingInstance(instance, simplified);
       }
+
+      virtual uint16_t FilterIncomingCStoreInstance(const DicomInstanceToStore& instance,
+                                                    const Json::Value& simplified) ORTHANC_OVERRIDE
+      {
+        return context_.filterLua_.FilterIncomingCStoreInstance(instance, simplified);
+      }
     };
     
     class ServerListener
@@ -231,7 +267,7 @@
     bool isUnknownSopClassAccepted_;
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
 
-    StoreStatus StoreAfterTranscoding(std::string& resultPublicId,
+    StoreResult StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
                                       StoreInstanceMode mode);
 
@@ -304,7 +340,7 @@
                        int64_t oldRevision,
                        const std::string& oldMD5);
 
-    StoreStatus Store(std::string& resultPublicId,
+    StoreResult Store(std::string& resultPublicId,
                       DicomInstanceToStore& dicom,
                       StoreInstanceMode mode);
 
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -161,8 +161,8 @@
     toStore->SetOrigin(origin_);
 
     std::string modifiedInstance;
-    if (GetContext().Store(modifiedInstance, *toStore,
-                       StoreInstanceMode_Default) != StoreStatus_Success)
+    ServerContext::StoreResult result = GetContext().Store(modifiedInstance, *toStore, StoreInstanceMode_Default);
+    if (result.GetStatus() != StoreStatus_Success)
     {
       LOG(ERROR) << "Error while storing a modified instance " << instance;
       return false;
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -290,8 +290,8 @@
      **/
 
     std::string modifiedInstance;
-    if (GetContext().Store(modifiedInstance, *toStore,
-                           StoreInstanceMode_Default) != StoreStatus_Success)
+    ServerContext::StoreResult result = GetContext().Store(modifiedInstance, *toStore, StoreInstanceMode_Default);
+    if (result.GetStatus() != StoreStatus_Success)
     {
       throw OrthancException(ErrorCode_CannotStoreInstance,
                              "Error while storing a modified instance " + instance);
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -143,8 +143,8 @@
     toStore->SetOrigin(origin_);
 
     std::string modifiedInstance;
-    if (GetContext().Store(modifiedInstance, *toStore,
-                           StoreInstanceMode_Default) != StoreStatus_Success)
+    ServerContext::StoreResult result = GetContext().Store(modifiedInstance, *toStore, StoreInstanceMode_Default);
+    if (result.GetStatus() != StoreStatus_Success)
     {
       LOG(ERROR) << "Error while storing a modified instance " << instance;
       return false;
--- a/OrthancServer/Sources/main.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/Sources/main.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -84,10 +84,10 @@
   }
 
 
-  virtual void Handle(DcmDataset& dicom,
-                      const std::string& remoteIp,
-                      const std::string& remoteAet,
-                      const std::string& calledAet) ORTHANC_OVERRIDE 
+  virtual uint16_t Handle(DcmDataset& dicom,
+                          const std::string& remoteIp,
+                          const std::string& remoteAet,
+                          const std::string& calledAet) ORTHANC_OVERRIDE 
   {
     std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromDcmDataset(dicom));
     
@@ -97,8 +97,11 @@
                          (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
 
       std::string id;
-      context_.Store(id, *toStore, StoreInstanceMode_Default);
+      ServerContext::StoreResult result = context_.Store(id, *toStore, StoreInstanceMode_Default);
+      return result.GetCStoreStatusCode();
     }
+
+    return STATUS_STORE_Error_CannotUnderstand;
   }
 };
 
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -859,7 +859,8 @@
       ASSERT_EQ(id, hasher.HashInstance());
 
       std::string id2;
-      ASSERT_EQ(StoreStatus_Success, context.Store(id2, *toStore, StoreInstanceMode_Default));
+      ServerContext::StoreResult result = context.Store(id2, *toStore, StoreInstanceMode_Default);
+      ASSERT_EQ(StoreStatus_Success, result.GetStatus());
       ASSERT_EQ(id, id2);
     }
 
@@ -908,8 +909,8 @@
       toStore->SetOrigin(DicomInstanceOrigin::FromPlugins());
 
       std::string id2;
-      ASSERT_EQ(overwrite ? StoreStatus_Success : StoreStatus_AlreadyStored,
-                context.Store(id2, *toStore, StoreInstanceMode_Default));
+      ServerContext::StoreResult result = context.Store(id2, *toStore, StoreInstanceMode_Default);
+      ASSERT_EQ(overwrite ? StoreStatus_Success : StoreStatus_AlreadyStored, result.GetStatus());
       ASSERT_EQ(id, id2);
     }
 
@@ -1008,7 +1009,8 @@
         std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
         dicomSize = toStore->GetBufferSize();
         toStore->SetOrigin(DicomInstanceOrigin::FromPlugins());
-        ASSERT_EQ(StoreStatus_Success, context.Store(id, *toStore, StoreInstanceMode_Default));
+        ServerContext::StoreResult result = context.Store(id, *toStore, StoreInstanceMode_Default);
+        ASSERT_EQ(StoreStatus_Success, result.GetStatus());
       }
 
       std::set<FileContentType> attachments;
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Thu Sep 30 17:52:07 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Fri Oct 01 18:36:45 2021 +0200
@@ -537,7 +537,8 @@
 
       std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
 
-      return (context_->Store(id, *toStore, StoreInstanceMode_Default) == StoreStatus_Success);
+      ServerContext::StoreResult result = context_->Store(id, *toStore, StoreInstanceMode_Default);
+      return (result.GetStatus() == StoreStatus_Success);
     }
   };
 }