changeset 4810:7afbb54bd028

merge storage-cache
author Alain Mazy <am@osimis.io>
date Tue, 23 Nov 2021 09:22:11 +0100
parents 2ca4213fb50a (diff) 96ab170294fd (current diff)
children 064d86287630 cd6dc99e0470
files NEWS OrthancServer/Resources/Configuration.json OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerJobs/ArchiveJob.cpp OrthancServer/Sources/ServerJobs/ArchiveJob.h OrthancServer/Sources/main.cpp
diffstat 47 files changed, 411 insertions(+), 78 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Oct 07 13:45:36 2021 +0200
+++ b/NEWS	Tue Nov 23 09:22:11 2021 +0100
@@ -17,6 +17,30 @@
 -----------
 
 * 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
+* Fix instances accumulating in DB while their attachments were not stored because of 
+  MaximumStorageSize limit reached with a single patient in DB.
+
+REST API
+--------
+
+* API version upgraded to 16
+* If an image can not be decoded, ../preview and ../rendered routes are now returning 
+  unsupported.png only if the ?returnUnsupportedImage option is specified; otherwise, 
+  it raises a 415 error code.
+* Archive jobs response now contains a header Content-Disposition:filename='archive.zip'
+  
+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/Resources/CMake/OrthancFrameworkParameters.cmake	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Tue Nov 23 09:22:11 2021 +0100
@@ -37,7 +37,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "15")
+set(ORTHANC_API_VERSION "16")
 
 
 #####################################################################
--- a/OrthancFramework/Sources/Compression/ZipReader.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/Compression/ZipReader.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -353,7 +353,7 @@
         }
         else
         {
-          throw OrthancException(ErrorCode_BadFileFormat);
+          throw OrthancException(ErrorCode_BadFileFormat, "Invalid file or unsupported compression method (e.g. Deflate64)");
         }
       }
       
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -93,6 +93,7 @@
     port_(104),
     continue_(false),
     associationTimeout_(30),
+    threadsCount_(4),
     modalities_(NULL),
     findRequestHandlerFactory_(NULL),
     moveRequestHandlerFactory_(NULL),
@@ -424,7 +425,10 @@
 #endif
 
     continue_ = true;
-    pimpl_->workers_.reset(new RunnableWorkersPool(4));   // Use 4 workers - TODO as a parameter?
+
+    CLOG(INFO, DICOM) << "The embedded DICOM server will use " << threadsCount_ << " threads";
+
+    pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_));
     pimpl_->thread_ = boost::thread(ServerThread, this, maximumPduLength_, useDicomTls_);
   }
 
@@ -588,4 +592,16 @@
   {
     return remoteCertificateRequired_;
   }
+
+  void DicomServer::SetThreadsCount(unsigned int threads)
+  {
+    if (threads == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    Stop();
+    threadsCount_ = threads;
+  }
+
 }
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Tue Nov 23 09:22:11 2021 +0100
@@ -72,6 +72,7 @@
     uint16_t port_;
     bool continue_;
     uint32_t associationTimeout_;
+    unsigned int threadsCount_;
     IRemoteModalities* modalities_;
     IFindRequestHandlerFactory* findRequestHandlerFactory_;
     IMoveRequestHandlerFactory* moveRequestHandlerFactory_;
@@ -89,6 +90,7 @@
     unsigned int maximumPduLength_;
     bool         remoteCertificateRequired_;  // New in 1.9.3
 
+
     static void ServerThread(DicomServer* server,
                              unsigned int maximumPduLength,
                              bool useDicomTls);
@@ -163,5 +165,8 @@
 
     void SetRemoteCertificateRequired(bool required);
     bool IsRemoteCertificateRequired() const;
+
+    void SetThreadsCount(unsigned int threadsCount);
+
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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/JobsEngine/IJob.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/IJob.h	Tue Nov 23 09:22:11 2021 +0100
@@ -59,6 +59,7 @@
     // "success" state
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
+                           std::string& filename,
                            const std::string& key) = 0;
   };
 }
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -650,6 +650,7 @@
 
   bool JobsRegistry::GetJobOutput(std::string& output,
                                   MimeType& mime,
+                                  std::string& filename,
                                   const std::string& job,
                                   const std::string& key)
   {
@@ -668,7 +669,7 @@
 
       if (handler.GetState() == JobState_Success)
       {
-        return handler.GetJob().GetOutput(output, mime, key);
+        return handler.GetJob().GetOutput(output, mime, filename, key);
       }
       else
       {
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Tue Nov 23 09:22:11 2021 +0100
@@ -148,6 +148,7 @@
 
     bool GetJobOutput(std::string& output,
                       MimeType& mime,
+                      std::string& filename,
                       const std::string& job,
                       const std::string& key);
 
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -448,6 +448,7 @@
 
   bool SequenceOfOperationsJob::GetOutput(std::string& output,
                                           MimeType& mime,
+                                          std::string& filename,
                                           const std::string& key)
   {
     return false;
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Tue Nov 23 09:22:11 2021 +0100
@@ -125,6 +125,7 @@
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
+                           std::string& filename,
                            const std::string& key) ORTHANC_OVERRIDE;
 
     void AwakeTrailingSleep();
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -270,6 +270,7 @@
 
   bool SetOfCommandsJob::GetOutput(std::string &output,
                                    MimeType &mime,
+                                   std::string& filename,
                                    const std::string &key)
   {
     return false;
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Tue Nov 23 09:22:11 2021 +0100
@@ -104,6 +104,7 @@
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
+                           std::string& filename,
                            const std::string& key) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.h	Tue Nov 23 09:22:11 2021 +0100
@@ -78,6 +78,8 @@
 
     void ExecuteToString(std::string& result);
 
+    void ExecuteToInt(int& result);
+
 #if ORTHANC_ENABLE_DCMTK == 1
     void ExecuteToDicom(DicomMap& target);
 #endif
--- a/OrthancFramework/Sources/RestApi/RestApiOutput.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -214,4 +214,9 @@
     // empty string
     SetCookie(name, "", 1);
   }
+
+  void RestApiOutput::SetContentFilename(const char* filename)
+  {
+    output_.SetContentFilename(filename);
+  }
 }
--- a/OrthancFramework/Sources/RestApi/RestApiOutput.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.h	Tue Nov 23 09:22:11 2021 +0100
@@ -77,6 +77,8 @@
                       size_t length,
                       MimeType contentType);
 
+    void SetContentFilename(const char* filename);
+
     void SignalError(HttpStatus status);
 
     void SignalError(HttpStatus status,
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -125,6 +125,7 @@
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
+                           std::string& filename,
                            const std::string& key) ORTHANC_OVERRIDE
     {
       return false;
--- a/OrthancServer/OrthancExplorer/explorer.js	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.js	Tue Nov 23 09:22:11 2021 +0100
@@ -1113,7 +1113,7 @@
           if (frames.length == 1)
           {
             // Viewing a single-frame image
-            jQuery.slimbox('../instances/' + pageData.uuid + '/preview', '', {
+            jQuery.slimbox('../instances/' + pageData.uuid + '/preview?returnUnsupportedImage', '', {
               overlayFadeDuration : 1,
               resizeDuration : 1,
               imageFadeDuration : 1
@@ -1125,7 +1125,7 @@
 
             images = [];
             for (var i = 0; i < frames.length; i++) {
-              images.push([ '../instances/' + pageData.uuid + '/frames/' + i + '/preview' ]);
+              images.push([ '../instances/' + pageData.uuid + '/frames/' + i + '/preview?returnUnsupportedImage' ]);
             }
 
             jQuery.slimbox(images, 0, {
@@ -1155,7 +1155,7 @@
 
         images = [];
         for (var i = 0; i < instances.length; i++) {
-          images.push([ '../instances/' + instances[i].ID + '/preview',
+          images.push([ '../instances/' + instances[i].ID + '/preview?returnUnsupportedImage',
                         (i + 1).toString() + '/' + instances.length.toString() ])
         }
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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,16 @@
   }
 
 
+  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 OrthancPlugins::RegisterRefreshMetricsCallback(const void* parameters)
   {
     const _OrthancPluginRegisterRefreshMetricsCallback& p = 
@@ -4957,6 +4999,10 @@
         RegisterIncomingDicomInstanceFilter(parameters);
         return true;
 
+      case _OrthancPluginService_RegisterIncomingCStoreInstanceFilter:
+        RegisterIncomingCStoreInstanceFilter(parameters);
+        return true;
+
       case _OrthancPluginService_RegisterRefreshMetricsCallback:
         RegisterRefreshMetricsCallback(parameters);
         return true;
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Tue Nov 23 09:22:11 2021 +0100
@@ -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/Engine/PluginsJob.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsJob.h	Tue Nov 23 09:22:11 2021 +0100
@@ -75,6 +75,7 @@
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
+                           std::string& filename,
                            const std::string& key) ORTHANC_OVERRIDE
     {
       // TODO
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Nov 23 09:22:11 2021 +0100
@@ -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/Resources/Configuration.json	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Resources/Configuration.json	Tue Nov 23 09:22:11 2021 +0100
@@ -424,7 +424,16 @@
   // (1.2.840.10008.1.2.1). This parameter can possibly correspond to
   // a compressed transfer syntax. (new in Orthanc 1.9.0)
   "DicomScuPreferredTransferSyntax" : "1.2.840.10008.1.2.1",
-  
+
+  // Number of threads that are used by the embedded DICOM server.
+  // This defines the number of concurrent DICOM operations that can
+  // be run.  Note: this is not limiting the number of concurrent 
+  // connections.  With a single thread, if a C-Find is received
+  // during e.g the transcoding of an incoming C-Store, it will
+  // have to wait until the end of the C-Store before being processed.
+  // (new in Orthanc 1.9.8, before this version, the value was fixed to 4)
+  "DicomThreadsCount" : 4,
+
   // The list of the known Orthanc peers. This option is ignored if
   // "OrthancPeersInDatabase" is set to "true", in which case you must
   // use the REST API to define Orthanc peers.
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -2810,7 +2810,7 @@
         
         if (!ok)
         {
-          throw OrthancException(ErrorCode_FullStorage);
+          throw OrthancException(ErrorCode_FullStorage, "Cannot recycle more patients");
         }
       
         LOG(TRACE) << "Recycling one patient";
@@ -3264,11 +3264,18 @@
         {
           if (e.GetErrorCode() == ErrorCode_DatabaseCannotSerialize)
           {
-            throw;
+            throw;  // the transaction has failed -> do not commit the current transaction (and retry)
           }
           else
           {
-            LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
+            LOG(ERROR) << "EXCEPTION [" << e.What() << " - " << e.GetDetails() << "]";
+
+            if (e.GetErrorCode() == ErrorCode_FullStorage)
+            {
+              throw; // do not commit the current transaction
+            }
+
+            // this is an expected failure, exit normaly and commit the current transaction
             storeStatus_ = StoreStatus_Failure;
           }
         }
--- a/OrthancServer/Sources/IServerListener.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/IServerListener.h	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/LuaScripting.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/LuaScripting.h	Tue Nov 23 09:22:11 2021 +0100
@@ -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/OrthancGetRequestHandler.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -497,7 +497,7 @@
   {
     MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_get_scp_duration_ms");
 
-    CLOG(WARNING, DICOM) << "C-GET-SCU request received from AET \"" << originatorAet << "\"";
+    CLOG(INFO, DICOM) << "C-GET-SCU request received from AET \"" << originatorAet << "\"";
 
     {
       DicomArray query(input);
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -334,7 +334,7 @@
   {
     MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_move_scp_duration_ms");
 
-    CLOG(WARNING, DICOM) << "Move-SCU request received for AET \"" << targetAet << "\"";
+    CLOG(INFO, DICOM) << "Move-SCU request received for AET \"" << targetAet << "\"";
 
     {
       DicomArray query(input);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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/OrthancRestApi/OrthancRestResources.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -755,6 +755,7 @@
             .SetTag("Instances")
             .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest")
             .SetHttpGetArgument("quality", RestApiCallDocumentation::Type_Number, "Quality for JPEG images (between 1 and 100, defaults to 90)", false)
+            .SetHttpGetArgument("returnUnsupportedImage", RestApiCallDocumentation::Type_Boolean, "Returns an unsupported.png placeholder image if unable to provide the image instead of returning a 415 HTTP error (defaults to false)", false)
             .SetHttpHeader("Accept", "Format of the resulting image. Can be `image/png` (default), `image/jpeg` or `image/x-portable-arbitrarymap`")
             .AddAnswerType(MimeType_Png, "PNG image")
             .AddAnswerType(MimeType_Jpeg, "JPEG image")
@@ -817,13 +818,20 @@
           }
           else
           {
-            std::string root = "";
-            for (size_t i = 1; i < call.GetFullUri().size(); i++)
+            if (call.HasArgument("returnUnsupportedImage"))
             {
-              root += "../";
+              std::string root = "";
+              for (size_t i = 1; i < call.GetFullUri().size(); i++)
+              {
+                root += "../";
+              }
+
+              call.GetOutput().Redirect(root + "app/images/unsupported.png");
             }
-
-            call.GetOutput().Redirect(root + "app/images/unsupported.png");
+            else
+            {
+              call.GetOutput().SignalError(HttpStatus_415_UnsupportedMediaType);
+            }
           }
           return;
         }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -696,10 +696,16 @@
 
     std::string value;
     MimeType mime;
+    std::string filename;
     
     if (OrthancRestApi::GetContext(call).GetJobsEngine().
-        GetRegistry().GetJobOutput(value, mime, job, key))
+        GetRegistry().GetJobOutput(value, mime, filename, job, key))
     {
+      if (!filename.empty())
+      {
+        call.GetOutput().SetContentFilename(filename.c_str());
+      }
+
       call.GetOutput().AnswerBuffer(value, mime);
     }
     else
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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 (filterResult != 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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Tue Nov 23 09:22:11 2021 +0100
@@ -94,6 +94,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
@@ -124,6 +154,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
@@ -233,7 +269,7 @@
     bool isUnknownSopClassAccepted_;
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
 
-    StoreStatus StoreAfterTranscoding(std::string& resultPublicId,
+    StoreResult StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
                                       StoreInstanceMode mode);
 
@@ -311,7 +347,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/ArchiveJob.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -1412,6 +1412,7 @@
 
   bool ArchiveJob::GetOutput(std::string& output,
                              MimeType& mime,
+                             std::string& filename,
                              const std::string& key)
   {   
     if (key == "archive" &&
@@ -1424,6 +1425,7 @@
         const DynamicTemporaryFile& f = dynamic_cast<DynamicTemporaryFile&>(accessor.GetItem());
         f.GetFile().Read(output);
         mime = MimeType_Zip;
+        filename = "archive.zip";
         return true;
       }
       else
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Tue Nov 23 09:22:11 2021 +0100
@@ -127,6 +127,7 @@
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
+                           std::string& filename,
                            const std::string& key) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/Sources/main.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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;
   }
 };
 
@@ -1210,6 +1213,7 @@
       dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet", false));
       dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout", 30));
       dicomServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242));
+      dicomServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomThreadsCount", 4));
       dicomServer.SetApplicationEntityTitle(lock.GetConfiguration().GetOrthancAET());
 
       // Configuration of DICOM TLS for Orthanc SCP (since Orthanc 1.9.0)
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Thu Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -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 Oct 07 13:45:36 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Tue Nov 23 09:22:11 2021 +0100
@@ -140,6 +140,7 @@
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
+                           std::string& filename,
                            const std::string& key) ORTHANC_OVERRIDE
     {
       return false;
@@ -537,7 +538,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);
     }
   };
 }
--- a/TODO	Thu Oct 07 13:45:36 2021 +0200
+++ b/TODO	Tue Nov 23 09:22:11 2021 +0100
@@ -60,6 +60,8 @@
   image. The SOPClassUID might be used to identify such secondary
   captures.
 * Support "/preview" and "/matlab" for LUT color images
+* Try to transcode files if a simple decoding fails:
+  https://groups.google.com/g/orthanc-users/c/b8168-NkAhA/m/Df3j-CO9CgAJ
 * Add asynchronous mode in "/modalitities/.../move" for C-MOVE SCU:
   https://groups.google.com/g/orthanc-users/c/G3_jBy4X4NQ/m/8BanTsdMBQAJ
 * Ranges of DICOM tags for "Keep" and "Remove" in ".../modify" and ".../anonymize": 
@@ -125,7 +127,10 @@
   useful in ServerContext::DecodeDicomInstance()
 * DicomMap: create a cache to the main DICOM tags index
 * Check out rapidjson: https://github.com/miloyip/nativejson-benchmark
-
+* optimize tools/find with ModalitiesInStudies: 
+  https://groups.google.com/g/orthanc-users/c/aN8nqcRd3jw/m/pmc9ylVeAwAJ.
+  One solution could be: filter first without ModalitiesInStudies and then
+  cycle through the responses to filter out with ModalitiesInStudies
 
 ========
 Database