# HG changeset patch # User Sebastien Jodogne # Date 1637842352 -3600 # Node ID 70d2a97ca8cb28ba92df426eca7d70aecf4b574e # Parent 61da49321754b976d610e2e59db58f8e0f427f40# Parent c0986ae1b9fc9741015573409620d3faa461c172 integration mainline->openssl-3.x diff -r 61da49321754 -r 70d2a97ca8cb NEWS --- 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) ========================== diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake --- 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() diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake --- 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") ##################################################################### diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/Cache/MemoryObjectCache.h --- 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: diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/Cache/MemoryStringCache.cpp --- 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(buffer), size)); + } + void MemoryStringCache::Invalidate(const std::string &key) { cache_.Invalidate(key); diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/Cache/MemoryStringCache.h --- 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, diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/Compression/ZipReader.cpp --- 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)"); } } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/DicomFormat/DicomMap.cpp --- 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(value[j].asInt()); + s += boost::lexical_cast(value[j].asInt64()); break; case Json::uintValue: - s += boost::lexical_cast(value[j].asUInt()); + s += boost::lexical_cast(value[j].asUInt64()); break; case Json::realValue: diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/DicomNetworking/DicomServer.cpp --- 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; + } + } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/DicomNetworking/DicomServer.h --- 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); + }; } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h --- 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; }; } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp --- 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) { diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/DicomParsing/DicomModification.cpp --- 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_)); + } } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/DicomParsing/DicomModification.h --- 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; }; } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/FileStorage/StorageAccessor.cpp --- 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 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 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 buffer(area_.Read(info.GetUuid(), info.GetContentType())); buffer->MoveToString(sender.GetBuffer()); + + cache_.Add(info.GetUuid(), info.GetContentType(), sender.GetBuffer()); } sender.SetContentType(mime); diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/FileStorage/StorageAccessor.h --- 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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/FileStorage/StorageCache.cpp --- /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 + * . + **/ + + +#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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/FileStorage/StorageCache.h --- /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 + * . + **/ + + +#pragma once + +#include "../Cache/MemoryStringCache.h" + +#include "../Compatibility.h" // For ORTHANC_OVERRIDE + +#include +#include + +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); + + }; +} diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/JobsEngine/IJob.h --- 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; }; } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp --- 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 { diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/JobsEngine/JobsRegistry.h --- 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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp --- 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; diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h --- 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(); diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp --- 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; diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h --- 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; }; } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/Logging.h --- 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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/Lua/LuaFunctionCall.cpp --- 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(lua_tointeger(context_.lua_, top)); + } + else + { + throw OrthancException(ErrorCode_LuaReturnsNoString); + } + } void LuaFunctionCall::PushStringMap(const std::map& value) { diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/Lua/LuaFunctionCall.h --- 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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/MultiThreading/Semaphore.cpp --- 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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/MultiThreading/Semaphore.h --- 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 { diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/RestApi/RestApiOutput.cpp --- 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); + } } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/RestApi/RestApiOutput.h --- 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, diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/SQLite/Transaction.h --- 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; }; } } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/StringMemoryBuffer.cpp --- 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 result(new StringMemoryBuffer); + result->Copy(buffer, start, end); + return result.release(); + } } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/Sources/StringMemoryBuffer.h --- 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 */); }; } diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/UnitTestsSources/FileStorageTests.cpp --- 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"; diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- 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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancFramework/UnitTestsSources/JobsTests.cpp --- 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; diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/OrthancExplorer/explorer.js --- 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() ]) } diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Plugins/Engine/OrthancPlugins.cpp --- 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 #include #include +#include #define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc API is necessary" @@ -1153,6 +1154,7 @@ typedef std::list IncomingHttpRequestFilters; typedef std::list IncomingHttpRequestFilters2; typedef std::list IncomingDicomInstanceFilters; + typedef std::list IncomingCStoreInstanceFilters; typedef std::list DecodeImageCallbacks; typedef std::list TranscoderCallbacks; typedef std::list 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 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(&wrapped)); + + if (filterResult >= 0 && filterResult <= 0xFFFF) + { + return static_cast(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(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; diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Plugins/Engine/OrthancPlugins.h --- 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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Plugins/Engine/PluginsJob.h --- 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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- 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, ¶ms); + } + + /** * @brief Get the transfer syntax of a DICOM file. * * This function returns a pointer to a newly created string that diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Resources/Configuration.json --- 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 } diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp --- 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; diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- 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; } } diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/IServerListener.h --- 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; + }; } diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/LuaScripting.cpp --- 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 + #include @@ -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(result); + } + + return STATUS_Success; + } + void LuaScripting::Execute(const std::string& command) { diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/LuaScripting.h --- 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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/OrthancGetRequestHandler.cpp --- 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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/OrthancMoveRequestHandler.cpp --- 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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- 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); } } diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp --- 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); } } diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp --- 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 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 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 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 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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- 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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp --- 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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/OrthancWebDav.cpp --- 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 << ")"; diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/Search/ISqlLookupFormatter.cpp --- 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(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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/Search/ISqlLookupFormatter.h --- 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& lookup, diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/ServerContext.cpp --- 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 +#include 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 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 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 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 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 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(s); + StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); + std::unique_ptr 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; } diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/ServerContext.h --- 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 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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/ServerJobs/ArchiveJob.cpp --- 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 > availableInstances_; + boost::mutex availableInstancesMutex_; + SharedMessageQueue instancesToPreload_; + std::vector 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(dynamic_cast(that->instancesToPreload_.Dequeue(0))); + if (instanceId.get() == NULL) // that's the signal to exit the thread + { + return; + } + + try + { + boost::shared_ptr 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(); + 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 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 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 zip_; std::unique_ptr 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(accessor.GetItem()); f.GetFile().Read(output); mime = MimeType_Zip; + filename = "archive.zip"; return true; } else diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/ServerJobs/ArchiveJob.h --- 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 synchronousTarget_; // Only valid before "Start()" std::unique_ptr asynchronousTarget_; ServerContext& context_; + std::unique_ptr instanceLoader_; boost::shared_ptr 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; }; } diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp --- 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; diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp --- 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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp --- 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; diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/ServerToolbox.cpp --- 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); diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/Sources/main.cpp --- 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 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); + } } { diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/UnitTestsSources/PluginsTests.cpp --- 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 +#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 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 diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/UnitTestsSources/ServerIndexTests.cpp --- 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 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 attachments; diff -r 61da49321754 -r 70d2a97ca8cb OrthancServer/UnitTestsSources/ServerJobsTests.cpp --- 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 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); } }; } diff -r 61da49321754 -r 70d2a97ca8cb TODO --- 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