changeset 4819:70d2a97ca8cb openssl-3.x

integration mainline->openssl-3.x
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 25 Nov 2021 13:12:32 +0100
parents 61da49321754 (current diff) c0986ae1b9fc (diff)
children 089b6c841da1
files NEWS OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancServer/OrthancExplorer/explorer.js OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Plugins/Engine/PluginsJob.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/IServerListener.h OrthancServer/Sources/LuaScripting.cpp OrthancServer/Sources/LuaScripting.h OrthancServer/Sources/OrthancGetRequestHandler.cpp OrthancServer/Sources/OrthancMoveRequestHandler.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/Search/ISqlLookupFormatter.cpp OrthancServer/Sources/Search/ISqlLookupFormatter.h OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerJobs/ArchiveJob.cpp OrthancServer/Sources/ServerJobs/ArchiveJob.h OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp OrthancServer/Sources/ServerToolbox.cpp OrthancServer/Sources/main.cpp OrthancServer/UnitTestsSources/PluginsTests.cpp OrthancServer/UnitTestsSources/ServerIndexTests.cpp OrthancServer/UnitTestsSources/ServerJobsTests.cpp TODO
diffstat 72 files changed, 1172 insertions(+), 148 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Aug 30 22:21:24 2021 +0200
+++ b/NEWS	Thu Nov 25 13:12:32 2021 +0100
@@ -15,6 +15,47 @@
 * Upgraded dependencies for static builds (notably on Windows and LSB):
   - openssl 3.0.0-beta1
 
+General
+-------
+
+* Added a storage cache in RAM to avoid reading the same files multiple times from 
+  the storage.  This greatly improves, among other things, the performance of WADO-RS
+  retrieval of individual frames of multiframe instances.
+* New configuration option "MaximumStorageCacheSize" to configure the size of
+  the new storage cache.
+* New configuration option "ZipLoaderThreads" to configure the number of threads used
+  to read instances from storage when createing a Zip archive/media.
+
+
+Maintenance
+-----------
+
+* 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/OrthancFrameworkConfiguration.cmake	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Nov 25 13:12:32 2021 +0100
@@ -385,6 +385,7 @@
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/HierarchicalZipWriter.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Compression/ZipWriter.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageAccessor.cpp
+      ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/StorageCache.cpp
       )
   endif()
 endif()
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Thu Nov 25 13:12:32 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/Cache/MemoryObjectCache.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Thu Nov 25 13:12:32 2021 +0100
@@ -37,6 +37,9 @@
 
 namespace Orthanc
 {
+  /**
+   *  Note: this class is thread safe
+   **/
   class ORTHANC_PUBLIC MemoryObjectCache : public boost::noncopyable
   {
   private:
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -35,7 +35,12 @@
       content_(content)
     {
     }
-      
+
+    explicit StringValue(const char* buffer, size_t size) :
+      content_(buffer, size)
+    {
+    }
+
     const std::string& GetContent() const
     {
       return content_;
@@ -63,6 +68,13 @@
     cache_.Acquire(key, new StringValue(value));
   }
 
+  void MemoryStringCache::Add(const std::string& key,
+                              const void* buffer,
+                              size_t size)
+  {
+    cache_.Acquire(key, new StringValue(reinterpret_cast<const char*>(buffer), size));
+  }
+
   void MemoryStringCache::Invalidate(const std::string &key)
   {
     cache_.Invalidate(key);
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h	Thu Nov 25 13:12:32 2021 +0100
@@ -29,6 +29,8 @@
   /**
    * Facade object around "MemoryObjectCache" that caches a dictionary
    * of strings, using the "fetch/add" paradigm of memcached.
+   * 
+   * Note: this class is thread safe
    **/
   class ORTHANC_PUBLIC MemoryStringCache : public boost::noncopyable
   {
@@ -44,7 +46,11 @@
 
     void Add(const std::string& key,
              const std::string& value);
-    
+
+    void Add(const std::string& key,
+             const void* buffer,
+             size_t size);
+
     void Invalidate(const std::string& key);
 
     bool Fetch(std::string& value,
--- a/OrthancFramework/Sources/Compression/ZipReader.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/Compression/ZipReader.cpp	Thu Nov 25 13:12:32 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/DicomFormat/DicomMap.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -1406,11 +1406,11 @@
                 break;
               
               case Json::intValue:
-                s += boost::lexical_cast<std::string>(value[j].asInt());
+                s += boost::lexical_cast<std::string>(value[j].asInt64());
                 break;
               
               case Json::uintValue:
-                s += boost::lexical_cast<std::string>(value[j].asUInt());
+                s += boost::lexical_cast<std::string>(value[j].asUInt64());
                 break;
               
               case Json::realValue:
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Thu Nov 25 13:12:32 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/DicomParsing/DicomModification.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -1808,4 +1808,25 @@
       }
     }
   }
+
+
+  bool DicomModification::IsAlteredTag(const DicomTag& tag) const
+  {
+    return (uids_.find(tag) != uids_.end() ||
+            IsCleared(tag) ||
+            IsRemoved(tag) ||
+            IsReplaced(tag) ||
+            (tag.IsPrivate() &&
+             ArePrivateTagsRemoved() &&
+             privateTagsToKeep_.find(tag) == privateTagsToKeep_.end()) ||
+            (isAnonymization_ && (
+              tag == DICOM_TAG_PATIENT_NAME ||
+              tag == DICOM_TAG_PATIENT_ID)) ||
+            (tag == DICOM_TAG_STUDY_INSTANCE_UID &&
+             !keepStudyInstanceUid_) ||
+            (tag == DICOM_TAG_SERIES_INSTANCE_UID &&
+             !keepSeriesInstanceUid_) ||
+            (tag == DICOM_TAG_SOP_INSTANCE_UID &&
+             !keepSopInstanceUid_));
+  }
 }
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h	Thu Nov 25 13:12:32 2021 +0100
@@ -252,5 +252,7 @@
     void Replace(const DicomPath& path,
                  const Json::Value& value,   // Encoded using UTF-8
                  bool safeForAnonymization);
+
+    bool IsAlteredTag(const DicomTag& tag) const;
   };
 }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -22,7 +22,10 @@
 
 #include "../PrecompiledHeaders.h"
 #include "StorageAccessor.h"
+#include "StorageCache.h"
 
+#include "../Logging.h"
+#include "../StringMemoryBuffer.h"
 #include "../Compatibility.h"
 #include "../Compression/ZlibCompressor.h"
 #include "../MetricsRegistry.h"
@@ -58,14 +61,18 @@
   };
 
 
-  StorageAccessor::StorageAccessor(IStorageArea &area) :
+  StorageAccessor::StorageAccessor(IStorageArea &area, StorageCache& cache) :
     area_(area),
+    cache_(cache),
     metrics_(NULL)
   {
   }
 
-  StorageAccessor::StorageAccessor(IStorageArea &area, MetricsRegistry &metrics) :
+  StorageAccessor::StorageAccessor(IStorageArea &area, 
+                                   StorageCache& cache,
+                                   MetricsRegistry &metrics) :
     area_(area),
+    cache_(cache),
     metrics_(&metrics)
   {
   }
@@ -93,6 +100,8 @@
         MetricsTimer timer(*this, METRICS_CREATE);
 
         area_.Create(uuid, data, size, type);
+        cache_.Add(uuid, type, data, size);
+
         return FileInfo(uuid, type, size, md5);
       }
 
@@ -123,6 +132,7 @@
           }
         }
 
+        cache_.Add(uuid, type, data, size);
         return FileInfo(uuid, type, size, md5,
                         CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
       }
@@ -145,6 +155,13 @@
   void StorageAccessor::Read(std::string& content,
                              const FileInfo& info)
   {
+    if (cache_.Fetch(content, info.GetUuid(), info.GetContentType()))
+    {
+      LOG(INFO) << "Read attachment \"" << info.GetUuid() << "\" "
+                << "content type from cache";
+      return;
+    }
+
     switch (info.GetCompressionType())
     {
       case CompressionType_None:
@@ -152,7 +169,9 @@
         MetricsTimer timer(*this, METRICS_READ);
 
         std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType()));
-        buffer->MoveToString(content);        
+        buffer->MoveToString(content);
+
+        cache_.Add(info.GetUuid(), info.GetContentType(), content);
         break;
       }
 
@@ -168,6 +187,8 @@
         }
 
         zlib.Uncompress(content, compressed->GetData(), compressed->GetSize());
+
+        cache_.Add(info.GetUuid(), info.GetContentType(), content);
         break;
       }
 
@@ -196,6 +217,14 @@
   {
     MetricsTimer timer(*this, METRICS_REMOVE);
     area_.Remove(fileUuid, type);
+
+    cache_.Invalidate(fileUuid, type);
+    
+    // in ReadStartRange, we might have cached only the start of the file -> try to remove it
+    if (type == FileContentType_Dicom)
+    {
+      cache_.Invalidate(fileUuid, FileContentType_DicomUntilPixelData);
+    }
   }
 
   void StorageAccessor::Remove(const FileInfo &info)
@@ -203,15 +232,56 @@
     Remove(info.GetUuid(), info.GetContentType());
   }
 
+  IMemoryBuffer* StorageAccessor::ReadStartRange(const std::string& fileUuid,
+                                                 FileContentType contentType,
+                                                 uint64_t end /* exclusive */,
+                                                 FileContentType startFileContentType)
+  {
+    std::string content;
+    if (cache_.Fetch(content, fileUuid, contentType))
+    {
+      LOG(INFO) << "Read attachment \"" << fileUuid << "\" "
+                << "(range from " << 0 << " to " << end << ") from cache";
+
+      return StringMemoryBuffer::CreateFromCopy(content, 0, end);
+    }
+
+    if (cache_.Fetch(content, fileUuid, startFileContentType))
+    {
+      LOG(INFO) << "Read attachment \"" << fileUuid << "\" "
+                << "(range from " << 0 << " to " << end << ") from cache";
+
+      assert(content.size() == end);
+      return StringMemoryBuffer::CreateFromCopy(content);
+    }
+
+    std::unique_ptr<IMemoryBuffer> buffer(area_.ReadRange(fileUuid, contentType, 0, end));
+
+    // we've read only the first part of the file -> add an entry in the cache
+    // note the uuid is still the uuid of the full file but the type is the type of the start of the file !
+    assert(buffer->GetSize() == end);
+    cache_.Add(fileUuid, startFileContentType, buffer->GetData(), buffer->GetSize());
+    return buffer.release();
+  }
+
+
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
   void StorageAccessor::SetupSender(BufferHttpSender& sender,
                                     const FileInfo& info,
                                     const std::string& mime)
   {
+    if (cache_.Fetch(sender.GetBuffer(), info.GetUuid(), info.GetContentType()))
+    {
+      LOG(INFO) << "Read attachment \"" << info.GetUuid() << "\" "
+                << "content type from cache";
+    }
+    else
     {
       MetricsTimer timer(*this, METRICS_READ);
       std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType()));
       buffer->MoveToString(sender.GetBuffer());
+
+      cache_.Add(info.GetUuid(), info.GetContentType(), sender.GetBuffer());
     }
 
     sender.SetContentType(mime);
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Thu Nov 25 13:12:32 2021 +0100
@@ -54,6 +54,7 @@
 namespace Orthanc
 {
   class MetricsRegistry;
+  class StorageCache;
 
   /**
    * This class handles the compression/decompression of the raw files
@@ -66,6 +67,7 @@
     class MetricsTimer;
 
     IStorageArea&     area_;
+    StorageCache&     cache_;
     MetricsRegistry*  metrics_;
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
@@ -75,9 +77,11 @@
 #endif
 
   public:
-    explicit StorageAccessor(IStorageArea& area);
+    explicit StorageAccessor(IStorageArea& area,
+                             StorageCache& cache);
 
     StorageAccessor(IStorageArea& area,
+                    StorageCache& cache,
                     MetricsRegistry& metrics);
 
     FileInfo Write(const void* data,
@@ -97,6 +101,11 @@
     void ReadRaw(std::string& content,
                  const FileInfo& info);
 
+    IMemoryBuffer* ReadStartRange(const std::string& fileUuid,
+                                  FileContentType fullFileContentType,
+                                  uint64_t end /* exclusive */,
+                                  FileContentType startFileContentType);
+
     void Remove(const std::string& fileUuid,
                 FileContentType type);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -0,0 +1,118 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "StorageCache.h"
+
+#include "../Compatibility.h"
+#include "../OrthancException.h"
+
+
+
+namespace Orthanc
+{
+  bool IsAcceptedContentType(FileContentType contentType)
+  {
+    return contentType == FileContentType_Dicom ||
+      contentType == FileContentType_DicomUntilPixelData ||
+      contentType == FileContentType_DicomAsJson;
+  }
+
+  const char* ToString(FileContentType contentType)
+  {
+    switch (contentType)
+    {
+      case FileContentType_Dicom:
+        return "dicom";
+      case FileContentType_DicomUntilPixelData:
+        return "dicom-header";
+      case FileContentType_DicomAsJson:
+        return "dicom-json";
+      default:
+        throw OrthancException(ErrorCode_InternalError,
+                               "ContentType not supported in StorageCache");
+    }
+  }
+
+  void GetCacheKey(std::string& key, const std::string& uuid, FileContentType contentType)
+  {
+    key = uuid + ":" + std::string(ToString(contentType));
+  }
+  
+  void StorageCache::SetMaximumSize(size_t size)
+  {
+    cache_.SetMaximumSize(size);
+  }
+
+  void StorageCache::Add(const std::string& uuid, 
+                         FileContentType contentType,
+                         const std::string& value)
+  {
+    if (!IsAcceptedContentType(contentType))
+    {
+      return;
+    }
+
+    std::string key;
+    GetCacheKey(key, uuid, contentType);
+    cache_.Add(key, value);
+  }
+
+  void StorageCache::Add(const std::string& uuid, 
+                         FileContentType contentType,
+                         const void* buffer,
+                         size_t size)
+  {
+    if (!IsAcceptedContentType(contentType))
+    {
+      return;
+    }
+    
+    std::string key;
+    GetCacheKey(key, uuid, contentType);
+    cache_.Add(key, buffer, size);
+  }
+
+  void StorageCache::Invalidate(const std::string& uuid, FileContentType contentType)
+  {
+    std::string key;
+    GetCacheKey(key, uuid, contentType);
+    cache_.Invalidate(key);
+  }
+
+  bool StorageCache::Fetch(std::string& value, 
+                           const std::string& uuid,
+                           FileContentType contentType)
+  {
+    if (!IsAcceptedContentType(contentType))
+    {
+      return false;
+    }
+
+    std::string key;
+    GetCacheKey(key, uuid, contentType);
+
+    return cache_.Fetch(value, key);
+  }
+
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/StorageCache.h	Thu Nov 25 13:12:32 2021 +0100
@@ -0,0 +1,59 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Cache/MemoryStringCache.h"
+
+#include "../Compatibility.h"  // For ORTHANC_OVERRIDE
+
+#include <boost/thread/mutex.hpp>
+#include <map>
+
+namespace Orthanc
+{
+   /**
+   *  Note: this class is thread safe
+   **/
+   class ORTHANC_PUBLIC StorageCache : public boost::noncopyable
+    {
+      MemoryStringCache   cache_;
+    public:
+      void SetMaximumSize(size_t size);
+
+      void Add(const std::string& uuid, 
+               FileContentType contentType,
+               const std::string& value);
+
+      void Add(const std::string& uuid, 
+               FileContentType contentType,
+               const void* buffer,
+               size_t size);
+
+      void Invalidate(const std::string& uuid, FileContentType contentType);
+
+      bool Fetch(std::string& value, 
+                 const std::string& uuid,
+                 FileContentType contentType);
+
+    };
+}
--- a/OrthancFramework/Sources/JobsEngine/IJob.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/IJob.h	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Thu Nov 25 13:12:32 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/Logging.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/Logging.h	Thu Nov 25 13:12:32 2021 +0100
@@ -67,7 +67,7 @@
       LogCategory_SQLITE  = (1 << 3),
       LogCategory_DICOM   = (1 << 4),
       LogCategory_JOBS    = (1 << 5),
-      LogCategory_LUA     = (1 << 6),
+      LogCategory_LUA     = (1 << 6)
     };
     
     ORTHANC_PUBLIC const char* EnumerationToString(LogLevel level);
--- a/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.h	Thu Nov 25 13:12:32 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/MultiThreading/Semaphore.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/MultiThreading/Semaphore.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -31,10 +31,6 @@
   Semaphore::Semaphore(unsigned int availableResources) :
     availableResources_(availableResources)
   {
-    if (availableResources_ == 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
   }
 
   unsigned int Semaphore::GetAvailableResourcesCount() const
--- a/OrthancFramework/Sources/MultiThreading/Semaphore.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/MultiThreading/Semaphore.h	Thu Nov 25 13:12:32 2021 +0100
@@ -36,16 +36,16 @@
     boost::mutex mutex_;
     boost::condition_variable condition_;
 
+  public:
+    explicit Semaphore(unsigned int availableResources);
+
+    unsigned int GetAvailableResourcesCount() const;
+
     void Release(unsigned int resourceCount = 1);
 
     void Acquire(unsigned int resourceCount = 1);
 
     bool TryAcquire(unsigned int resourceCount = 1);
-  public:
-    explicit Semaphore(unsigned int availableResources);
-
-    unsigned int GetAvailableResourcesCount() const;
-
 
     class Locker : public boost::noncopyable
     {
--- a/OrthancFramework/Sources/RestApi/RestApiOutput.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.cpp	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.h	Thu Nov 25 13:12:32 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/Sources/SQLite/Transaction.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/SQLite/Transaction.h	Thu Nov 25 13:12:32 2021 +0100
@@ -61,11 +61,11 @@
       // Returns true when there is a transaction that has been successfully begun.
       bool IsOpen() const;
 
-      virtual void Begin();
+      virtual void Begin() ORTHANC_OVERRIDE;
 
-      virtual void Rollback();
+      virtual void Rollback() ORTHANC_OVERRIDE;
 
-      virtual void Commit();
+      virtual void Commit() ORTHANC_OVERRIDE;
     };
   }
 }
--- a/OrthancFramework/Sources/StringMemoryBuffer.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -47,4 +47,14 @@
     result->Copy(buffer);
     return result.release();
   }
+
+
+  IMemoryBuffer* StringMemoryBuffer::CreateFromCopy(const std::string& buffer, 
+                                                    size_t start /* inclusive */, 
+                                                    size_t end /* exclusive */)
+  {
+    std::unique_ptr<StringMemoryBuffer> result(new StringMemoryBuffer);
+    result->Copy(buffer, start, end);
+    return result.release();
+  }
 }
--- a/OrthancFramework/Sources/StringMemoryBuffer.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/Sources/StringMemoryBuffer.h	Thu Nov 25 13:12:32 2021 +0100
@@ -38,6 +38,11 @@
       buffer_ = buffer;
     }
 
+    void Copy(const std::string& buffer, size_t start /* inclusive */, size_t end /* exclusive */)
+    {
+      buffer_.assign(buffer, start, end - start);
+    }
+
     void Swap(std::string& buffer)
     {
       buffer_.swap(buffer);
@@ -58,5 +63,7 @@
     static IMemoryBuffer* CreateFromSwap(std::string& buffer);
 
     static IMemoryBuffer* CreateFromCopy(const std::string& buffer);
+
+    static IMemoryBuffer* CreateFromCopy(const std::string& buffer, size_t start /* inclusive */, size_t end /* exclusive */);
   };
 }
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -29,6 +29,7 @@
 
 #include "../Sources/FileStorage/FilesystemStorage.h"
 #include "../Sources/FileStorage/StorageAccessor.h"
+#include "../Sources/FileStorage/StorageCache.h"
 #include "../Sources/HttpServer/BufferHttpSender.h"
 #include "../Sources/HttpServer/FilesystemHttpSender.h"
 #include "../Sources/Logging.h"
@@ -124,7 +125,8 @@
 TEST(StorageAccessor, NoCompression)
 {
   FilesystemStorage s("UnitTestsStorage");
-  StorageAccessor accessor(s);
+  StorageCache cache;
+  StorageAccessor accessor(s, cache);
 
   std::string data = "Hello world";
   FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true);
@@ -145,7 +147,8 @@
 TEST(StorageAccessor, Compression)
 {
   FilesystemStorage s("UnitTestsStorage");
-  StorageAccessor accessor(s);
+  StorageCache cache;
+  StorageAccessor accessor(s, cache);
 
   std::string data = "Hello world";
   FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true);
@@ -165,7 +168,8 @@
 TEST(StorageAccessor, Mix)
 {
   FilesystemStorage s("UnitTestsStorage");
-  StorageAccessor accessor(s);
+  StorageCache cache;
+  StorageAccessor accessor(s, cache);
 
   std::string r;
   std::string compressedData = "Hello";
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -3132,6 +3132,23 @@
 }
 
 
+TEST(DicomMap, DicomWebWithInteger64)
+{
+  /**
+   * This failed in Orthanc <= 1.9.7 with
+   * "http://localhost:8042/dicom-web/studies/1.3.6.1.4.1.14519.5.2.1.314316487728501506587013300243937537423/series/1.3.6.1.4.1.1459.5.2.1.62266640231940987006694557463549207147/instances/1.3.6.1.4.1.14519.5.2.1.147718809116229175846174241356499989705/metadata"
+   * of patient "GLIOMA01-i_03A6" from collection "ICDC-Glioma" of
+   * TCIA.
+   **/
+  Json::Value v = Json::objectValue;
+  v["00191297"]["Value"][0] = 29362240;
+  v["00191297"]["Value"][1] = Json::Int64(4294948074l);
+  v["00191297"]["vr"] = "UL";
+  DicomMap m;
+  m.FromDicomWeb(v);
+}
+
+
 
 
 #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/OrthancExplorer/explorer.js	Thu Nov 25 13:12:32 2021 +0100
@@ -1101,7 +1101,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
@@ -1113,7 +1113,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, {
@@ -1143,7 +1143,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -67,6 +67,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"
 
@@ -1153,6 +1154,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;
@@ -1175,6 +1177,7 @@
     IncomingHttpRequestFilters  incomingHttpRequestFilters_;
     IncomingHttpRequestFilters2 incomingHttpRequestFilters2_;
     IncomingDicomInstanceFilters  incomingDicomInstanceFilters_;
+    IncomingCStoreInstanceFilters  incomingCStoreInstanceFilters_;  // New in Orthanc 1.9.8
     RefreshMetricsCallbacks refreshMetricsCallbacks_;
     StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_;
     std::unique_ptr<StorageAreaFactory>  storageArea_;
@@ -2249,7 +2252,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)
@@ -2467,6 +2499,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 = 
@@ -4945,6 +4987,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Thu Nov 25 13:12:32 2021 +0100
@@ -121,6 +121,8 @@
 
     void RegisterIncomingDicomInstanceFilter(const void* parameters);
 
+    void RegisterIncomingCStoreInstanceFilter(const void* parameters);
+
     void RegisterRefreshMetricsCallback(const void* parameters);
 
     void RegisterStorageCommitmentScpCallback(const void* parameters);
@@ -267,6 +269,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsJob.h	Thu Nov 25 13:12:32 2021 +0100
@@ -63,6 +63,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Nov 25 13:12:32 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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Resources/Configuration.json	Thu Nov 25 13:12:32 2021 +0100
@@ -40,7 +40,13 @@
   // in the storage (a value of "0" indicates no limit on the number
   // of patients)
   "MaximumPatientCount" : 0,
-  
+
+  // Maximum size of the storage cache in MB.  The storage cache
+  // is stored in RAM and contains a copy of recently accessed
+  // files (written or read).  A value of "0" indicates the cache
+  // is disabled.  (new in Orthanc 1.9.8)
+  "MaximumStorageCacheSize" : 128,
+
   // List of paths to the custom Lua scripts that are to be loaded
   // into this instance of Orthanc
   "LuaScripts" : [
@@ -418,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.
@@ -828,5 +843,12 @@
   // disk space and might lead to HTTP timeouts on large archives). If
   // set to "true", the chunks of the ZIP file are progressively sent
   // as soon as one DICOM file gets compressed (new in Orthanc 1.9.4)
-  "SynchronousZipStream" : true
+  "SynchronousZipStream" : true,
+
+  // Default number of loader threads when generating Zip archive/media.
+  // A value of 0 means reading and writing are performed in sequence
+  // (default behaviour).  A value > 1 is meaningful only if the storage
+  // is a distributed network storage (e.g object storage plugin).
+  // (new in Orthanc 1.9.8)
+  "ZipLoaderThreads": 0
 }
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -62,6 +62,11 @@
       return "ESCAPE '\\'";
     }
 
+    virtual bool IsEscapeBrackets() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
     void Bind(SQLite::Statement& statement) const
     {
       size_t pos = 0;
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -2798,7 +2798,7 @@
         
         if (!ok)
         {
-          throw OrthancException(ErrorCode_FullStorage);
+          throw OrthancException(ErrorCode_FullStorage, "Cannot recycle more patients");
         }
       
         LOG(TRACE) << "Recycling one patient";
@@ -3252,11 +3252,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/IServerListener.h	Thu Nov 25 13:12:32 2021 +0100
@@ -43,5 +43,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/LuaScripting.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -31,6 +31,8 @@
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h"
 
+#include <dcmtk/dcmnet/dimse.h>
+
 #include <OrthancServerResources.h>
 
 
@@ -933,6 +935,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/LuaScripting.h	Thu Nov 25 13:12:32 2021 +0100
@@ -117,6 +117,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -485,7 +485,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -322,7 +322,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -542,16 +542,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -187,10 +187,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)
@@ -240,9 +240,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/OrthancRestArchive.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -39,7 +39,9 @@
   static const char* const KEY_RESOURCES = "Resources";
   static const char* const KEY_EXTENDED = "Extended";
   static const char* const KEY_TRANSCODE = "Transcode";
-  
+
+  static const char* const CONFIG_LOADER_THREADS = "ZipLoaderThreads";
+
   static void AddResourcesOfInterestFromArray(ArchiveJob& job,
                                               const Json::Value& resources)
   {
@@ -111,6 +113,7 @@
                                bool& transcode,              /* out */
                                DicomTransferSyntax& syntax,  /* out */
                                int& priority,                /* out */
+                               unsigned int& loaderThreads,  /* out */
                                const Json::Value& body,      /* in */
                                const bool defaultExtended    /* in */)
   {
@@ -139,6 +142,12 @@
     {
       transcode = false;
     }
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0);  // New in Orthanc 1.9.8
+    }
+   
   }
 
 
@@ -542,8 +551,9 @@
       bool synchronous, extended, transcode;
       DicomTransferSyntax transferSyntax;
       int priority;
+      unsigned int loaderThreads;
       GetJobParameters(synchronous, extended, transcode, transferSyntax,
-                       priority, body, DEFAULT_IS_EXTENDED);
+                       priority, loaderThreads, body, DEFAULT_IS_EXTENDED);
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
       AddResourcesOfInterest(*job, body);
@@ -553,6 +563,8 @@
         job->SetTranscode(transferSyntax);
       }
       
+      job->SetLoaderThreads(loaderThreads);
+
       SubmitJob(call.GetOutput(), context, job, priority, synchronous, "Archive.zip");
     }
     else
@@ -566,6 +578,8 @@
   template <bool IS_MEDIA>
   static void CreateSingleGet(RestApiGetCall& call)
   {
+    static const char* const TRANSCODE = "transcode";
+
     if (call.IsDocumentation())
     {
       ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
@@ -579,7 +593,7 @@
                         "which might *not* be desirable to archive large amount of data, as it might "
                         "lead to network timeouts. Prefer the asynchronous version using `POST` method.")
         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
-        .SetHttpGetArgument("transcode", RestApiCallDocumentation::Type_String,
+        .SetHttpGetArgument(TRANSCODE, RestApiCallDocumentation::Type_String,
                             "If present, the DICOM files in the archive will be transcoded to the provided "
                             "transfer syntax: https://book.orthanc-server.com/faq/transcoding.html", false)
         .AddAnswerType(MimeType_Zip, "ZIP file containing the archive");
@@ -609,12 +623,17 @@
     std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
     job->AddResource(id);
 
-    static const char* const TRANSCODE = "transcode";
     if (call.HasArgument(TRANSCODE))
     {
       job->SetTranscode(GetTransferSyntax(call.GetArgument(TRANSCODE, "")));
     }
 
+    {
+      OrthancConfiguration::ReaderLock lock;
+      unsigned int loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0);  // New in Orthanc 1.9.8
+      job->SetLoaderThreads(loaderThreads);
+    }
+
     SubmitJob(call.GetOutput(), context, job, 0 /* priority */,
               true /* synchronous */, id + ".zip");
   }
@@ -648,8 +667,9 @@
       bool synchronous, extended, transcode;
       DicomTransferSyntax transferSyntax;
       int priority;
+      unsigned int loaderThreads;
       GetJobParameters(synchronous, extended, transcode, transferSyntax,
-                       priority, body, false /* by default, not extented */);
+                       priority, loaderThreads, body, false /* by default, not extented */);
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
       job->AddResource(id);
@@ -659,6 +679,8 @@
         job->SetTranscode(transferSyntax);
       }
 
+      job->SetLoaderThreads(loaderThreads);
+
       SubmitJob(call.GetOutput(), context, job, priority, synchronous, id + ".zip");
     }
     else
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -743,6 +743,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")
@@ -805,13 +806,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;
         }
@@ -2957,7 +2965,7 @@
     std::string publicId = call.GetUriComponent("id", "");
 
     std::string dicomContent;
-    context.ReadDicom(dicomContent, publicId);
+    context.ReadDicomForHeader(dicomContent, publicId);
 
     // TODO Consider using "DicomMap::ParseDicomMetaInformation()" to
     // speed up things here
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -684,10 +684,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -1301,9 +1301,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/Search/ISqlLookupFormatter.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -67,7 +67,8 @@
   static bool FormatComparison(std::string& target,
                                ISqlLookupFormatter& formatter,
                                const DatabaseConstraint& constraint,
-                               size_t index)
+                               size_t index,
+                               bool escapeBrackets)
   {
     std::string tag = "t" + boost::lexical_cast<std::string>(index);
 
@@ -184,6 +185,14 @@
             {
               escaped += "\\\\";
             }
+            else if (escapeBrackets && value[i] == '[')
+            {
+              escaped += "\\[";
+            }
+            else if (escapeBrackets && value[i] == ']')
+            {
+              escaped += "\\]";
+            }
             else
             {
               escaped += value[i];
@@ -291,6 +300,8 @@
     assert(upperLevel <= queryLevel &&
            queryLevel <= lowerLevel);
 
+    const bool escapeBrackets = formatter.IsEscapeBrackets();
+    
     std::string joins, comparisons;
 
     size_t count = 0;
@@ -299,7 +310,7 @@
     {
       std::string comparison;
       
-      if (FormatComparison(comparison, formatter, lookup[i], count))
+      if (FormatComparison(comparison, formatter, lookup[i], count, escapeBrackets))
       {
         std::string join;
         FormatJoin(join, lookup[i], count);
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Thu Nov 25 13:12:32 2021 +0100
@@ -48,6 +48,13 @@
 
     virtual std::string FormatWildcardEscape() = 0;
 
+    /**
+     * Whether to escape '[' and ']', which is only needed for
+     * MSSQL. New in Orthanc 1.9.8, from the following changeset:
+     * https://hg.orthanc-server.com/orthanc-databases/rev/389c037387ea
+     **/
+    virtual bool IsEscapeBrackets() const = 0;
+
     static void Apply(std::string& sql,
                       ISqlLookupFormatter& formatter,
                       const std::vector<DatabaseConstraint>& lookup,
--- a/OrthancServer/Sources/ServerContext.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -45,6 +45,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
@@ -88,6 +89,13 @@
       transferSyntax != DicomTransferSyntax_XML);
   }
 
+
+  ServerContext::StoreResult::StoreResult() :
+    status_(StoreStatus_Failure),
+    cstoreStatusCode_(0)
+  {
+  }
+
   
   void ServerContext::ChangeThread(ServerContext* that,
                                    unsigned int sleepDelay)
@@ -472,14 +480,14 @@
   void ServerContext::RemoveFile(const std::string& fileUuid,
                                  FileContentType type)
   {
-    StorageAccessor accessor(area_, GetMetricsRegistry());
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     accessor.Remove(fileUuid, type);
   }
 
 
-  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)
@@ -514,7 +522,7 @@
     try
     {
       MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms");
-      StorageAccessor accessor(area_, GetMetricsRegistry());
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
 
       DicomInstanceHasher hasher(summary);
       resultPublicId = hasher.HashInstance();
@@ -526,7 +534,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_);
@@ -537,9 +545,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)
           {
@@ -551,10 +572,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
@@ -583,9 +604,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();
@@ -596,7 +617,7 @@
         dicom.AddMetadata(ResourceType_Instance, it->first, it->second);
       }
             
-      if (status != StoreStatus_Success)
+      if (result.GetStatus() != StoreStatus_Success)
       {
         accessor.Remove(dicomInfo);
 
@@ -606,7 +627,7 @@
         }
       }
 
-      switch (status)
+      switch (result.GetStatus())
       {
         case StoreStatus_Success:
           LOG(INFO) << "New instance stored";
@@ -625,8 +646,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_);
 
@@ -645,7 +666,7 @@
         }
       }
 
-      return status;
+      return result;
     }
     catch (OrthancException& e)
     {
@@ -659,9 +680,9 @@
   }
 
 
-  StoreStatus ServerContext::Store(std::string& resultPublicId,
-                                   DicomInstanceToStore& dicom,
-                                   StoreInstanceMode mode)
+  ServerContext::StoreResult ServerContext::Store(std::string& resultPublicId,
+                                                  DicomInstanceToStore& dicom,
+                                                  StoreInstanceMode mode)
   {
     if (!isIngestTranscoding_)
     {
@@ -721,10 +742,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
         {
@@ -748,7 +769,7 @@
     }
     else
     {
-      StorageAccessor accessor(area_, GetMetricsRegistry());
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
       accessor.AnswerFile(output, attachment, GetFileContentMime(content));
     }
   }
@@ -778,7 +799,7 @@
 
     std::string content;
 
-    StorageAccessor accessor(area_, GetMetricsRegistry());
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     accessor.Read(content, attachment);
 
     FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(),
@@ -834,7 +855,7 @@
       std::string dicom;
 
       {
-        StorageAccessor accessor(area_, GetMetricsRegistry());
+        StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
         accessor.Read(dicom, attachment);
       }
 
@@ -899,8 +920,8 @@
       
         std::unique_ptr<IMemoryBuffer> dicom;
         {
-          MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_storage_read_range_duration_ms");
-          dicom.reset(area_.ReadRange(attachment.GetUuid(), FileContentType_Dicom, 0, pixelDataOffset));
+          StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+          dicom.reset(accessor.ReadStartRange(attachment.GetUuid(), FileContentType_Dicom, pixelDataOffset, FileContentType_DicomUntilPixelData));
         }
 
         if (dicom.get() == NULL)
@@ -929,7 +950,7 @@
         std::string dicomAsJson;
 
         {
-          StorageAccessor accessor(area_, GetMetricsRegistry());
+          StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
           accessor.Read(dicomAsJson, attachment);
         }
 
@@ -998,7 +1019,15 @@
     int64_t revision;
     ReadAttachment(dicom, revision, instancePublicId, FileContentType_Dicom, true /* uncompress */);
   }
-    
+
+  void ServerContext::ReadDicomForHeader(std::string& dicom,
+                                         const std::string& instancePublicId)
+  {
+    if (!ReadDicomUntilPixelData(dicom, instancePublicId))
+    {
+      ReadDicom(dicom, instancePublicId);
+    }
+  }
 
   bool ServerContext::ReadDicomUntilPixelData(std::string& dicom,
                                               const std::string& instancePublicId)
@@ -1027,8 +1056,10 @@
       {
         uint64_t pixelDataOffset = boost::lexical_cast<uint64_t>(s);
 
+        StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
+
         std::unique_ptr<IMemoryBuffer> buffer(
-          area_.ReadRange(attachment.GetUuid(), attachment.GetContentType(), 0, pixelDataOffset));
+          accessor.ReadStartRange(attachment.GetUuid(), attachment.GetContentType(), pixelDataOffset, FileContentType_DicomUntilPixelData));
         buffer->MoveToString(dicom);
         return true;   // Success
       }
@@ -1059,7 +1090,7 @@
     assert(attachment.GetContentType() == content);
 
     {
-      StorageAccessor accessor(area_, GetMetricsRegistry());
+      StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
 
       if (uncompressIfNeeded)
       {
@@ -1159,7 +1190,7 @@
     // TODO Should we use "gzip" instead?
     CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
-    StorageAccessor accessor(area_, GetMetricsRegistry());
+    StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry());
     FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
 
     try
@@ -1975,10 +2006,9 @@
     static const std::string redactedContent = "*** POTENTIAL PHI ***";
 
     const DicomTag& tag = element.GetTag();
-    if (deidentifyLogs_ && (
-          logsDeidentifierRules_.IsCleared(tag) ||
-          logsDeidentifierRules_.IsRemoved(tag) ||
-          logsDeidentifierRules_.IsReplaced(tag)))
+    if (deidentifyLogs_ &&
+        !element.GetValue().GetContent().empty() &&
+        logsDeidentifierRules_.IsAlteredTag(tag))
     {
       return redactedContent;
     }
--- a/OrthancServer/Sources/ServerContext.h	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Thu Nov 25 13:12:32 2021 +0100
@@ -31,6 +31,7 @@
 #include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h"
 #include "../../OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h"
 #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomCache.h"
+#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h"
 #include "../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
 
 
@@ -81,6 +82,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
@@ -111,6 +142,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
@@ -157,6 +194,7 @@
 
     ServerIndex index_;
     IStorageArea& area_;
+    StorageCache storageCache_;
 
     bool compressionEnabled_;
     bool storeMD5_;
@@ -219,7 +257,7 @@
     bool isUnknownSopClassAccepted_;
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
 
-    StoreStatus StoreAfterTranscoding(std::string& resultPublicId,
+    StoreResult StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
                                       StoreInstanceMode mode);
 
@@ -276,6 +314,11 @@
       return index_;
     }
 
+    void SetMaximumStorageCacheSize(size_t size)
+    {
+      return storageCache_.SetMaximumSize(size);
+    }
+
     void SetCompressionEnabled(bool enabled);
 
     bool IsCompressionEnabled() const
@@ -292,7 +335,7 @@
                        int64_t oldRevision,
                        const std::string& oldMD5);
 
-    StoreStatus Store(std::string& resultPublicId,
+    StoreResult Store(std::string& resultPublicId,
                       DicomInstanceToStore& dicom,
                       StoreInstanceMode mode);
 
@@ -313,7 +356,10 @@
 
     void ReadDicom(std::string& dicom,
                    const std::string& instancePublicId);
-    
+
+    void ReadDicomForHeader(std::string& dicom,
+                            const std::string& instancePublicId);
+
     bool ReadDicomUntilPixelData(std::string& dicom,
                                  const std::string& instancePublicId);
 
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -28,6 +28,7 @@
 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
 #include "../OrthancConfiguration.h"
 #include "../ServerContext.h"
 
@@ -76,6 +77,181 @@
   }
 
 
+  class ArchiveJob::InstanceLoader : public boost::noncopyable
+  {
+  protected:
+    ServerContext&                        context_;
+  public:
+    explicit InstanceLoader(ServerContext& context)
+    : context_(context)
+    {
+    }
+
+    virtual ~InstanceLoader()
+    {
+    }
+
+    virtual void PrepareDicom(const std::string& instanceId)
+    {
+
+    }
+
+    virtual void GetDicom(std::string& dicom, const std::string& instanceId) = 0;
+
+    virtual void Clear()
+    {
+    }
+  };
+
+  class ArchiveJob::SynchronousInstanceLoader : public ArchiveJob::InstanceLoader
+  {
+  public:
+    explicit SynchronousInstanceLoader(ServerContext& context)
+    : InstanceLoader(context)
+    {
+    }
+
+    virtual void GetDicom(std::string& dicom, const std::string& instanceId) ORTHANC_OVERRIDE
+    {
+      context_.ReadDicom(dicom, instanceId);
+    }
+  };
+
+  class InstanceId : public Orthanc::IDynamicObject
+  {
+  private:
+    std::string id_;
+
+  public:
+    explicit InstanceId(const std::string& id) : id_(id)
+    {
+    }
+
+    virtual ~InstanceId() ORTHANC_OVERRIDE
+    {
+    }
+
+    std::string GetId() const {return id_;};
+  };
+
+  class ArchiveJob::ThreadedInstanceLoader : public ArchiveJob::InstanceLoader
+  {
+    Semaphore                           availableInstancesSemaphore_;
+    std::map<std::string, boost::shared_ptr<std::string> >  availableInstances_;
+    boost::mutex                        availableInstancesMutex_;
+    SharedMessageQueue                  instancesToPreload_;
+    std::vector<boost::thread*>         threads_;
+
+
+  public:
+    ThreadedInstanceLoader(ServerContext& context, size_t threadCount)
+    : InstanceLoader(context),
+      availableInstancesSemaphore_(0)
+    {
+      for (size_t i = 0; i < threadCount; i++)
+      {
+        threads_.push_back(new boost::thread(PreloaderWorkerThread, this));
+      }
+    }
+
+    virtual ~ThreadedInstanceLoader()
+    {
+      Clear();
+    }
+
+    virtual void Clear() ORTHANC_OVERRIDE
+    {
+      for (size_t i = 0; i < threads_.size(); i++)
+      {
+        instancesToPreload_.Enqueue(NULL);
+      }
+
+      for (size_t i = 0; i < threads_.size(); i++)
+      {
+        if (threads_[i]->joinable())
+        {
+          threads_[i]->join();
+        }
+        delete threads_[i];
+      }
+
+      threads_.clear();
+      availableInstances_.clear();
+    }
+
+    static void PreloaderWorkerThread(ThreadedInstanceLoader* that)
+    {
+      while (true)
+      {
+        std::unique_ptr<InstanceId> instanceId(dynamic_cast<InstanceId*>(that->instancesToPreload_.Dequeue(0)));
+        if (instanceId.get() == NULL)  // that's the signal to exit the thread
+        {
+          return;
+        }
+
+        try
+        {
+          boost::shared_ptr<std::string> dicomContent(new std::string());
+          that->context_.ReadDicom(*dicomContent, instanceId->GetId());
+          {
+            boost::mutex::scoped_lock lock(that->availableInstancesMutex_);
+            that->availableInstances_[instanceId->GetId()] = dicomContent;
+          }
+
+          that->availableInstancesSemaphore_.Release();
+        }
+        catch (OrthancException& e)
+        {
+          boost::mutex::scoped_lock lock(that->availableInstancesMutex_);
+          // store a NULL result to notify that we could not read the instance
+          that->availableInstances_[instanceId->GetId()] = boost::shared_ptr<std::string>(); 
+          that->availableInstancesSemaphore_.Release();
+        }
+      }
+    }
+
+    virtual void PrepareDicom(const std::string& instanceId) ORTHANC_OVERRIDE
+    {
+      instancesToPreload_.Enqueue(new InstanceId(instanceId));
+    }
+
+    virtual void GetDicom(std::string& dicom, const std::string& instanceId) ORTHANC_OVERRIDE
+    {
+      while (true)
+      {
+        // wait for an instance to be available but this might not be the one we are waiting for !
+        availableInstancesSemaphore_.Acquire();
+
+        boost::shared_ptr<std::string> dicomContent;
+        {
+          if (availableInstances_.find(instanceId) != availableInstances_.end())
+          {
+            // this is the instance we were waiting for
+            dicomContent = availableInstances_[instanceId];
+            availableInstances_.erase(instanceId);
+
+            if (dicomContent.get() == NULL)  // there has been an error while reading the file
+            {
+              throw OrthancException(ErrorCode_InexistentItem);
+            }
+            dicom.swap(*dicomContent);
+
+            if (availableInstances_.size() > 0)
+            {
+              // we have just read the instance we were waiting for but there are still other instances available ->
+              // make sure the next GetDicom call does not wait !
+              availableInstancesSemaphore_.Release();
+            }
+            return;
+          }
+          // we have not found the expected instance, simply wait for the next loader thread to signal the semaphore when
+          // a new instance is available
+        }
+      }
+    }
+  };
+
+
   class ArchiveJob::ResourceIdentifiers : public boost::noncopyable
   {
   private:
@@ -390,6 +566,7 @@
         
       void Apply(HierarchicalZipWriter& writer,
                  ServerContext& context,
+                 InstanceLoader& instanceLoader,
                  DicomDirWriter* dicomDir,
                  const std::string& dicomDirFolder,
                  bool transcode,
@@ -411,7 +588,7 @@
 
             try
             {
-              context.ReadDicom(content, instanceId_);
+              instanceLoader.GetDicom(content, instanceId_);
             }
             catch (OrthancException& e)
             {
@@ -482,10 +659,12 @@
     std::deque<Command*>  commands_;
     uint64_t              uncompressedSize_;
     unsigned int          instancesCount_;
+    InstanceLoader&       instanceLoader_;
 
       
     void ApplyInternal(HierarchicalZipWriter& writer,
                        ServerContext& context,
+                       InstanceLoader& instanceLoader,
                        size_t index,
                        DicomDirWriter* dicomDir,
                        const std::string& dicomDirFolder,
@@ -497,13 +676,14 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
 
-      commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder, transcode, transferSyntax);
+      commands_[index]->Apply(writer, context, instanceLoader, dicomDir, dicomDirFolder, transcode, transferSyntax);
     }
       
   public:
-    ZipCommands() :
+    explicit ZipCommands(InstanceLoader& instanceLoader) :
       uncompressedSize_(0),
-      instancesCount_(0)
+      instancesCount_(0),
+      instanceLoader_(instanceLoader)
     {
     }
       
@@ -535,23 +715,25 @@
     // "media" flavor (with DICOMDIR)
     void Apply(HierarchicalZipWriter& writer,
                ServerContext& context,
+               InstanceLoader& instanceLoader,
                size_t index,
                DicomDirWriter& dicomDir,
                const std::string& dicomDirFolder,
                bool transcode,
                DicomTransferSyntax transferSyntax) const
     {
-      ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder, transcode, transferSyntax);
+      ApplyInternal(writer, context, instanceLoader, index, &dicomDir, dicomDirFolder, transcode, transferSyntax);
     }
 
     // "archive" flavor (without DICOMDIR)
     void Apply(HierarchicalZipWriter& writer,
                ServerContext& context,
+               InstanceLoader& instanceLoader,
                size_t index,
                bool transcode,
                DicomTransferSyntax transferSyntax) const
     {
-      ApplyInternal(writer, context, index, NULL, "", transcode, transferSyntax);
+      ApplyInternal(writer, context, instanceLoader, index, NULL, "", transcode, transferSyntax);
     }
       
     void AddOpenDirectory(const std::string& filename)
@@ -568,6 +750,7 @@
                           const std::string& instanceId,
                           uint64_t uncompressedSize)
     {
+      instanceLoader_.PrepareDicom(instanceId);
       commands_.push_back(new Command(Type_WriteInstance, filename, instanceId));
       instancesCount_ ++;
       uncompressedSize_ += uncompressedSize;
@@ -735,6 +918,7 @@
   {
   private:
     ServerContext&                          context_;
+    InstanceLoader&                         instanceLoader_;
     ZipCommands                             commands_;
     std::unique_ptr<HierarchicalZipWriter>  zip_;
     std::unique_ptr<DicomDirWriter>         dicomDir_;
@@ -743,10 +927,13 @@
 
   public:
     ZipWriterIterator(ServerContext& context,
+                      InstanceLoader& instanceLoader,
                       ArchiveIndex& archive,
                       bool isMedia,
                       bool enableExtendedSopClass) :
       context_(context),
+      instanceLoader_(instanceLoader),
+      commands_(instanceLoader),
       isMedia_(isMedia),
       isStream_(false)
     {
@@ -870,13 +1057,13 @@
         if (isMedia_)
         {
           assert(dicomDir_.get() != NULL);
-          commands_.Apply(*zip_, context_, index, *dicomDir_,
+          commands_.Apply(*zip_, context_, instanceLoader_, index, *dicomDir_,
                           MEDIA_IMAGES_FOLDER, transcode, transferSyntax);
         }
         else
         {
           assert(dicomDir_.get() == NULL);
-          commands_.Apply(*zip_, context_, index, transcode, transferSyntax);
+          commands_.Apply(*zip_, context_, instanceLoader_, index, transcode, transferSyntax);
         }
       }
     }
@@ -905,7 +1092,8 @@
     uncompressedSize_(0),
     archiveSize_(0),
     transcode_(false),
-    transferSyntax_(DicomTransferSyntax_LittleEndianImplicit)
+    transferSyntax_(DicomTransferSyntax_LittleEndianImplicit),
+    loaderThreads_(0)
   {
   }
 
@@ -981,6 +1169,19 @@
   }
 
   
+  void ArchiveJob::SetLoaderThreads(unsigned int loaderThreads)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      loaderThreads_ = loaderThreads;
+    }
+  }
+
+
   void ArchiveJob::Reset()
   {
     throw OrthancException(ErrorCode_BadSequenceOfCalls,
@@ -990,6 +1191,16 @@
   
   void ArchiveJob::Start()
   {
+    if (loaderThreads_ == 0)
+    {
+      // default behaviour before loaderThreads was introducted in 1.9.8
+      instanceLoader_.reset(new SynchronousInstanceLoader(context_));
+    }
+    else
+    {
+      instanceLoader_.reset(new ThreadedInstanceLoader(context_, loaderThreads_));
+    }
+
     if (writer_.get() != NULL)
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
@@ -1011,7 +1222,7 @@
           assert(asynchronousTarget_.get() != NULL);
           asynchronousTarget_->Touch();  // Make sure we can write to the temporary file
           
-          writer_.reset(new ZipWriterIterator(context_, *archive_, isMedia_, enableExtendedSopClass_));
+          writer_.reset(new ZipWriterIterator(context_, *instanceLoader_, *archive_, isMedia_, enableExtendedSopClass_));
           writer_->SetOutputFile(asynchronousTarget_->GetPath());
         }
       }
@@ -1019,7 +1230,7 @@
       {
         assert(synchronousTarget_.get() != NULL);
     
-        writer_.reset(new ZipWriterIterator(context_, *archive_, isMedia_, enableExtendedSopClass_));
+        writer_.reset(new ZipWriterIterator(context_, *instanceLoader_, *archive_, isMedia_, enableExtendedSopClass_));
         writer_->AcquireOutputStream(synchronousTarget_.release());
       }
 
@@ -1064,6 +1275,11 @@
       writer_.reset();
     }
 
+    if (instanceLoader_.get() != NULL)
+    {
+      instanceLoader_->Clear();
+    }
+
     if (asynchronousTarget_.get() != NULL)
     {
       // Asynchronous behavior: Move the resulting file into the media archive
@@ -1184,6 +1400,7 @@
 
   bool ArchiveJob::GetOutput(std::string& output,
                              MimeType& mime,
+                             std::string& filename,
                              const std::string& key)
   {   
     if (key == "archive" &&
@@ -1196,6 +1413,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Thu Nov 25 13:12:32 2021 +0100
@@ -43,10 +43,14 @@
     class ResourceIdentifiers;
     class ZipCommands;
     class ZipWriterIterator;
-    
+    class InstanceLoader;
+    class SynchronousInstanceLoader;
+    class ThreadedInstanceLoader;
+
     std::unique_ptr<ZipWriter::IOutputStream>  synchronousTarget_;  // Only valid before "Start()"
     std::unique_ptr<TemporaryFile>        asynchronousTarget_;
     ServerContext&                        context_;
+    std::unique_ptr<InstanceLoader>       instanceLoader_;
     boost::shared_ptr<ArchiveIndex>       archive_;
     bool                                  isMedia_;
     bool                                  enableExtendedSopClass_;
@@ -63,6 +67,9 @@
     bool                 transcode_;
     DicomTransferSyntax  transferSyntax_;
 
+    // New in Orthanc 1.9.8
+    unsigned int         loaderThreads_;
+
     void FinalizeTarget();
     
   public:
@@ -85,6 +92,8 @@
 
     void SetTranscode(DicomTransferSyntax transferSyntax);
 
+    void SetLoaderThreads(unsigned int loaderThreads);
+
     virtual void Reset() ORTHANC_OVERRIDE;
 
     virtual void Start() ORTHANC_OVERRIDE;
@@ -106,6 +115,7 @@
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
+                           std::string& filename,
                            const std::string& key) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -149,8 +149,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -278,8 +278,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -131,8 +131,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/ServerToolbox.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -24,6 +24,7 @@
 
 #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
 #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h"
+#include "../../OrthancFramework/Sources/FileStorage/StorageCache.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/OrthancException.h"
 #include "Database/IDatabaseWrapper.h"
@@ -164,7 +165,8 @@
         try
         {
           // Read and parse the content of the DICOM file
-          StorageAccessor accessor(storageArea);
+          StorageCache cache; // we create a temporary cache for this operation (required by the StorageAccessor)
+          StorageAccessor accessor(storageArea, cache);
 
           std::string content;
           accessor.Read(content, attachment);
--- a/OrthancServer/Sources/main.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/Sources/main.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -72,10 +72,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));
     
@@ -85,8 +85,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;
   }
 };
 
@@ -1198,6 +1201,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)
@@ -1509,6 +1513,16 @@
     {
       context.GetIndex().SetMaximumStorageSize(0);
     }
+
+    try
+    {
+      uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageCacheSize", 128);
+      context.SetMaximumStorageCacheSize(size * 1024 * 1024);
+    }
+    catch (...)
+    {
+      context.SetMaximumStorageCacheSize(128);
+    }
   }
 
   {
--- a/OrthancServer/UnitTestsSources/PluginsTests.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/UnitTestsSources/PluginsTests.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -22,6 +22,7 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include <gtest/gtest.h>
 
+#include "../../OrthancFramework/Sources/Compatibility.h"
 #include "../../OrthancFramework/Sources/OrthancException.h"
 #include "../Plugins/Engine/PluginsManager.h"
 
@@ -74,11 +75,25 @@
   //ASSERT_TRUE(l.HasFunction("_init"));
   
 #elif defined(__linux__) || defined(__FreeBSD_kernel__)
-  SharedLibrary l("libdl.so");
-  ASSERT_THROW(l.GetFunction("world"), OrthancException);
-  ASSERT_TRUE(l.GetFunction("dlopen") != NULL);
-  ASSERT_TRUE(l.HasFunction("dlclose"));
-  ASSERT_FALSE(l.HasFunction("world"));
+  std::unique_ptr<SharedLibrary> l;
+  try
+  {
+    /**
+     * Since Orthanc 1.9.8, we test the "libdl.so.2" instead of the
+     * "libdl.so", as discussed here:
+     * https://groups.google.com/g/orthanc-users/c/I5g1fN6MCvg/m/JVdvRyjJAAAJ
+     **/
+    l.reset(new SharedLibrary("libdl.so.2"));
+  }
+  catch (OrthancException&)
+  {
+    l.reset(new SharedLibrary("libdl.so")); // Fallback for backward compat
+  }
+  
+  ASSERT_THROW(l->GetFunction("world"), OrthancException);
+  ASSERT_TRUE(l->GetFunction("dlopen") != NULL);
+  ASSERT_TRUE(l->HasFunction("dlclose"));
+  ASSERT_FALSE(l->HasFunction("world"));
 
 #elif defined(__FreeBSD__) || defined(__OpenBSD__)
   // dlopen() in FreeBSD/OpenBSD is supplied by libc, libc.so is
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -847,7 +847,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);
     }
 
@@ -896,8 +897,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);
     }
 
@@ -996,7 +997,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Thu Nov 25 13:12:32 2021 +0100
@@ -128,6 +128,7 @@
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
+                           std::string& filename,
                            const std::string& key) ORTHANC_OVERRIDE
     {
       return false;
@@ -525,7 +526,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	Mon Aug 30 22:21:24 2021 +0200
+++ b/TODO	Thu Nov 25 13:12:32 2021 +0100
@@ -36,7 +36,15 @@
 * Discuss HL7 in a dedicated page:
   https://groups.google.com/d/msg/orthanc-users/4dt4992O0lQ/opTjTFU2BgAJ
   https://groups.google.com/g/orthanc-users/c/Spjtcj9vSPo/m/ktUArWxUDQAJ
-  
+
+
+================
+Orthanc Explorer
+================
+
+* Option to tune the number of results for a local lookup:
+  https://groups.google.com/g/orthanc-users/c/LF39musq02Y/
+
 
 ========
 REST API
@@ -60,6 +68,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": 
@@ -119,11 +129,16 @@
 Performance
 ===========
 
+* StorageAccessor => add a memory cache using MemoryStringCache, for
+  instance to speed up WADO-RS Retrieve Frames in DICOMweb plugin
 * ServerContext::DicomCacheLocker => give access to the raw buffer,
   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