Mercurial > hg > orthanc
changeset 6214:e64c3ae969e4 sql-opti
merged default -> sql-opti
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Fri, 27 Jun 2025 15:00:33 +0200 |
parents | 0290cf80dd93 (current diff) 3a974dbf4740 (diff) |
children | 02c3f861b6e6 |
files | OrthancServer/Sources/ServerEnumerations.h |
diffstat | 89 files changed, 6587 insertions(+), 1308 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Fri Jun 27 14:59:41 2025 +0200 +++ b/.hgignore Fri Jun 27 15:00:33 2025 +0200 @@ -15,3 +15,4 @@ .project Resources/Testing/Issue32/Java/bin Resources/Testing/Issue32/Java/target +build/
--- a/CITATION.cff Fri Jun 27 14:59:41 2025 +0200 +++ b/CITATION.cff Fri Jun 27 15:00:33 2025 +0200 @@ -10,5 +10,5 @@ doi: "10.1007/s10278-018-0082-y" license: "GPL-3.0-or-later" repository-code: "https://orthanc.uclouvain.be/hg/orthanc/" -version: 1.12.7 -date-released: 2025-04-07 +version: 1.12.8 +date-released: 2025-06-13
--- a/NEWS Fri Jun 27 14:59:41 2025 +0200 +++ b/NEWS Fri Jun 27 15:00:33 2025 +0200 @@ -1,34 +1,87 @@ Pending changes in the mainline =============================== +General +------- + +* Lua: new "SetStableStatus" function. + + +Plugin SDK +---------- + +* Added new function OrthancPluginSetStableStatus to e.g force the + stabilization of a study from a plugin. + + +Plugins +------- + +* Housekeeper plugin: + - new "ForceReconstructFiles": If "Force" is set to true, forces + the "ReconstructFiles" option when reconstructing resources even + if the plugin did not detect any changes in the configuration that + should trigger a Reconstruct. + + + +Version 1.12.8 (2025-06-13) +=========================== + +General +------- + +* The default SQLite database engine now supports metadata and attachment revisions. + +REST API +-------- + +* API version upgraded to 29 +* If the database backend provides the "HasExtendedFind" primitive, the + value "IsProtected" can be included in the "ResponseContent" field of + "/tools/find" to request the "IsProtected" status of patient resources. + +Plugin SDK +---------- + +* Added new functions (available to all plugins) to access key-value + stores and queues stored as a part of the Orthanc database. +* New SDK to create storage area plugins (V3) that associate custom data with + attachments. The built-in SQLite database engine supports such custom data. +* New SDK to handle custom data for attachments, key-value stores, and queues + by custom database backends (cf. "OrthancDatabasePlugin.proto"). +* Added OrthancPluginAdoptDicomInstance() to adopt DICOM instances stored elsewhere + than in the storage area (to be used by "orthanc-advanced-storage" plugin). + +Plugins +------- + +* New sample plugins: "CppSkeleton" and "AdoptDicomInstance" +* Housekeeper plugin: + - If "LimitMainDicomTagsReconstructLevel" was set, files were not transcoded + if they had to. The "LimitMainDicomTagsReconstructLevel" configuration is now + ignored when a full processing is required. +* Delayed Deletion plugin: + - Added an index in the delayed-deletion SQLite external DB to speed up delayed + deletions. This new index will only apply to new databases. If you want to speed + up an existing installation, run "CREATE INDEX PendingIndex ON Pending(uuid)" + manually in the plugin SQLite DB. With this patch, we observed a 100 fold + performance improvement when the "Pending" table contains 1-2 millions files. + Contribution by Yurii (George) from ivtech.dev. + Maintenance ----------- -* In verbose logs, added the elapsed time spent in each HTTP call. -* Housekeeper plugin: - - If "LimitMainDicomTagsReconstructLevel" was set, files were not transcoded if they had to. - The "LimitMainDicomTagsReconstructLevel" configuration is now ignored when a full processing - is required. -* Delayed Deletion plugin: - - Added an index in the delayed-deletion SQLite external DB to speed up delayed deletions. - This new index will only apply to new databases. If you wish to speed up an existing installation, - run "CREATE INDEX PendingIndex ON Pending(uuid)" manually in the plugin SQLite DB. - Patch provided by Yurii (George) from ivtech.dev. - With this patch, we observed a 100 fold performance improvement when the - "Pending" table contains 1-2 millions files. - +* In verbose logs, the elapsed time spent in each HTTP call is now reported. +* Fix computation of MD5 hashes for memory buffers whose size is larger than 2^31 bytes. * Configuration options "RejectSopClasses" and "RejectedSopClasses" are taken as synonyms. In Orthanc 1.12.6 and 1.12.7, "RejectSopClasses" was used instead of the expected "RejectedSopClasses" spelling. - - -REST API --------- - -* If the index database provides the "HasExtendedFind" primitive, the "ResponseContent" option in - "/tools/find" now allows to specify "IsProtected" to retrieve the "IsProtected" status of a - patient resource. - +* Fix the re-encoding of DICOM files larger than 4GB. +* Improved translations of HTTP error codes when a plugin calls the core REST API. + In particular, a plugin could receive an error OrthancPluginErrorCode_UnknownResource (code 17) + when the underlying REST handler was actually returning an HTTP error 415. The plugin will + now receive an error OrthancPluginErrorCode_UnsupportedMediaType (code 3000). Version 1.12.7 (2025-04-07)
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Fri Jun 27 15:00:33 2025 +0200 @@ -171,6 +171,8 @@ set(ORTHANC_FRAMEWORK_MD5 "0e971f32f4f3e4951e0f3b5de49a3da6") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.7") set(ORTHANC_FRAMEWORK_MD5 "f27c27d7a7a694dab1fd7f0a99d9715a") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.8") + set(ORTHANC_FRAMEWORK_MD5 "eb1c719234338e8277b80d3453563e9f") # Below this point are development snapshots that were used to # release some plugin, before an official release of the Orthanc @@ -515,7 +517,6 @@ include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake) include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake) include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake) - set(EMBED_RESOURCES_PYTHON ${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py) if (ORTHANC_FRAMEWORK_USE_SHARED) list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri Jun 27 15:00:33 2025 +0200 @@ -170,6 +170,7 @@ ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Enumerations.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FileInfo.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/MemoryStorageArea.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/PluginStorageAreaAdapter.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/CStringMatcher.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpContentNegociation.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpToolbox.cpp
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Fri Jun 27 15:00:33 2025 +0200 @@ -39,7 +39,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 "28") +set(ORTHANC_API_VERSION "29") #####################################################################
--- a/OrthancFramework/Sources/ChunkedBuffer.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/ChunkedBuffer.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -25,6 +25,8 @@ #include "PrecompiledHeaders.h" #include "ChunkedBuffer.h" +#include "OrthancException.h" + #include <cassert> #include <string.h> @@ -54,7 +56,16 @@ else { assert(chunkData != NULL); - chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize)); + + try + { + chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize)); + } + catch (...) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + numBytes_ += chunkSize; } } @@ -172,24 +183,59 @@ void ChunkedBuffer::Flatten(std::string& result) { FlushPendingBuffer(); - result.resize(numBytes_); - size_t pos = 0; - for (Chunks::iterator it = chunks_.begin(); - it != chunks_.end(); ++it) + if (chunks_.empty()) { - assert(*it != NULL); - - size_t s = (*it)->size(); - if (s != 0) + if (numBytes_ != 0) { - memcpy(&result[pos], (*it)->c_str(), s); - pos += s; + throw OrthancException(ErrorCode_InternalError); } - delete *it; + result.clear(); + } + else if (chunks_.size() == 1) + { + // Avoid reallocating a buffer if there is a single chunk + assert(chunks_.front() != NULL); + if (chunks_.front()->size() != numBytes_) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + chunks_.front()->swap(result); + delete chunks_.front(); + } + } + else + { + try + { + result.resize(numBytes_); + } + catch (...) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + size_t pos = 0; + for (Chunks::iterator it = chunks_.begin(); + it != chunks_.end(); ++it) + { + assert(*it != NULL); + + size_t s = (*it)->size(); + if (s != 0) + { + memcpy(&result[pos], (*it)->c_str(), s); + pos += s; + } + + delete *it; + } } + // Reset the data structure chunks_.clear(); numBytes_ = 0; }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -31,7 +31,6 @@ #include "../Compatibility.h" #include "../Endianness.h" -#include "../Logging.h" #include "../OrthancException.h" #include "../Toolbox.h" #include "DicomArray.h" @@ -44,6 +43,7 @@ #if !defined(__EMSCRIPTEN__) // Multithreading is not supported in WebAssembly # include <boost/thread/shared_mutex.hpp> +# include <boost/thread/lock_types.hpp> // For boost::unique_lock<> and boost::shared_lock<> #endif namespace Orthanc @@ -1220,7 +1220,7 @@ } - void DicomMap::LogMissingTagsForStore() const + std::string DicomMap::FormatMissingTagsForStore() const { std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid; @@ -1244,14 +1244,14 @@ sopInstanceUid = ValueAsString(*this, DICOM_TAG_SOP_INSTANCE_UID); } - LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + return FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); } - void DicomMap::LogMissingTagsForStore(const std::string& patientId, - const std::string& studyInstanceUid, - const std::string& seriesInstanceUid, - const std::string& sopInstanceUid) + std::string DicomMap::FormatMissingTagsForStore(const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid) { std::string s, t; @@ -1309,11 +1309,11 @@ if (t.size() == 0) { - LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)"; + return "Store has failed because all the required tags (" + s + ") are missing (is it a DICOMDIR file?)"; } else { - LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t; + return "Store has failed because required tags (" + s + ") are missing for the following instance: " + t; } }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Fri Jun 27 15:00:33 2025 +0200 @@ -171,12 +171,12 @@ const void* dicom, size_t size); - void LogMissingTagsForStore() const; + std::string FormatMissingTagsForStore() const; - static void LogMissingTagsForStore(const std::string& patientId, - const std::string& studyInstanceUid, - const std::string& seriesInstanceUid, - const std::string& sopInstanceUid); + static std::string FormatMissingTagsForStore(const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid); bool LookupStringValue(std::string& result, const DicomTag& tag,
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -192,7 +192,7 @@ if (e.GetErrorCode() == ErrorCode_InexistentTag) { - FromDcmtkBridge::LogMissingTagsForStore(**imageDataSet); + LOG(ERROR) << FromDcmtkBridge::FormatMissingTagsForStore(**imageDataSet); } else {
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -38,6 +38,7 @@ #include "FromDcmtkBridge.h" #include "ToDcmtkBridge.h" +#include "../ChunkedBuffer.h" #include "../Compatibility.h" #include "../Logging.h" #include "../Toolbox.h" @@ -57,7 +58,6 @@ #include <dcmtk/dcmdata/dcdeftag.h> #include <dcmtk/dcmdata/dcdicent.h> -#include <dcmtk/dcmdata/dcdict.h> #include <dcmtk/dcmdata/dcfilefo.h> #include <dcmtk/dcmdata/dcistrmb.h> #include <dcmtk/dcmdata/dcostrmb.h> @@ -126,6 +126,38 @@ namespace Orthanc { + FromDcmtkBridge::DictionaryWriterLock::DictionaryWriterLock() : + dictionary_(dcmDataDict.wrlock()) + { + } + + + FromDcmtkBridge::DictionaryWriterLock::~DictionaryWriterLock() + { +#if DCMTK_VERSION_NUMBER >= 364 + dcmDataDict.wrunlock(); +#else + dcmDataDict.unlock(); +#endif + } + + + FromDcmtkBridge::DictionaryReaderLock::DictionaryReaderLock() : + dictionary_(dcmDataDict.rdlock()) + { + } + + + FromDcmtkBridge::DictionaryReaderLock::~DictionaryReaderLock() + { +#if DCMTK_VERSION_NUMBER >= 364 + dcmDataDict.rdunlock(); +#else + dcmDataDict.unlock(); +#endif + } + + static bool IsBinaryTag(const DcmTag& key) { return (key.isUnknownVR() || @@ -167,37 +199,73 @@ namespace { - class DictionaryLocker : public boost::noncopyable + class ChunkedBufferStream : public DcmOutputStream { private: - DcmDataDictionary& dictionary_; + class Consumer : public DcmConsumer + { + private: + ChunkedBuffer buffer_; + + public: + void Flatten(std::string& buffer) + { + buffer_.Flatten(buffer); + } + + OFBool good() const ORTHANC_OVERRIDE + { + return true; + } + + OFCondition status() const ORTHANC_OVERRIDE + { + return EC_Normal; + } + + OFBool isFlushed() const ORTHANC_OVERRIDE + { + return true; + } + + offile_off_t avail() const ORTHANC_OVERRIDE + { + // since we cannot report "unlimited", let's claim that we can still write 10MB. + // Note that offile_off_t is a signed type. + return 10 * 1024 * 1024; + } + + offile_off_t write(const void *buf, + offile_off_t buflen) ORTHANC_OVERRIDE + { + buffer_.AddChunk(buf, buflen); + return buflen; + } + + void flush() ORTHANC_OVERRIDE + { + // Nothing to flush + } + }; + + Consumer consumer_; public: - DictionaryLocker() : dictionary_(dcmDataDict.wrlock()) + ChunkedBufferStream() : + DcmOutputStream(&consumer_) { } - ~DictionaryLocker() + void Flatten(std::string& buffer) { -#if DCMTK_VERSION_NUMBER >= 364 - dcmDataDict.wrunlock(); -#else - dcmDataDict.unlock(); -#endif - } - - DcmDataDictionary& operator*() - { - return dictionary_; - } - - DcmDataDictionary* operator->() - { - return &dictionary_; + consumer_.Flatten(buffer); } }; - - + } + + + namespace + { ORTHANC_FORCE_INLINE static std::string FloatToString(float v) { @@ -296,9 +364,9 @@ #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 { - DictionaryLocker locker; - - locker->clear(); + DictionaryWriterLock lock; + + lock.GetDictionary().clear(); CLOG(INFO, DICOM) << "Loading the embedded dictionaries"; /** @@ -306,14 +374,14 @@ * command "strace storescu 2>&1 |grep dic" shows that DICONDE * dictionary is not loaded by storescu. **/ - //LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_DICONDE); - - LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_DICOM); + //LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICONDE); + + LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICOM); if (loadPrivateDictionary) { CLOG(INFO, DICOM) << "Loading the embedded dictionary of private tags"; - LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_PRIVATE); + LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_PRIVATE); } else { @@ -373,16 +441,16 @@ void FromDcmtkBridge::LoadExternalDictionaries(const std::vector<std::string>& dictionaries) { - DictionaryLocker locker; + DictionaryWriterLock lock; CLOG(INFO, DICOM) << "Clearing the DICOM dictionary"; - locker->clear(); + lock.GetDictionary().clear(); for (size_t i = 0; i < dictionaries.size(); i++) { LOG(WARNING) << "Loading external DICOM dictionary: \"" << dictionaries[i] << "\""; - if (!locker->loadDictionary(dictionaries[i].c_str())) + if (!lock.GetDictionary().loadDictionary(dictionaries[i].c_str())) { throw OrthancException(ErrorCode_InexistentFile); } @@ -475,10 +543,10 @@ entry->setElementRangeRestriction(DcmDictRange_Unspecified); { - DictionaryLocker locker; - - if (locker->findEntry(DcmTagKey(tag.GetGroup(), tag.GetElement()), - privateCreator.empty() ? NULL : privateCreator.c_str())) + DictionaryWriterLock lock; + + if (lock.GetDictionary().findEntry(DcmTagKey(tag.GetGroup(), tag.GetElement()), + privateCreator.empty() ? NULL : privateCreator.c_str())) { throw OrthancException(ErrorCode_AlreadyExistingTag, "Cannot register twice the tag (" + tag.Format() + @@ -486,7 +554,7 @@ } else { - locker->addEntry(entry.release()); + lock.GetDictionary().addEntry(entry.release()); } } } @@ -663,10 +731,11 @@ * syntax (cf. DICOM CP 246). * ftp://medical.nema.org/medical/dicom/final/cp246_ft.pdf **/ - DictionaryLocker locker; - - const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(), - element.getTag().getPrivateCreator()); + DictionaryReaderLock lock; + + // The "entry" value is only valid while "lock" is active + const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(), + element.getTag().getPrivateCreator()); if (entry != NULL && entry->getVR().isaString()) { @@ -1111,8 +1180,8 @@ if (!(flags & DicomToJsonFlags_IncludeUnknownTags)) { - DictionaryLocker locker; - if (locker->findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL) + DictionaryReaderLock lock; + if (lock.GetDictionary().findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL) { continue; } @@ -1572,7 +1641,12 @@ } - +#if 0 + /** + * This was the implementation in Orthanc <= 1.12.7. This version + * uses "DcmFileFormat::calcElementLength()", which cannot handle + * DICOM files whose size cannot be represented on 32 bits. + **/ static bool SaveToMemoryBufferInternal(std::string& buffer, DcmFileFormat& dicom, E_TransferSyntax xfer, @@ -1619,6 +1693,46 @@ return false; } } +#endif + + +#if 1 + /** + * This is the cleaner implementation used in Orthanc >= 1.12.8, + * which allows to write DICOM files larger than 4GB. + **/ + static bool SaveToMemoryBufferInternal(std::string& buffer, + DcmFileFormat& dicom, + E_TransferSyntax xfer, + std::string& errorMessage) + { + ChunkedBufferStream ob; + + // Fill the (chunked) memory buffer with the meta-header and the dataset + dicom.transferInit(); + OFCondition c = dicom.write(ob, xfer, /*opt_sequenceType*/ EET_ExplicitLength, NULL, + /*opt_groupLength*/ EGL_recalcGL, + /*opt_paddingType*/ EPD_noChange, + /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0, + EWM_updateMeta /* creates new SOP instance UID on lossy */); + dicom.transferEnd(); + + if (c.good()) + { + ob.flush(); + ob.Flatten(buffer); + return true; + } + else + { + // Error + buffer.clear(); + errorMessage = std::string(c.text()); + return false; + } + } +#endif + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, DcmDataset& dataSet) @@ -2646,10 +2760,11 @@ if (evr == EVR_UN) { // New in Orthanc 1.9.5 - DictionaryLocker locker; - - const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(), - element.getTag().getPrivateCreator()); + FromDcmtkBridge::DictionaryReaderLock lock; + + // The "entry" value is only valid while "lock" is active + const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(), + element.getTag().getPrivateCreator()); if (entry != NULL) { @@ -3162,7 +3277,7 @@ } - void FromDcmtkBridge::LogMissingTagsForStore(DcmDataset& dicom) + std::string FromDcmtkBridge::FormatMissingTagsForStore(DcmDataset& dicom) { std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid; @@ -3194,7 +3309,7 @@ sopInstanceUid.assign(c); } - DicomMap::LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + return DicomMap::FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); }
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Fri Jun 27 15:00:33 2025 +0200 @@ -30,9 +30,10 @@ #include "../DicomFormat/DicomPath.h" #include <dcmtk/dcmdata/dcdatset.h> +#include <dcmtk/dcmdata/dcdict.h> +#include <dcmtk/dcmdata/dcfilefo.h> #include <dcmtk/dcmdata/dcmetinf.h> #include <dcmtk/dcmdata/dcpixseq.h> -#include <dcmtk/dcmdata/dcfilefo.h> #include <json/value.h> #if ORTHANC_ENABLE_DCMTK != 1 @@ -86,6 +87,40 @@ }; + class ORTHANC_PUBLIC DictionaryWriterLock : public boost::noncopyable + { + private: + DcmDataDictionary& dictionary_; + + public: + DictionaryWriterLock(); + + ~DictionaryWriterLock(); + + DcmDataDictionary& GetDictionary() + { + return dictionary_; + } + }; + + + class ORTHANC_PUBLIC DictionaryReaderLock : public boost::noncopyable + { + private: + const DcmDataDictionary& dictionary_; + + public: + DictionaryReaderLock(); + + ~DictionaryReaderLock(); + + const DcmDataDictionary& GetDictionary() const + { + return dictionary_; + } + }; + + private: FromDcmtkBridge(); // Pure static class @@ -280,7 +315,7 @@ static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target, DcmDataset& dicom); - static void LogMissingTagsForStore(DcmDataset& dicom); + static std::string FormatMissingTagsForStore(DcmDataset& dicom); static void RemovePath(DcmDataset& dataset, const DicomPath& path);
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -169,4 +169,56 @@ throw OrthancException(ErrorCode_BadSequenceOfCalls); } } + + void FileInfo::SetCustomData(const void* data, + size_t size) + { + if (valid_) + { + customData_.assign(reinterpret_cast<const char*>(data), size); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FileInfo::SetCustomData(const std::string& data) + { + if (valid_) + { + customData_ = data; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FileInfo::SwapCustomData(std::string& data) + { + if (valid_) + { + customData_.swap(data); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const std::string& FileInfo::GetCustomData() const + { + if (valid_) + { + return customData_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } }
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.h Fri Jun 27 15:00:33 2025 +0200 @@ -42,6 +42,7 @@ CompressionType compressionType_; uint64_t compressedSize_; std::string compressedMD5_; + std::string customData_; public: FileInfo(); @@ -80,5 +81,14 @@ const std::string& GetCompressedMD5() const; const std::string& GetUncompressedMD5() const; + + void SetCustomData(const void* data, + size_t size); + + void SetCustomData(const std::string& data); + + void SwapCustomData(std::string& data); + + const std::string& GetCustomData() const; }; }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -187,8 +187,8 @@ } - IMemoryBuffer* FilesystemStorage::Read(const std::string& uuid, - FileContentType type) + IMemoryBuffer* FilesystemStorage::ReadWhole(const std::string& uuid, + FileContentType type) { Toolbox::ElapsedTimer timer; LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) @@ -221,12 +221,6 @@ } - bool FilesystemStorage::HasReadRange() const - { - return true; - } - - uintmax_t FilesystemStorage::GetSize(const std::string& uuid) const { boost::filesystem::path path = GetPath(uuid); @@ -354,7 +348,7 @@ const std::string& uuid, FileContentType type) { - std::unique_ptr<IMemoryBuffer> buffer(Read(uuid, type)); + std::unique_ptr<IMemoryBuffer> buffer(ReadWhole(uuid, type)); buffer->MoveToString(content); } #endif
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Fri Jun 27 15:00:33 2025 +0200 @@ -80,15 +80,19 @@ size_t size, FileContentType type) ORTHANC_OVERRIDE; - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE; + // This flavor is only used in the "DelayedDeletion" plugin + IMemoryBuffer* ReadWhole(const std::string& uuid, + FileContentType type); virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) ORTHANC_OVERRIDE; - virtual bool HasReadRange() const ORTHANC_OVERRIDE; + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return true; + } virtual void Remove(const std::string& uuid, FileContentType type) ORTHANC_OVERRIDE;
--- a/OrthancFramework/Sources/FileStorage/IStorageArea.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h Fri Jun 27 15:00:33 2025 +0200 @@ -24,8 +24,9 @@ #pragma once +#include "../Compatibility.h" +#include "../Enumerations.h" #include "../IMemoryBuffer.h" -#include "../Enumerations.h" #include <stdint.h> #include <string> @@ -33,6 +34,8 @@ namespace Orthanc { + class DicomInstanceToStore; + class IStorageArea : public boost::noncopyable { public: @@ -45,17 +48,44 @@ size_t size, FileContentType type) = 0; - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) = 0; - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) = 0; - virtual bool HasReadRange() const = 0; + virtual bool HasEfficientReadRange() const = 0; virtual void Remove(const std::string& uuid, FileContentType type) = 0; }; + + + // storage area with customData (customData are used only in plugins) + class IPluginStorageArea : public boost::noncopyable + { + public: + virtual ~IPluginStorageArea() + { + } + + virtual void Create(std::string& customData /* out */, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance /* can be NULL if not a DICOM instance */) = 0; + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) = 0; + + virtual bool HasEfficientReadRange() const = 0; + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) = 0; + }; }
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -69,31 +69,6 @@ } - IMemoryBuffer* MemoryStorageArea::Read(const std::string& uuid, - FileContentType type) - { - LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" - << static_cast<int>(type) << "\" content type"; - - Mutex::ScopedLock lock(mutex_); - - Content::const_iterator found = content_.find(uuid); - - if (found == content_.end()) - { - throw OrthancException(ErrorCode_InexistentFile); - } - else if (found->second == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - else - { - return StringMemoryBuffer::CreateFromCopy(*found->second); - } - } - - IMemoryBuffer* MemoryStorageArea::ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, @@ -149,12 +124,6 @@ } - bool MemoryStorageArea::HasReadRange() const - { - return true; - } - - void MemoryStorageArea::Remove(const std::string& uuid, FileContentType type) {
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Fri Jun 27 15:00:33 2025 +0200 @@ -49,15 +49,15 @@ size_t size, FileContentType type) ORTHANC_OVERRIDE; - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE; - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) ORTHANC_OVERRIDE; - virtual bool HasReadRange() const ORTHANC_OVERRIDE; + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return true; + } virtual void Remove(const std::string& uuid, FileContentType type) ORTHANC_OVERRIDE;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeaders.h" +#include "PluginStorageAreaAdapter.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + PluginStorageAreaAdapter::PluginStorageAreaAdapter(IStorageArea* storage) : + storage_(storage) + { + if (storage == NULL) + { + throw OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + + void PluginStorageAreaAdapter::Create(std::string& customData, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance) + { + customData.clear(); + storage_->Create(uuid, content, size, type); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,69 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "IStorageArea.h" + + +namespace Orthanc +{ + class ORTHANC_PUBLIC PluginStorageAreaAdapter : public IPluginStorageArea + { + private: + std::unique_ptr<IStorageArea> storage_; + + public: + explicit PluginStorageAreaAdapter(IStorageArea* storage /* takes ownership */); + + virtual void Create(std::string& customData, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance) ORTHANC_OVERRIDE; + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) ORTHANC_OVERRIDE + { + return storage_->ReadRange(uuid, type, start, end); + } + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + storage_->Remove(uuid, type); + } + + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return storage_->HasEfficientReadRange(); + } + }; +}
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -275,7 +275,7 @@ }; - StorageAccessor::StorageAccessor(IStorageArea& area) : + StorageAccessor::StorageAccessor(IPluginStorageArea& area) : area_(area), cache_(NULL), metrics_(NULL) @@ -283,7 +283,7 @@ } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, StorageCache& cache) : area_(area), cache_(&cache), @@ -292,7 +292,7 @@ } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, MetricsRegistry& metrics) : area_(area), cache_(NULL), @@ -300,7 +300,7 @@ { } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, StorageCache& cache, MetricsRegistry& metrics) : area_(area), @@ -310,13 +310,15 @@ } - FileInfo StorageAccessor::Write(const void* data, - size_t size, - FileContentType type, - CompressionType compression, - bool storeMd5) + void StorageAccessor::Write(FileInfo& info, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const DicomInstanceToStore* instance) { - std::string uuid = Toolbox::GenerateUuid(); + const std::string uuid = Toolbox::GenerateUuid(); std::string md5; @@ -325,13 +327,15 @@ Toolbox::ComputeMD5(md5, data, size); } + std::string customData; + switch (compression) { case CompressionType_None: { { MetricsTimer timer(*this, METRICS_CREATE_DURATION); - area_.Create(uuid, data, size, type); + area_.Create(customData, uuid, data, size, type, compression, instance); } if (metrics_ != NULL) @@ -345,7 +349,9 @@ cacheAccessor.Add(uuid, type, data, size); } - return FileInfo(uuid, type, size, md5); + info = FileInfo(uuid, type, size, md5); + info.SetCustomData(customData); + return; } case CompressionType_ZlibWithSize: @@ -367,11 +373,11 @@ if (compressed.size() > 0) { - area_.Create(uuid, &compressed[0], compressed.size(), type); + area_.Create(customData, uuid, &compressed[0], compressed.size(), type, compression, instance); } else { - area_.Create(uuid, NULL, 0, type); + area_.Create(customData, uuid, NULL, 0, type, compression, instance); } } @@ -386,8 +392,10 @@ cacheAccessor.Add(uuid, type, data, size); // always add uncompressed data to cache } - return FileInfo(uuid, type, size, md5, + info = FileInfo(uuid, type, size, md5, CompressionType_ZlibWithSize, compressed.size(), compressedMD5); + info.SetCustomData(customData); + return; } default: @@ -395,16 +403,6 @@ } } - FileInfo StorageAccessor::Write(const std::string &data, - FileContentType type, - CompressionType compression, - bool storeMd5) - { - return Write((data.size() == 0 ? NULL : data.c_str()), - data.size(), type, compression, storeMd5); - } - - void StorageAccessor::Read(std::string& content, const FileInfo& info) { @@ -446,7 +444,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } if (metrics_ != NULL) @@ -467,7 +465,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - compressed.reset(area_.Read(info.GetUuid(), info.GetContentType())); + compressed.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } if (metrics_ != NULL) @@ -526,7 +524,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } if (metrics_ != NULL) @@ -539,7 +537,8 @@ void StorageAccessor::Remove(const std::string& fileUuid, - FileContentType type) + FileContentType type, + const std::string& customData) { if (cache_ != NULL) { @@ -548,14 +547,14 @@ { MetricsTimer timer(*this, METRICS_REMOVE_DURATION); - area_.Remove(fileUuid, type); + area_.Remove(fileUuid, type, customData); } } void StorageAccessor::Remove(const FileInfo &info) { - Remove(info.GetUuid(), info.GetContentType()); + Remove(info.GetUuid(), info.GetContentType(), info.GetCustomData()); } @@ -616,7 +615,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end)); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end, info.GetCustomData())); assert(buffer->GetSize() == end); } @@ -682,19 +681,19 @@ if (range.HasStart() && range.HasEnd()) { - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1)); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1, info.GetCustomData())); } else if (range.HasStart()) { - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize(), info.GetCustomData())); } else if (range.HasEnd()) { - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1)); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1, info.GetCustomData())); } else { - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } buffer->MoveToString(target); @@ -785,4 +784,5 @@ output.AnswerStream(transcoder); } #endif + }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Fri Jun 27 15:00:33 2025 +0200 @@ -110,7 +110,7 @@ private: class MetricsTimer; - IStorageArea& area_; + IPluginStorageArea& area_; StorageCache* cache_; MetricsRegistry* metrics_; @@ -121,28 +121,25 @@ #endif public: - explicit StorageAccessor(IStorageArea& area); + explicit StorageAccessor(IPluginStorageArea& area); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, StorageCache& cache); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, MetricsRegistry& metrics); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, StorageCache& cache, MetricsRegistry& metrics); - FileInfo Write(const void* data, - size_t size, - FileContentType type, - CompressionType compression, - bool storeMd5); - - FileInfo Write(const std::string& data, - FileContentType type, - CompressionType compression, - bool storeMd5); + void Write(FileInfo& info /* out */, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const DicomInstanceToStore* instance); void Read(std::string& content, const FileInfo& info); @@ -155,7 +152,8 @@ uint64_t end /* exclusive */); void Remove(const std::string& fileUuid, - FileContentType type); + FileContentType type, + const std::string& customData); void Remove(const FileInfo& info); @@ -185,6 +183,7 @@ const std::string& mime, const std::string& contentFilename); #endif + private: void ReadStartRangeInternal(std::string& target, const FileInfo& info,
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -51,7 +51,10 @@ if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, httpHeaders, getArguments, NULL /* no body for GET */, 0)) { - stream.GetBody(answerBody); + if (stream.GetStatus() == HttpStatus_200_Ok) + { + stream.GetBody(answerBody); + } if (answerHeaders != NULL) {
--- a/OrthancFramework/Sources/SQLite/Statement.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/SQLite/Statement.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -236,10 +236,22 @@ BindString(col, UTF16ToUTF8(value)); }*/ - void Statement::BindBlob(int col, const void* val, int val_len) + void Statement::BindBlob(int col, const void* val, size_t val_len) { - CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, val_len, SQLITE_TRANSIENT), - ErrorCode_BadParameterType); + if (static_cast<size_t>(static_cast<int>(val_len)) != val_len) + { + throw OrthancSQLiteException(ErrorCode_SQLiteBindOutOfRange); + } + else + { + CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, static_cast<int>(val_len), SQLITE_TRANSIENT), + ErrorCode_BadParameterType); + } + } + + void Statement::BindBlob(int col, const std::string& value) + { + BindBlob(col, value.empty() ? NULL : value.c_str(), value.size()); }
--- a/OrthancFramework/Sources/SQLite/Statement.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/SQLite/Statement.h Fri Jun 27 15:00:33 2025 +0200 @@ -130,7 +130,8 @@ void BindCString(int col, const char* val); void BindString(int col, const std::string& val); //void BindString16(int col, const string16& value); - void BindBlob(int col, const void* value, int value_len); + void BindBlob(int col, const void* value, size_t value_len); + void BindBlob(int col, const std::string& value); // Retrieving ----------------------------------------------------------------
--- a/OrthancFramework/Sources/SystemToolbox.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -454,6 +454,64 @@ } } +#if ORTHANC_ENABLE_MD5 == 1 + void SystemToolbox::ComputeStreamMD5(std::string& result, + std::istream& inputStream) + { + Toolbox::MD5Context context; + + const size_t bufferSize = 1024; + char buffer[bufferSize]; + + while (inputStream.good()) + { + inputStream.read(buffer, bufferSize); + std::streamsize bytesRead = inputStream.gcount(); + + if (bytesRead > 0) + { + context.Append(buffer, bytesRead); + } + } + + context.Export(result); + } + + + void SystemToolbox::ComputeFileMD5(std::string& result, + const std::string& path) + { + boost::filesystem::ifstream fileStream; + fileStream.open(path, std::ifstream::in | std::ifstream::binary); + + if (!fileStream.good()) + { + throw OrthancException(ErrorCode_InexistentFile, "File not found: " + path); + } + + ComputeStreamMD5(result, fileStream); + } + + + bool SystemToolbox::CompareFilesMD5(const std::string& path1, + const std::string& path2) + { + if (GetFileSize(path1) != GetFileSize(path2)) + { + return false; + } + else + { + std::string path1md5, path2md5; + + ComputeFileMD5(path1md5, path1); + ComputeFileMD5(path2md5, path2); + + return path1md5 == path2md5; + } + } +#endif + void SystemToolbox::MakeDirectory(const std::string& path) {
--- a/OrthancFramework/Sources/SystemToolbox.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.h Fri Jun 27 15:00:33 2025 +0200 @@ -30,6 +30,10 @@ # error The macro ORTHANC_SANDBOXED must be defined #endif +#if !defined(ORTHANC_ENABLE_MD5) +# error The macro ORTHANC_ENABLE_MD5 must be defined +#endif + #if ORTHANC_SANDBOXED == 1 # error The namespace SystemToolbox cannot be used in sandboxed environments #endif @@ -83,6 +87,18 @@ static uint64_t GetFileSize(const std::string& path); +#if ORTHANC_ENABLE_MD5 == 1 + static void ComputeStreamMD5(std::string& result, + std::istream& stream); + + static void ComputeFileMD5(std::string& result, + const std::string& path); + + // returns true if file have the same MD5 + static bool CompareFilesMD5(const std::string& path1, + const std::string& path2); +#endif + static void MakeDirectory(const std::string& path); static bool IsExistingFile(const std::string& path);
--- a/OrthancFramework/Sources/Toolbox.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -64,6 +64,7 @@ #include <boost/algorithm/string/join.hpp> #include <boost/lexical_cast.hpp> #include <boost/regex.hpp> +#include <cassert> #if BOOST_VERSION >= 106600 # include <boost/uuid/detail/sha1.hpp> @@ -207,6 +208,112 @@ namespace Orthanc { +#if ORTHANC_ENABLE_MD5 == 1 + static char GetHexadecimalCharacter(uint8_t value) + { + assert(value < 16); + + if (value < 10) + { + return value + '0'; + } + else + { + return (value - 10) + 'a'; + } + } + + + struct Toolbox::MD5Context::PImpl + { + md5_state_s state_; + bool done_; + + PImpl() : + done_(false) + { + md5_init(&state_); + } + }; + + + Toolbox::MD5Context::MD5Context() : + pimpl_(new PImpl) + { + } + + + void Toolbox::MD5Context::Append(const void* data, + size_t size) + { + static const size_t MAX_SIZE = 128 * 1024 * 1024; + + if (pimpl_->done_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + const uint8_t *p = reinterpret_cast<const uint8_t*>(data); + + while (size > 0) + { + /** + * The built-in implementation of MD5 requires that "size" can + * be casted to "int", so we feed it by chunks of maximum + * 128MB. This fixes an incorrect behavior in Orthanc <= 1.12.7. + **/ + + int chunkSize; + if (size > MAX_SIZE) + { + chunkSize = static_cast<int>(MAX_SIZE); + } + else + { + chunkSize = static_cast<int>(size); + } + + md5_append(&pimpl_->state_, reinterpret_cast<const md5_byte_t*>(p), chunkSize); + + p += chunkSize; + + assert(static_cast<size_t>(chunkSize) <= size); + size -= chunkSize; + } + } + + + void Toolbox::MD5Context::Append(const std::string& source) + { + if (source.size() > 0) + { + Append(source.c_str(), source.size()); + } + } + + + void Toolbox::MD5Context::Export(std::string& target) + { + if (pimpl_->done_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + pimpl_->done_ = true; + + md5_byte_t actualHash[16]; + md5_finish(&pimpl_->state_, actualHash); + + target.resize(32); + for (unsigned int i = 0; i < 16; i++) + { + target[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16)); + target[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16)); + } + } +#endif /* ORTHANC_ENABLE_MD5 */ + + void Toolbox::LinesIterator::FindEndOfLine() { lineEnd_ = lineStart_; @@ -444,21 +551,6 @@ #if ORTHANC_ENABLE_MD5 == 1 - static char GetHexadecimalCharacter(uint8_t value) - { - assert(value < 16); - - if (value < 10) - { - return value + '0'; - } - else - { - return (value - 10) + 'a'; - } - } - - void Toolbox::ComputeMD5(std::string& result, const std::string& data) { @@ -477,25 +569,9 @@ const void* data, size_t size) { - md5_state_s state; - md5_init(&state); - - if (size > 0) - { - md5_append(&state, - reinterpret_cast<const md5_byte_t*>(data), - static_cast<int>(size)); - } - - md5_byte_t actualHash[16]; - md5_finish(&state, actualHash); - - result.resize(32); - for (unsigned int i = 0; i < 16; i++) - { - result[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16)); - result[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16)); - } + MD5Context context; + context.Append(data, size); + context.Export(result); } void Toolbox::ComputeMD5(std::string& result,
--- a/OrthancFramework/Sources/Toolbox.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.h Fri Jun 27 15:00:33 2025 +0200 @@ -82,6 +82,25 @@ class ORTHANC_PUBLIC Toolbox { public: +#if ORTHANC_ENABLE_MD5 == 1 + class ORTHANC_PUBLIC MD5Context : public boost::noncopyable + { + private: + class PImpl; + boost::shared_ptr<PImpl> pimpl_; + + public: + MD5Context(); + + void Append(const void* data, + size_t size); + + void Append(const std::string& source); + + void Export(std::string& target); + }; +#endif + class ORTHANC_PUBLIC LinesIterator : public boost::noncopyable { private:
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -30,10 +30,9 @@ #include <gtest/gtest.h> #include "../Sources/FileStorage/FilesystemStorage.h" +#include "../Sources/FileStorage/PluginStorageAreaAdapter.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" #include "../Sources/OrthancException.h" #include "../Sources/Toolbox.h" @@ -63,12 +62,18 @@ s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); std::string d; { - std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown)); + std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); buffer->MoveToString(d); } ASSERT_EQ(d.size(), data.size()); ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } } TEST(FilesystemStorage, Basic2) @@ -81,12 +86,18 @@ s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); std::string d; { - std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown)); + std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); buffer->MoveToString(d); } ASSERT_EQ(d.size(), data.size()); ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } } TEST(FilesystemStorage, FileWithSameNameAsTopDirectory) @@ -169,13 +180,14 @@ TEST(StorageAccessor, NoCompression) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true); - + const std::string data = "Hello world"; + FileInfo info; + accessor.Write(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, NULL); + std::string r; accessor.Read(r, info); @@ -191,13 +203,14 @@ TEST(StorageAccessor, Compression) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true); - + const std::string data = "Hello world"; + FileInfo info; + accessor.Write(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, NULL); + std::string r; accessor.Read(r, info); @@ -212,20 +225,22 @@ TEST(StorageAccessor, Mix) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string r; - std::string compressedData = "Hello"; - std::string uncompressedData = "HelloWorld"; + const std::string compressedData = "Hello"; + const std::string uncompressedData = "HelloWorld"; - FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom, CompressionType_ZlibWithSize, false); - FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom, CompressionType_None, false); - + FileInfo compressedInfo; + accessor.Write(compressedInfo, compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, NULL); + + std::string r; accessor.Read(r, compressedInfo); ASSERT_EQ(compressedData, r); + FileInfo uncompressedInfo; + accessor.Write(uncompressedInfo, uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, NULL); accessor.Read(r, uncompressedInfo); ASSERT_EQ(uncompressedData, r); ASSERT_NE(compressedData, r);
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -397,6 +397,25 @@ Toolbox::ComputeMD5(s, set); ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); // set md5 same as string with the values sorted + + { + Toolbox::MD5Context context; + context.Append(""); + context.Append(NULL, 0); + context.Append("Hello"); + context.Export(s); + ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s); + ASSERT_THROW(context.Append("World"), OrthancException); + ASSERT_THROW(context.Export(s), OrthancException); + } + +#if ORTHANC_SANDBOXED != 1 + { + std::istringstream iss(std::string("aaabbbccc")); + SystemToolbox::ComputeStreamMD5(s, iss); + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); + } +#endif } TEST(Toolbox, ComputeSHA1) @@ -1591,6 +1610,47 @@ #endif +#if ORTHANC_SANDBOXED != 1 && ORTHANC_ENABLE_MD5 == 1 +TEST(Toolbox, FileMD5) +{ + { + TemporaryFile tmp1, tmp2; + std::string s = "aaabbbccc"; + + SystemToolbox::WriteFile(s, tmp1.GetPath()); + SystemToolbox::WriteFile(s, tmp2.GetPath()); + + std::string md5; + SystemToolbox::ComputeFileMD5(md5, tmp1.GetPath()); + + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", md5); + ASSERT_TRUE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } + + { // different sizes + TemporaryFile tmp1, tmp2; + std::string s1 = "aaabbbccc"; + std::string s2 = "aaabbbcccd"; + + SystemToolbox::WriteFile(s1, tmp1.GetPath()); + SystemToolbox::WriteFile(s2, tmp2.GetPath()); + + ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } + + { // same sizes, different contents + TemporaryFile tmp1, tmp2; + std::string s1 = "aaabbbccc"; + std::string s2 = "aaabbbccd"; + + SystemToolbox::WriteFile(s1, tmp1.GetPath()); + SystemToolbox::WriteFile(s2, tmp2.GetPath()); + + ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } +} +#endif + #if ORTHANC_SANDBOXED != 1 TEST(Toolbox, GetMacAddressess) {
--- a/OrthancServer/CMakeLists.txt Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/CMakeLists.txt Fri Jun 27 15:00:33 2025 +0200 @@ -203,6 +203,8 @@ ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV3.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV4.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPlugins.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginMemoryBuffer32.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginMemoryBuffer64.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsEnumerations.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsErrorDictionary.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsJob.cpp @@ -243,15 +245,18 @@ ##################################################################### set(ORTHANC_EMBEDDED_FILES - CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json - DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt - FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json - LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua - PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql - UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql - INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql - INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua + PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql + INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql + INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + INSTALL_REVISION_AND_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql + INSTALL_DELETED_FILES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallDeletedFiles.sql + INSTALL_KEY_VALUE_STORES_AND_QUEUES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallKeyValueStoresAndQueues.sql ) if (STANDALONE_BUILD)
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -1448,6 +1448,69 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } }; @@ -1620,7 +1683,7 @@ void OrthancPluginDatabase::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { VoidDatabaseListener listener;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Fri Jun 27 15:00:33 2025 +0200 @@ -103,7 +103,7 @@ virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -677,7 +677,6 @@ } } - virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) ORTHANC_OVERRIDE @@ -1061,6 +1060,69 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } }; @@ -1231,7 +1293,7 @@ void OrthancPluginDatabaseV3::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { VoidDatabaseListener listener;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Fri Jun 27 15:00:33 2025 +0200 @@ -76,7 +76,7 @@ virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -41,7 +41,7 @@ #include "OrthancDatabasePlugin.pb.h" // Auto-generated file #include <cassert> - +#include <limits> namespace Orthanc { @@ -100,15 +100,17 @@ } - static FileInfo Convert(const DatabasePluginMessages::FileInfo& source) + static void Convert(FileInfo& info, + const DatabasePluginMessages::FileInfo& source) { - return FileInfo(source.uuid(), + info = FileInfo(source.uuid(), static_cast<FileContentType>(source.content_type()), source.uncompressed_size(), source.uncompressed_hash(), static_cast<CompressionType>(source.compression_type()), source.compressed_size(), source.compressed_hash()); + info.SetCustomData(source.custom_data()); } @@ -576,6 +578,7 @@ request.mutable_add_attachment()->mutable_attachment()->set_compression_type(attachment.GetCompressionType()); request.mutable_add_attachment()->mutable_attachment()->set_compressed_size(attachment.GetCompressedSize()); request.mutable_add_attachment()->mutable_attachment()->set_compressed_hash(attachment.GetCompressedMD5()); + request.mutable_add_attachment()->mutable_attachment()->set_custom_data(attachment.GetCustomData()); // New in 1.12.8 request.mutable_add_attachment()->set_revision(revision); ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request); @@ -604,7 +607,9 @@ DatabasePluginMessages::TransactionResponse response; ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DELETE_ATTACHMENT, request); - listener_.SignalAttachmentDeleted(Convert(response.delete_attachment().deleted_attachment())); + FileInfo info; + Convert(info, response.delete_attachment().deleted_attachment()); + listener_.SignalAttachmentDeleted(info); } @@ -629,7 +634,9 @@ for (int i = 0; i < response.delete_resource().deleted_attachments().size(); i++) { - listener_.SignalAttachmentDeleted(Convert(response.delete_resource().deleted_attachments(i))); + FileInfo info; + Convert(info, response.delete_resource().deleted_attachments(i)); + listener_.SignalAttachmentDeleted(info); } for (int i = 0; i < response.delete_resource().deleted_resources().size(); i++) @@ -1006,7 +1013,7 @@ if (response.lookup_attachment().found()) { - attachment = Convert(response.lookup_attachment().attachment()); + Convert(attachment, response.lookup_attachment().attachment()); revision = response.lookup_attachment().revision(); return true; } @@ -1016,7 +1023,48 @@ } } - + + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_attachment_custom_data()->set_uuid(attachmentUuid); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ATTACHMENT_CUSTOM_DATA, request); + + customData = response.get_attachment_custom_data().custom_data(); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_set_attachment_custom_data()->set_uuid(attachmentUuid); + request.mutable_set_attachment_custom_data()->set_custom_data(customData, customDataSize); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_SET_ATTACHMENT_CUSTOM_DATA, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) ORTHANC_OVERRIDE @@ -1686,7 +1734,9 @@ for (int j = 0; j < source.attachments().size(); j++) { - target->AddAttachment(Convert(source.attachments(j)), source.attachments_revisions(j)); + FileInfo info; + Convert(info, source.attachments(j)); + target->AddAttachment(info, source.attachments_revisions(j)); } Convert(*target, ResourceType_Patient, source.patient_content()); @@ -1748,7 +1798,8 @@ for (int j = 0; j < source.one_instance_attachments().size(); j++) { - FileInfo info(Convert(source.one_instance_attachments(j))); + FileInfo info; + Convert(info, source.one_instance_attachments(j)); if (attachments.find(info.GetContentType()) == attachments.end()) { attachments[info.GetContentType()] = info; @@ -1805,6 +1856,201 @@ find.ExecuteExpand(response, capabilities, request, identifier); } } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32" + // https://protobuf.dev/programming-guides/proto3/ + if (valueSize > std::numeric_limits<uint32_t>::max()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_store_key_value()->set_store_id(storeId); + request.mutable_store_key_value()->set_key(key); + request.mutable_store_key_value()->set_value(value, valueSize); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_STORE_KEY_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_delete_key_value()->set_store_id(storeId); + request.mutable_delete_key_value()->set_key(key); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_DELETE_KEY_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_key_value()->set_store_id(storeId); + request.mutable_get_key_value()->set_key(key); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_KEY_VALUE, request); + + if (response.get_key_value().found()) + { + value = response.get_key_value().value(); + return true; + } + else + { + return false; + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool fromFirst, + const std::string& fromKey, + uint64_t limit) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_list_keys_values()->set_store_id(storeId); + request.mutable_list_keys_values()->set_from_first(fromFirst); + request.mutable_list_keys_values()->set_from_key(fromKey); + request.mutable_list_keys_values()->set_limit(limit); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LIST_KEY_VALUES, request); + + for (int i = 0; i < response.list_keys_values().keys_values_size(); ++i) + { + keys.push_back(response.list_keys_values().keys_values(i).key()); + values.push_back(response.list_keys_values().keys_values(i).value()); + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32" + // https://protobuf.dev/programming-guides/proto3/ + if (valueSize > std::numeric_limits<uint32_t>::max()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_enqueue_value()->set_queue_id(queueId); + request.mutable_enqueue_value()->set_value(value, valueSize); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_ENQUEUE_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_dequeue_value()->set_queue_id(queueId); + + switch (origin) + { + case QueueOrigin_Back: + request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_BACK); + break; + + case QueueOrigin_Front: + request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_FRONT); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DEQUEUE_VALUE, request); + + if (response.dequeue_value().found()) + { + value = response.dequeue_value().value(); + return true; + } + else + { + return false; + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_queue_size()->set_queue_id(queueId); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_QUEUE_SIZE, request); + + return response.get_queue_size().size(); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } }; @@ -1895,6 +2141,9 @@ dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency()); dbCapabilities_.SetHasExtendedChanges(systemInfo.has_extended_changes()); dbCapabilities_.SetHasFindSupport(systemInfo.supports_find()); + dbCapabilities_.SetKeyValueStoresSupport(systemInfo.supports_key_value_stores()); + dbCapabilities_.SetQueuesSupport(systemInfo.supports_queues()); + dbCapabilities_.SetAttachmentCustomDataSupport(systemInfo.has_attachment_custom_data()); } open_ = true; @@ -1961,7 +2210,7 @@ void OrthancPluginDatabaseV4::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { if (!open_) {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Fri Jun 27 15:00:33 2025 +0200 @@ -88,7 +88,7 @@ virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -39,7 +39,7 @@ #include "../../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h" #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" -#include "../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h" +#include "../../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../../OrthancFramework/Sources/HttpServer/HttpServer.h" #include "../../../OrthancFramework/Sources/HttpServer/HttpToolbox.h" #include "../../../OrthancFramework/Sources/Images/Image.h" @@ -54,7 +54,6 @@ #include "../../../OrthancFramework/Sources/MetricsRegistry.h" #include "../../../OrthancFramework/Sources/OrthancException.h" #include "../../../OrthancFramework/Sources/SerializationToolbox.h" -#include "../../../OrthancFramework/Sources/StringMemoryBuffer.h" #include "../../../OrthancFramework/Sources/Toolbox.h" #include "../../Sources/Database/VoidDatabaseListener.h" #include "../../Sources/OrthancConfiguration.h" @@ -65,12 +64,12 @@ #include "OrthancPluginDatabase.h" #include "OrthancPluginDatabaseV3.h" #include "OrthancPluginDatabaseV4.h" +#include "PluginMemoryBuffer32.h" #include "PluginsEnumerations.h" #include "PluginsJob.h" #include <boost/math/special_functions/round.hpp> #include <boost/regex.hpp> -#include <dcmtk/dcmdata/dcdict.h> #include <dcmtk/dcmdata/dcdicent.h> #include <dcmtk/dcmnet/dimse.h> @@ -79,6 +78,125 @@ namespace Orthanc { + class OrthancPlugins::IDicomInstance : public boost::noncopyable + { + public: + virtual ~IDicomInstance() + { + } + + virtual bool CanBeFreed() const = 0; + + virtual const DicomInstanceToStore& GetInstance() const = 0; + }; + + + class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance + { + private: + const DicomInstanceToStore& instance_; + + public: + explicit DicomInstanceFromCallback(const DicomInstanceToStore& instance) : + instance_(instance) + { + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return false; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance + { + private: + std::string buffer_; + std::unique_ptr<DicomInstanceToStore> instance_; + + void Setup(const void* buffer, + size_t size) + { + buffer_.assign(reinterpret_cast<const char*>(buffer), size); + + instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_)); + instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + + public: + DicomInstanceFromBuffer(const void* buffer, + size_t size) + { + Setup(buffer, size); + } + + explicit DicomInstanceFromBuffer(const std::string& buffer) + { + Setup(buffer.empty() ? NULL : buffer.c_str(), buffer.size()); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return *instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance + { + private: + std::unique_ptr<ParsedDicomFile> parsed_; + std::unique_ptr<DicomInstanceToStore> instance_; + + void Setup(ParsedDicomFile* parsed) + { + parsed_.reset(parsed); + + if (parsed_.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_)); + instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + } + + public: + explicit DicomInstanceFromParsed(IDicomTranscoder::DicomImage& transcoded) + { + Setup(transcoded.ReleaseAsParsedDicomFile()); + } + + explicit DicomInstanceFromParsed(ParsedDicomFile* parsed /* takes ownership */) + { + Setup(parsed); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return *instance_; + }; + }; + + class OrthancPlugins::WebDavCollection : public IWebDavBucket { private: @@ -417,78 +535,45 @@ }; - static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer* target, const void* data, size_t size) { - if (static_cast<uint32_t>(size) != size) - { - throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT); - } - - target.size = size; - - if (size == 0) - { - target.data = NULL; - } - else - { - target.data = malloc(size); - if (target.data != NULL) - { - memcpy(target.data, data, size); - } - else - { - throw OrthancException(ErrorCode_NotEnoughMemory); - } - } + PluginMemoryBuffer32 buffer; + buffer.Assign(data, size); + buffer.Release(target); } - static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer* target, const std::string& str) { - if (str.size() == 0) - { - target.size = 0; - target.data = NULL; - } - else - { - CopyToMemoryBuffer(target, str.c_str(), str.size()); - } + PluginMemoryBuffer32 buffer; + buffer.Assign(str); + buffer.Release(target); } static char* CopyString(const std::string& str) { - if (static_cast<uint32_t>(str.size()) != str.size()) - { - throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT); - } - char *result = reinterpret_cast<char*>(malloc(str.size() + 1)); if (result == NULL) { throw OrthancException(ErrorCode_NotEnoughMemory); } - if (str.size() == 0) - { - result[0] = '\0'; - } - else - { - memcpy(result, &str[0], str.size() + 1); - } + if (!str.empty()) + { + memcpy(result, str.c_str(), str.size()); + } + + result[str.size()] = '\0'; // Add the null terminator of the string return result; } - static void CopyDictionary(OrthancPluginMemoryBuffer& target, + static void CopyDictionary(PluginMemoryBuffer32& target, const std::map<std::string, std::string>& dictionary) { Json::Value json = Json::objectValue; @@ -499,59 +584,49 @@ json[it->first] = it->second; } - std::string s = json.toStyledString(); - CopyToMemoryBuffer(target, s); + target.Assign(json.toStyledString()); } namespace { - class MemoryBufferRaii : public boost::noncopyable - { - private: - OrthancPluginMemoryBuffer buffer_; - - public: - MemoryBufferRaii() - { - buffer_.size = 0; - buffer_.data = NULL; - } - - ~MemoryBufferRaii() - { - if (buffer_.size != 0) + static IMemoryBuffer* GetRangeFromWhole(std::unique_ptr<IMemoryBuffer>& whole, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */) + { + if (start > end) + { + throw OrthancException(ErrorCode_BadRange); + } + else if (start == end) + { + return new PluginMemoryBuffer64; // Empty + } + else + { + if (start == 0 && + end == whole->GetSize()) { - free(buffer_.data); + return whole.release(); } - } - - OrthancPluginMemoryBuffer* GetObject() - { - return &buffer_; - } - - void ToString(std::string& target) const - { - if ((buffer_.data == NULL && buffer_.size != 0) || - (buffer_.data != NULL && buffer_.size == 0)) + else if (end > whole->GetSize()) { - throw OrthancException(ErrorCode_Plugin); + throw OrthancException(ErrorCode_BadRange); } else { - target.resize(buffer_.size); - - if (buffer_.size != 0) - { - memcpy(&target[0], buffer_.data, buffer_.size); - } + std::unique_ptr<PluginMemoryBuffer64> range(new PluginMemoryBuffer64); + range->Assign(reinterpret_cast<const char*>(whole->GetData()) + start, end - start); + assert(range->GetSize() > 0); + + return range.release(); } } - }; - - - class StorageAreaBase : public IStorageArea + } + + + // "legacy" storage plugins don't store customData -> derive from IStorageArea + class StorageAreaWithoutCustomData : public IStorageArea { private: OrthancPluginStorageCreate create_; @@ -564,50 +639,10 @@ return errorDictionary_; } - IMemoryBuffer* RangeFromWhole(const std::string& uuid, - FileContentType type, - uint64_t start /* inclusive */, - uint64_t end /* exclusive */) - { - if (start > end) - { - throw OrthancException(ErrorCode_BadRange); - } - else if (start == end) - { - return new StringMemoryBuffer; // Empty - } - else - { - std::unique_ptr<IMemoryBuffer> whole(Read(uuid, type)); - - if (start == 0 && - end == whole->GetSize()) - { - return whole.release(); - } - else if (end > whole->GetSize()) - { - throw OrthancException(ErrorCode_BadRange); - } - else - { - std::string range; - range.resize(end - start); - assert(!range.empty()); - - memcpy(&range[0], reinterpret_cast<const char*>(whole->GetData()) + start, range.size()); - - whole.reset(NULL); - return StringMemoryBuffer::CreateFromSwap(range); - } - } - } - public: - StorageAreaBase(OrthancPluginStorageCreate create, - OrthancPluginStorageRemove remove, - PluginsErrorDictionary& errorDictionary) : + StorageAreaWithoutCustomData(OrthancPluginStorageCreate create, + OrthancPluginStorageRemove remove, + PluginsErrorDictionary& errorDictionary) : create_(create), remove_(remove), errorDictionary_(errorDictionary) @@ -649,24 +684,16 @@ }; - class PluginStorageArea : public StorageAreaBase + class PluginStorageAreaV1 : public StorageAreaWithoutCustomData { private: OrthancPluginStorageRead read_; OrthancPluginFree free_; - void Free(void* buffer) const - { - if (buffer != NULL) - { - free_(buffer); - } - } - public: - PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks, - PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaV1(const _OrthancPluginRegisterStorageArea& callbacks, + PluginsErrorDictionary& errorDictionary) : + StorageAreaWithoutCustomData(callbacks.create, callbacks.remove, errorDictionary), read_(callbacks.read), free_(callbacks.free) { @@ -676,21 +703,25 @@ } } - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE - { - std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer); + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */) ORTHANC_OVERRIDE + { + std::unique_ptr<IMemoryBuffer> whole(new MallocMemoryBuffer); void* buffer = NULL; int64_t size = 0; - OrthancPluginErrorCode error = read_ - (&buffer, &size, uuid.c_str(), Plugins::Convert(type)); + OrthancPluginErrorCode error = read_(&buffer, &size, uuid.c_str(), Plugins::Convert(type)); if (error == OrthancPluginErrorCode_Success) { - result->Assign(buffer, size, free_); - return result.release(); + // Beware that the buffer must be unallocated by the "free_" function provided by the plugin, + // so we cannot use "PluginMemoryBuffer64" + dynamic_cast<MallocMemoryBuffer&>(*whole).Assign(buffer, size, free_); + + return GetRangeFromWhole(whole, start, end); } else { @@ -699,15 +730,7 @@ } } - virtual IMemoryBuffer* ReadRange(const std::string& uuid, - FileContentType type, - uint64_t start /* inclusive */, - uint64_t end /* exclusive */) ORTHANC_OVERRIDE - { - return RangeFromWhole(uuid, type, start, end); - } - - virtual bool HasReadRange() const ORTHANC_OVERRIDE + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE { return false; } @@ -715,16 +738,16 @@ // New in Orthanc 1.9.0 - class PluginStorageArea2 : public StorageAreaBase + class PluginStorageAreaV2 : public StorageAreaWithoutCustomData { private: OrthancPluginStorageReadWhole readWhole_; OrthancPluginStorageReadRange readRange_; public: - PluginStorageArea2(const _OrthancPluginRegisterStorageArea2& callbacks, - PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaV2(const _OrthancPluginRegisterStorageArea2& callbacks, + PluginsErrorDictionary& errorDictionary) : + StorageAreaWithoutCustomData(callbacks.create, callbacks.remove, errorDictionary), readWhole_(callbacks.readWhole), readRange_(callbacks.readRange) { @@ -734,29 +757,6 @@ } } - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE - { - std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer); - - OrthancPluginMemoryBuffer64 buffer; - buffer.size = 0; - buffer.data = NULL; - - OrthancPluginErrorCode error = readWhole_(&buffer, uuid.c_str(), Plugins::Convert(type)); - - if (error == OrthancPluginErrorCode_Success) - { - result->Assign(buffer.data, buffer.size, ::free); - return result.release(); - } - else - { - GetErrorDictionary().LogError(error, true); - throw OrthancException(static_cast<ErrorCode>(error)); - } - } - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, @@ -764,34 +764,44 @@ { if (readRange_ == NULL) { - return RangeFromWhole(uuid, type, start, end); + std::unique_ptr<IMemoryBuffer> whole(new PluginMemoryBuffer64); + + OrthancPluginErrorCode error = readWhole_(dynamic_cast<PluginMemoryBuffer64&>(*whole).GetObject(), + uuid.c_str(), Plugins::Convert(type)); + + if (error == OrthancPluginErrorCode_Success) + { + return GetRangeFromWhole(whole, start, end); + } + else + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } } else { + std::unique_ptr<PluginMemoryBuffer64> buffer(new PluginMemoryBuffer64); + if (start > end) { throw OrthancException(ErrorCode_BadRange); } else if (start == end) { - return new StringMemoryBuffer; + return buffer.release(); } else { - std::string range; - range.resize(end - start); - assert(!range.empty()); - - OrthancPluginMemoryBuffer64 buffer; - buffer.data = &range[0]; - buffer.size = static_cast<uint64_t>(range.size()); + buffer->Resize(end - start); + assert(buffer->GetSize() > 0); OrthancPluginErrorCode error = - readRange_(&buffer, uuid.c_str(), Plugins::Convert(type), start); + readRange_(buffer->GetObject(), uuid.c_str(), Plugins::Convert(type), start); if (error == OrthancPluginErrorCode_Success) { - return StringMemoryBuffer::CreateFromSwap(range); + return buffer.release(); } else { @@ -802,26 +812,148 @@ } } - virtual bool HasReadRange() const ORTHANC_OVERRIDE + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE { return (readRange_ != NULL); } }; + // New in Orthanc 1.12.8 + class PluginStorageAreaV3 : public IPluginStorageArea + { + private: + OrthancPluginStorageCreate2 create_; + OrthancPluginStorageReadRange2 readRange_; + OrthancPluginStorageRemove2 remove_; + PluginsErrorDictionary& errorDictionary_; + + protected: + PluginsErrorDictionary& GetErrorDictionary() const + { + return errorDictionary_; + } + + public: + PluginStorageAreaV3(const _OrthancPluginRegisterStorageArea3& callbacks, + PluginsErrorDictionary& errorDictionary) : + create_(callbacks.create), + readRange_(callbacks.readRange), + remove_(callbacks.remove), + errorDictionary_(errorDictionary) + { + if (create_ == NULL || + readRange_ == NULL || + remove_ == NULL) + { + throw OrthancException(ErrorCode_Plugin, "Storage area plugin does not implement all the required primitives (create, remove, and readRange)"); + } + } + + virtual void Create(std::string& customData /* out */, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance /* can be NULL if not a DICOM instance */) ORTHANC_OVERRIDE + { + PluginMemoryBuffer32 customDataBuffer; + OrthancPluginErrorCode error; + + if (dicomInstance != NULL) + { + Orthanc::OrthancPlugins::DicomInstanceFromCallback wrapped(*dicomInstance); + error = create_(customDataBuffer.GetObject(), uuid.c_str(), content, size, Plugins::Convert(type), Plugins::Convert(compression), + reinterpret_cast<OrthancPluginDicomInstance*>(&wrapped)); + } + else + { + error = create_(customDataBuffer.GetObject(), uuid.c_str(), content, size, Plugins::Convert(type), Plugins::Convert(compression), NULL); + } + + if (error != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + else + { + customDataBuffer.MoveToString(customData); + } + } + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + OrthancPluginErrorCode error = remove_(uuid.c_str(), Plugins::Convert(type), + customData.empty() ? NULL : customData.c_str(), customData.size()); + + if (error != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) ORTHANC_OVERRIDE + { + if (start > end) + { + throw OrthancException(ErrorCode_BadRange); + } + else if (start == end) + { + return new PluginMemoryBuffer64; + } + else + { + std::unique_ptr<PluginMemoryBuffer64> buffer(new PluginMemoryBuffer64); + buffer->Resize(end - start); + assert(buffer->GetSize() > 0); + + OrthancPluginErrorCode error = + readRange_(buffer->GetObject(), uuid.c_str(), Plugins::Convert(type), start, customData.empty() ? NULL : customData.c_str(), customData.size()); + + if (error == OrthancPluginErrorCode_Success) + { + return buffer.release(); + } + else + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + } + + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return true; + } + }; + + class StorageAreaFactory : public boost::noncopyable { private: enum Version { Version1, - Version2 + Version2, + Version3 }; SharedLibrary& sharedLibrary_; Version version_; - _OrthancPluginRegisterStorageArea callbacks_; + _OrthancPluginRegisterStorageArea callbacks1_; _OrthancPluginRegisterStorageArea2 callbacks2_; + _OrthancPluginRegisterStorageArea3 callbacks3_; PluginsErrorDictionary& errorDictionary_; static void WarnNoReadRange() @@ -835,7 +967,7 @@ PluginsErrorDictionary& errorDictionary) : sharedLibrary_(sharedLibrary), version_(Version1), - callbacks_(callbacks), + callbacks1_(callbacks), errorDictionary_(errorDictionary) { WarnNoReadRange(); @@ -855,20 +987,37 @@ } } + StorageAreaFactory(SharedLibrary& sharedLibrary, + const _OrthancPluginRegisterStorageArea3& callbacks, + PluginsErrorDictionary& errorDictionary) : + sharedLibrary_(sharedLibrary), + version_(Version3), + callbacks3_(callbacks), + errorDictionary_(errorDictionary) + { + if (callbacks.readRange == NULL) + { + WarnNoReadRange(); + } + } + SharedLibrary& GetSharedLibrary() { return sharedLibrary_; } - IStorageArea* Create() const + IPluginStorageArea* Create() const { switch (version_) { case Version1: - return new PluginStorageArea(callbacks_, errorDictionary_); + return new PluginStorageAreaAdapter(new PluginStorageAreaV1(callbacks1_, errorDictionary_)); case Version2: - return new PluginStorageArea2(callbacks2_, errorDictionary_); + return new PluginStorageAreaAdapter(new PluginStorageAreaV2(callbacks2_, errorDictionary_)); + + case Version3: + return new PluginStorageAreaV3(callbacks3_, errorDictionary_); default: throw OrthancException(ErrorCode_InternalError); @@ -1612,7 +1761,8 @@ std::unique_ptr<OrthancPluginDatabaseV4> databaseV4_; // New in Orthanc 1.12.0 PluginsErrorDictionary dictionary_; std::string databaseServerIdentifier_; // New in Orthanc 1.9.2 - unsigned int maxDatabaseRetries_; // New in Orthanc 1.9.2 + unsigned int maxDatabaseRetries_; // New in Orthanc 1.9.2 + bool hasStorageAreaCustomData_; // New in Orthanc 1.12.8 explicit PImpl(const std::string& databaseServerIdentifier) : contextRefCount_(0), @@ -1623,7 +1773,8 @@ argc_(1), argv_(NULL), databaseServerIdentifier_(databaseServerIdentifier), - maxDatabaseRetries_(0) + maxDatabaseRetries_(0), + hasStorageAreaCustomData_(false) { memset(&moveCallbacks_, 0, sizeof(moveCallbacks_)); } @@ -1715,7 +1866,7 @@ } } - void GetDicomQuery(OrthancPluginMemoryBuffer& target) const + void GetDicomQuery(OrthancPluginMemoryBuffer* target) const { if (currentQuery_ == NULL) { @@ -1724,7 +1875,7 @@ std::string dicom; currentQuery_->SaveToMemoryBuffer(dicom); - CopyToMemoryBuffer(target, dicom.c_str(), dicom.size()); + CopyToMemoryBuffer(target, dicom); } bool IsMatch(const void* dicom, @@ -2109,16 +2260,16 @@ sizeof(int32_t) != sizeof(OrthancPluginContentType) || sizeof(int32_t) != sizeof(OrthancPluginResourceType) || sizeof(int32_t) != sizeof(OrthancPluginChangeType) || + sizeof(int32_t) != sizeof(OrthancPluginCompressionType) || sizeof(int32_t) != sizeof(OrthancPluginImageFormat) || - sizeof(int32_t) != sizeof(OrthancPluginCompressionType) || sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) || sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) || sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) || sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) || - sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) || sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) || sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) || + sizeof(int32_t) != sizeof(OrthancPluginJobStopReason) || sizeof(int32_t) != sizeof(OrthancPluginConstraintType) || sizeof(int32_t) != sizeof(OrthancPluginMetricsType) || sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || @@ -2127,6 +2278,14 @@ sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) || sizeof(int32_t) != sizeof(OrthancPluginLogLevel) || sizeof(int32_t) != sizeof(OrthancPluginLogCategory) || + sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) || + sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin) || + + // From OrthancCDatabasePlugin.h + sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType) || + static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) || @@ -2527,125 +2686,6 @@ } - class OrthancPlugins::IDicomInstance : public boost::noncopyable - { - public: - virtual ~IDicomInstance() - { - } - - virtual bool CanBeFreed() const = 0; - - virtual const DicomInstanceToStore& GetInstance() const = 0; - }; - - - class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance - { - private: - const DicomInstanceToStore& instance_; - - public: - explicit DicomInstanceFromCallback(const DicomInstanceToStore& instance) : - instance_(instance) - { - } - - virtual bool CanBeFreed() const ORTHANC_OVERRIDE - { - return false; - } - - virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE - { - return instance_; - }; - }; - - - class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance - { - private: - std::string buffer_; - std::unique_ptr<DicomInstanceToStore> instance_; - - void Setup(const void* buffer, - size_t size) - { - buffer_.assign(reinterpret_cast<const char*>(buffer), size); - - instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_)); - instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); - } - - public: - DicomInstanceFromBuffer(const void* buffer, - size_t size) - { - Setup(buffer, size); - } - - explicit DicomInstanceFromBuffer(const std::string& buffer) - { - Setup(buffer.empty() ? NULL : buffer.c_str(), buffer.size()); - } - - virtual bool CanBeFreed() const ORTHANC_OVERRIDE - { - return true; - } - - virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE - { - return *instance_; - }; - }; - - - class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance - { - private: - std::unique_ptr<ParsedDicomFile> parsed_; - std::unique_ptr<DicomInstanceToStore> instance_; - - void Setup(ParsedDicomFile* parsed) - { - parsed_.reset(parsed); - - if (parsed_.get() == NULL) - { - throw OrthancException(ErrorCode_NullPointer); - } - else - { - instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_)); - instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); - } - } - - public: - explicit DicomInstanceFromParsed(IDicomTranscoder::DicomImage& transcoded) - { - Setup(transcoded.ReleaseAsParsedDicomFile()); - } - - explicit DicomInstanceFromParsed(ParsedDicomFile* parsed /* takes ownership */) - { - Setup(parsed); - } - - virtual bool CanBeFreed() const ORTHANC_OVERRIDE - { - return true; - } - - virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE - { - return *instance_; - }; - }; - - void OrthancPlugins::SignalStoredInstance(const std::string& instanceId, const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) @@ -2735,32 +2775,22 @@ } - OrthancPluginReceivedInstanceAction OrthancPlugins::ApplyReceivedInstanceCallbacks( - MallocMemoryBuffer& modified, - const void* receivedDicom, - size_t receivedDicomSize, - RequestOrigin origin) + OrthancPluginReceivedInstanceAction OrthancPlugins::ApplyReceivedInstanceCallbacks(PluginMemoryBuffer64& modified, + const void* receivedDicom, + size_t receivedDicomSize, + RequestOrigin origin) { boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); + modified.Clear(); + if (pimpl_->receivedInstanceCallback_ == NULL) { return OrthancPluginReceivedInstanceAction_KeepAsIs; } else { - OrthancPluginReceivedInstanceAction action; - - { - OrthancPluginMemoryBuffer64 buffer; - buffer.size = 0; - buffer.data = NULL; - - action = (*pimpl_->receivedInstanceCallback_) (&buffer, receivedDicom, receivedDicomSize, Plugins::Convert(origin)); - modified.Assign(buffer.data, buffer.size, ::free); - } - - return action; + return (*pimpl_->receivedInstanceCallback_) (modified.GetObject(), receivedDicom, receivedDicomSize, Plugins::Convert(origin)); } } @@ -3224,7 +3254,7 @@ lock.GetContext().ReadDicom(dicom, p.instanceId); } - CopyToMemoryBuffer(*p.target, dicom); + CopyToMemoryBuffer(p.target, dicom); } static void ThrowOnHttpError(HttpStatus httpStatus) @@ -3242,6 +3272,10 @@ { throw OrthancException(ErrorCode_UnknownResource); } + else if (intHttpStatus == 415) + { + throw OrthancException(ErrorCode_UnsupportedMediaType); + } else { throw OrthancException(ErrorCode_BadRequest); @@ -3270,7 +3304,7 @@ std::string result; ThrowOnHttpError(IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, httpHeaders)); - CopyToMemoryBuffer(*p.target, result); + CopyToMemoryBuffer(p.target, result); } @@ -3301,7 +3335,7 @@ std::string result; ThrowOnHttpError(IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, headers)); - CopyToMemoryBuffer(*p.target, result); + CopyToMemoryBuffer(p.target, result); } @@ -3331,7 +3365,7 @@ p.body, p.bodySize, httpHeaders) : IHttpHandler::SimplePut(result, NULL, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders))); - CopyToMemoryBuffer(*p.target, result); + CopyToMemoryBuffer(p.target, result); } @@ -3611,6 +3645,12 @@ break; } + case OrthancPluginCompressionType_None: + { + CopyToMemoryBuffer(p.target, p.source, p.size); + return; + } + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -3625,7 +3665,7 @@ } } - CopyToMemoryBuffer(*p.target, result); + CopyToMemoryBuffer(p.target, result); } @@ -3686,7 +3726,7 @@ MimeType mime; std::string frame; instance.GetParsedDicomFile().GetRawFrame(frame, mime, p.frameIndex); - CopyToMemoryBuffer(*p.targetBuffer, frame); + CopyToMemoryBuffer(p.targetBuffer, frame); return; } @@ -3716,7 +3756,7 @@ p.targetBuffer->data = NULL; p.targetBuffer->size = 0; - CopyToMemoryBuffer(*p.targetBuffer, instance.GetBufferData(), instance.GetBufferSize()); + CopyToMemoryBuffer(p.targetBuffer, instance.GetBufferData(), instance.GetBufferSize()); return; } @@ -3825,7 +3865,7 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } - CopyToMemoryBuffer(*p.target, compressed.size() > 0 ? compressed.c_str() : NULL, compressed.size()); + CopyToMemoryBuffer(p.target, compressed); } @@ -3922,29 +3962,29 @@ } // Copy the HTTP headers of the answer, if the plugin requested them + PluginMemoryBuffer32 tmpHeaders; if (answerHeaders != NULL) { - CopyDictionary(*answerHeaders, headers); + CopyDictionary(tmpHeaders, headers); } // Copy the body of the answer if it makes sense - if (client.GetMethod() != HttpMethod_Delete) - { - try - { - if (answerBody != NULL) - { - CopyToMemoryBuffer(*answerBody, body); - } - } - catch (OrthancException&) - { - if (answerHeaders != NULL) - { - free(answerHeaders->data); - } - throw; - } + PluginMemoryBuffer32 tmpBody; + if (client.GetMethod() != HttpMethod_Delete && + answerBody != NULL) + { + tmpBody.Assign(body); + } + + // All the memory has been allocated at this point, so we can safely release the buffers + if (answerHeaders != NULL) + { + tmpHeaders.Release(answerHeaders); + } + + if (answerBody != NULL) + { + tmpBody.Release(answerBody); } } @@ -4143,25 +4183,28 @@ *p.httpStatus = static_cast<uint16_t>(status); + PluginMemoryBuffer32 tmpHeaders; if (p.answerHeaders != NULL) { - CopyDictionary(*p.answerHeaders, answerHeaders); - } - - try - { - if (p.answerBody != NULL) - { - CopyToMemoryBuffer(*p.answerBody, answerBody); - } - } - catch (OrthancException&) - { - if (p.answerHeaders != NULL) - { - free(p.answerHeaders->data); - } - throw; + CopyDictionary(tmpHeaders, answerHeaders); + } + + PluginMemoryBuffer32 tmpBody; + if (p.method != OrthancPluginHttpMethod_Delete && + p.answerBody != NULL) + { + tmpBody.Assign(answerBody); + } + + // All the memory has been allocated at this point, so we can safely release the buffers + if (p.answerHeaders != NULL) + { + tmpHeaders.Release(p.answerHeaders); + } + + if (p.answerBody != NULL) + { + tmpBody.Release(p.answerBody); } } @@ -4228,29 +4271,29 @@ } // Copy the HTTP headers of the answer, if the plugin requested them + PluginMemoryBuffer32 tmpHeaders; if (p.answerHeaders != NULL) { - CopyDictionary(*p.answerHeaders, headers); + CopyDictionary(tmpHeaders, headers); } // Copy the body of the answer if it makes sense - if (p.method != OrthancPluginHttpMethod_Delete) - { - try - { - if (p.answerBody != NULL) - { - CopyToMemoryBuffer(*p.answerBody, body); - } - } - catch (OrthancException&) - { - if (p.answerHeaders != NULL) - { - free(p.answerHeaders->data); - } - throw; - } + PluginMemoryBuffer32 tmpBody; + if (p.method != OrthancPluginHttpMethod_Delete && + p.answerBody != NULL) + { + tmpBody.Assign(body); + } + + // All the memory has been allocated at this point, so we can safely release the buffers + if (p.answerHeaders != NULL) + { + tmpHeaders.Release(p.answerHeaders); + } + + if (p.answerBody != NULL) + { + tmpBody.Release(p.answerBody); } } @@ -4391,7 +4434,7 @@ file->SaveToMemoryBuffer(dicom); } - CopyToMemoryBuffer(*parameters.target, dicom); + CopyToMemoryBuffer(parameters.target, dicom); } @@ -4499,6 +4542,184 @@ reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendMultipartItem(p.answer, p.answerSize, headers); } + void OrthancPlugins::ApplyAdoptDicomInstance(const _OrthancPluginAdoptDicomInstance& parameters) + { + if (!pimpl_->hasStorageAreaCustomData_) + { + LOG(WARNING) << "The adoption of a DICOM instance should only be used in combination with a custom " + << "storage area registered using OrthancPluginRegisterStorageArea3()"; + } + + std::string md5; + Toolbox::ComputeMD5(md5, parameters.dicom, parameters.dicomSize); + + std::unique_ptr<DicomInstanceToStore> dicom(DicomInstanceToStore::CreateFromBuffer(parameters.dicom, parameters.dicomSize)); + dicom->SetOrigin(DicomInstanceOrigin::FromPlugins()); + + const std::string attachmentUuid = Toolbox::GenerateUuid(); + + FileInfo adoptedFile(attachmentUuid, FileContentType_Dicom, parameters.dicomSize, md5); + adoptedFile.SetCustomData(parameters.customData, parameters.customDataSize); + + std::string instanceId; + ServerContext::StoreResult result; + + { + PImpl::ServerContextReference lock(*pimpl_); + result = lock.GetContext().AdoptDicomInstance(instanceId, *dicom, StoreInstanceMode_Default, adoptedFile); + } + + CopyToMemoryBuffer(parameters.attachmentUuid, attachmentUuid); + CopyToMemoryBuffer(parameters.instanceId, instanceId); + *(parameters.storeStatus) = Plugins::Convert(result.GetStatus()); + } + + static void CheckAttachmentCustomDataSupport(ServerContext& context) + { + if (!context.GetIndex().HasAttachmentCustomDataSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support custom data for attachments"); + } + } + + void OrthancPlugins::ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckAttachmentCustomDataSupport(lock.GetContext()); + + std::string customData; + lock.GetContext().GetIndex().GetAttachmentCustomData(customData, parameters.attachmentUuid); + + CopyToMemoryBuffer(parameters.customData, customData); + } + + void OrthancPlugins::ApplySetAttachmentCustomData(const _OrthancPluginSetAttachmentCustomData& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckAttachmentCustomDataSupport(lock.GetContext()); + + lock.GetContext().GetIndex().SetAttachmentCustomData(parameters.attachmentUuid, parameters.customData, parameters.customDataSize); + } + + static void CheckKeyValueStoresSupport(ServerContext& context) + { + if (!context.GetIndex().HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support key-value stores"); + } + } + + void OrthancPlugins::ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + lock.GetContext().GetIndex().StoreKeyValue(parameters.storeId, parameters.key, parameters.value, parameters.valueSize); + } + + void OrthancPlugins::ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + lock.GetContext().GetIndex().DeleteKeyValue(parameters.storeId, parameters.key); + } + + void OrthancPlugins::ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + std::string value; + + if (lock.GetContext().GetIndex().GetKeyValue(value, parameters.storeId, parameters.key)) + { + CopyToMemoryBuffer(parameters.target, value); + *parameters.found = true; + } + else + { + *parameters.found = false; + } + } + + void OrthancPlugins::ApplyCreateKeysValuesIterator(const _OrthancPluginCreateKeysValuesIterator& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + *parameters.target = reinterpret_cast<OrthancPluginKeysValuesIterator*>( + new StatelessDatabaseOperations::KeysValuesIterator(lock.GetContext().GetIndex(), parameters.storeId)); + } + + static void CheckQueuesSupport(ServerContext& context) + { + if (!context.GetIndex().HasQueuesSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support queues"); + } + } + + void OrthancPlugins::ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckQueuesSupport(lock.GetContext()); + + lock.GetContext().GetIndex().EnqueueValue(parameters.queueId, parameters.value, parameters.valueSize); + } + + void OrthancPlugins::ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckQueuesSupport(lock.GetContext()); + + std::string value; + + if (lock.GetContext().GetIndex().DequeueValue(value, parameters.queueId, Plugins::Convert(parameters.origin))) + { + CopyToMemoryBuffer(parameters.target, value); + *parameters.found = true; + } + else + { + *parameters.found = false; + } + } + + void OrthancPlugins::ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckQueuesSupport(lock.GetContext()); + + *parameters.size = lock.GetContext().GetIndex().GetQueueSize(parameters.queueId); + } + + void OrthancPlugins::ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + bool statusHasChanged = false; + + lock.GetContext().GetIndex().SetStableStatus(statusHasChanged, + parameters.resourceId, + (parameters.stableStatus == OrthancPluginStableStatus_Stable)); + if (statusHasChanged) + { + *(parameters.statusHasChanged) = 1; + } + else + { + *(parameters.statusHasChanged) = 0; + } + } void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params) { @@ -4624,35 +4845,6 @@ } - namespace - { - class DictionaryReadLocker - { - private: - const DcmDataDictionary& dictionary_; - - public: - DictionaryReadLocker() : dictionary_(dcmDataDict.rdlock()) - { - } - - ~DictionaryReadLocker() - { -#if DCMTK_VERSION_NUMBER >= 364 - dcmDataDict.rdunlock(); -#else - dcmDataDict.unlock(); -#endif - } - - const DcmDataDictionary* operator->() - { - return &dictionary_; - } - }; - } - - void OrthancPlugins::ApplyLookupDictionary(const void* parameters) { const _OrthancPluginLookupDictionary& p = @@ -4661,38 +4853,41 @@ DicomTag tag(FromDcmtkBridge::ParseTag(p.name)); DcmTagKey tag2(tag.GetGroup(), tag.GetElement()); - DictionaryReadLocker locker; - const DcmDictEntry* entry = NULL; - - if (tag.IsPrivate()) - { - // Fix issue 168 (Plugins can't read private tags from the - // configuration file) - // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=168 - std::string privateCreator; - { - OrthancConfiguration::ReaderLock lock; - privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator(); - } - - entry = locker->findEntry(tag2, privateCreator.c_str()); - } - else - { - entry = locker->findEntry(tag2, NULL); - } - - if (entry == NULL) - { - throw OrthancException(ErrorCode_UnknownDicomTag, p.name); - } - else - { - p.target->group = entry->getKey().getGroup(); - p.target->element = entry->getKey().getElement(); - p.target->vr = Plugins::Convert(FromDcmtkBridge::Convert(entry->getEVR())); - p.target->minMultiplicity = static_cast<uint32_t>(entry->getVMMin()); - p.target->maxMultiplicity = (entry->getVMMax() == DcmVariableVM ? 0 : static_cast<uint32_t>(entry->getVMMax())); + { + FromDcmtkBridge::DictionaryReaderLock lock; + + const DcmDictEntry* entry = NULL; // This value is only valid while "lock" is active + + if (tag.IsPrivate()) + { + // Fix issue 168 (Plugins can't read private tags from the + // configuration file) + // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=168 + std::string privateCreator; + { + OrthancConfiguration::ReaderLock configurationLock; + privateCreator = configurationLock.GetConfiguration().GetDefaultPrivateCreator(); + } + + entry = lock.GetDictionary().findEntry(tag2, privateCreator.c_str()); + } + else + { + entry = lock.GetDictionary().findEntry(tag2, NULL); + } + + if (entry == NULL) + { + throw OrthancException(ErrorCode_UnknownDicomTag, p.name); + } + else + { + p.target->group = entry->getKey().getGroup(); + p.target->element = entry->getKey().getElement(); + p.target->vr = Plugins::Convert(FromDcmtkBridge::Convert(entry->getEVR())); + p.target->minMultiplicity = static_cast<uint32_t>(entry->getVMMin()); + p.target->maxMultiplicity = (entry->getVMMax() == DcmVariableVM ? 0 : static_cast<uint32_t>(entry->getVMMax())); + } } } @@ -4948,7 +5143,7 @@ std::string content; SystemToolbox::ReadFile(content, p.path); - CopyToMemoryBuffer(*p.target, content.size() > 0 ? content.c_str() : NULL, content.size()); + CopyToMemoryBuffer(p.target, content); return true; } @@ -5066,32 +5261,13 @@ return true; case _OrthancPluginService_StorageAreaCreate: - { - const _OrthancPluginStorageAreaCreate& p = - *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - storage.Create(p.uuid, p.content, static_cast<size_t>(p.size), Plugins::Convert(p.type)); - return true; - } + throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaCreate() is only available in Orthanc <= 1.12.6"); case _OrthancPluginService_StorageAreaRead: - { - const _OrthancPluginStorageAreaRead& p = - *reinterpret_cast<const _OrthancPluginStorageAreaRead*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - std::unique_ptr<IMemoryBuffer> content(storage.Read(p.uuid, Plugins::Convert(p.type))); - CopyToMemoryBuffer(*p.target, content->GetData(), content->GetSize()); - return true; - } + throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaRead() is only available in Orthanc <= 1.12.6"); case _OrthancPluginService_StorageAreaRemove: - { - const _OrthancPluginStorageAreaRemove& p = - *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - storage.Remove(p.uuid, Plugins::Convert(p.type)); - return true; - } + throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaRemove() is only available in Orthanc <= 1.12.6"); case _OrthancPluginService_DicomBufferToJson: case _OrthancPluginService_DicomInstanceToJson: @@ -5143,7 +5319,7 @@ { const _OrthancPluginWorklistQueryOperation& p = *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters); - reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(*p.target); + reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(p.target); return true; } @@ -5526,20 +5702,10 @@ const _OrthancPluginCreateMemoryBuffer& p = *reinterpret_cast<const _OrthancPluginCreateMemoryBuffer*>(parameters); - p.target->data = NULL; - p.target->size = 0; - - if (p.size != 0) - { - p.target->data = malloc(p.size); - if (p.target->data == NULL) - { - throw OrthancException(ErrorCode_NotEnoughMemory); - } - - p.target->size = p.size; - } - + PluginMemoryBuffer32 buffer; + buffer.Resize(p.size); + buffer.Release(p.target); + return true; } @@ -5548,19 +5714,9 @@ const _OrthancPluginCreateMemoryBuffer64& p = *reinterpret_cast<const _OrthancPluginCreateMemoryBuffer64*>(parameters); - p.target->data = NULL; - p.target->size = 0; - - if (p.size != 0) - { - p.target->data = malloc(p.size); - if (p.target->data == NULL) - { - throw OrthancException(ErrorCode_NotEnoughMemory); - } - - p.target->size = p.size; - } + PluginMemoryBuffer64 buffer; + buffer.Resize(p.size); + buffer.Release(p.target); return true; } @@ -5587,6 +5743,114 @@ return true; } + case _OrthancPluginService_AdoptDicomInstance: + { + const _OrthancPluginAdoptDicomInstance& p = *reinterpret_cast<const _OrthancPluginAdoptDicomInstance*>(parameters); + ApplyAdoptDicomInstance(p); + return true; + } + + case _OrthancPluginService_GetAttachmentCustomData: + { + const _OrthancPluginGetAttachmentCustomData& p = *reinterpret_cast<const _OrthancPluginGetAttachmentCustomData*>(parameters); + ApplyGetAttachmentCustomData(p); + return true; + } + + case _OrthancPluginService_SetAttachmentCustomData: + { + const _OrthancPluginSetAttachmentCustomData& p = *reinterpret_cast<const _OrthancPluginSetAttachmentCustomData*>(parameters); + ApplySetAttachmentCustomData(p); + return true; + } + + case _OrthancPluginService_StoreKeyValue: + { + const _OrthancPluginStoreKeyValue& p = *reinterpret_cast<const _OrthancPluginStoreKeyValue*>(parameters); + ApplyStoreKeyValue(p); + return true; + } + + case _OrthancPluginService_DeleteKeyValue: + { + const _OrthancPluginDeleteKeyValue& p = *reinterpret_cast<const _OrthancPluginDeleteKeyValue*>(parameters); + ApplyDeleteKeyValue(p); + return true; + } + + case _OrthancPluginService_GetKeyValue: + { + const _OrthancPluginGetKeyValue& p = *reinterpret_cast<const _OrthancPluginGetKeyValue*>(parameters); + ApplyGetKeyValue(p); + return true; + } + + case _OrthancPluginService_CreateKeysValuesIterator: + { + const _OrthancPluginCreateKeysValuesIterator& p = *reinterpret_cast<const _OrthancPluginCreateKeysValuesIterator*>(parameters); + ApplyCreateKeysValuesIterator(p); + return true; + } + + case _OrthancPluginService_FreeKeysValuesIterator: + { + const _OrthancPluginFreeKeysValuesIterator& p = *reinterpret_cast<const _OrthancPluginFreeKeysValuesIterator*>(parameters); + delete reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + return true; + } + + case _OrthancPluginService_KeysValuesIteratorNext: + { + const _OrthancPluginKeysValuesIteratorNext& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorNext*>(parameters); + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + *p.done = iterator.Next() ? 1 : 0; + return true; + } + + case _OrthancPluginService_KeysValuesIteratorGetKey: + { + const _OrthancPluginKeysValuesIteratorGetKey& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetKey*>(parameters); + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + *p.target = iterator.GetKey().c_str(); + return true; + } + + case _OrthancPluginService_KeysValuesIteratorGetValue: + { + const _OrthancPluginKeysValuesIteratorGetValue& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetValue*>(parameters); + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + CopyToMemoryBuffer(p.target, iterator.GetValue()); + return true; + } + + case _OrthancPluginService_EnqueueValue: + { + const _OrthancPluginEnqueueValue& p = *reinterpret_cast<const _OrthancPluginEnqueueValue*>(parameters); + ApplyEnqueueValue(p); + return true; + } + + case _OrthancPluginService_DequeueValue: + { + const _OrthancPluginDequeueValue& p = *reinterpret_cast<const _OrthancPluginDequeueValue*>(parameters); + ApplyDequeueValue(p); + return true; + } + + case _OrthancPluginService_GetQueueSize: + { + const _OrthancPluginGetQueueSize& p = *reinterpret_cast<const _OrthancPluginGetQueueSize*>(parameters); + ApplyGetQueueSize(p); + return true; + } + + case _OrthancPluginService_SetStableStatus: + { + const _OrthancPluginSetStableStatus& p = *reinterpret_cast<const _OrthancPluginSetStableStatus*>(parameters); + ApplySetStableStatus(p); + return true; + } + default: return false; } @@ -5670,23 +5934,35 @@ case _OrthancPluginService_RegisterStorageArea: case _OrthancPluginService_RegisterStorageArea2: - { - CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area"; - + case _OrthancPluginService_RegisterStorageArea3: + { if (pimpl_->storageArea_.get() == NULL) { if (service == _OrthancPluginService_RegisterStorageArea) { + CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v1)"; + const _OrthancPluginRegisterStorageArea& p = *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters); pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); } else if (service == _OrthancPluginService_RegisterStorageArea2) { + CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v2)"; + const _OrthancPluginRegisterStorageArea2& p = *reinterpret_cast<const _OrthancPluginRegisterStorageArea2*>(parameters); pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); } + else if (service == _OrthancPluginService_RegisterStorageArea3) + { + CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v3)"; + + const _OrthancPluginRegisterStorageArea3& p = + *reinterpret_cast<const _OrthancPluginRegisterStorageArea3*>(parameters); + pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); + pimpl_->hasStorageAreaCustomData_ = true; + } else { throw OrthancException(ErrorCode_InternalError); @@ -5809,7 +6085,7 @@ case _OrthancPluginService_RegisterDatabaseBackendV4: { - CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end"; + CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end (v4)"; const _OrthancPluginRegisterDatabaseBackendV4& p = *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV4*>(parameters); @@ -5874,7 +6150,7 @@ VoidDatabaseListener listener; { - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); + IPluginStorageArea& storage = *reinterpret_cast<IPluginStorageArea*>(p.storageArea); std::unique_ptr<IDatabaseWrapper::ITransaction> transaction( pimpl_->database_->StartTransaction(TransactionType_ReadWrite, listener)); @@ -5973,7 +6249,7 @@ } - IStorageArea* OrthancPlugins::CreateStorageArea() + IPluginStorageArea* OrthancPlugins::CreateStorageArea() { if (!HasStorageArea()) { @@ -6495,13 +6771,13 @@ transcoder = pimpl_->transcoderCallbacks_.begin(); transcoder != pimpl_->transcoderCallbacks_.end(); ++transcoder) { - MemoryBufferRaii a; + PluginMemoryBuffer32 a; if ((*transcoder) (a.GetObject(), buffer, size, uids.empty() ? NULL : &uids[0], static_cast<uint32_t>(uids.size()), allowNewSopInstanceUid) == OrthancPluginErrorCode_Success) { - a.ToString(target); + a.MoveToString(target); return true; } }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Fri Jun 27 15:00:33 2025 +0200 @@ -56,6 +56,7 @@ #include "../../Sources/IDicomImageDecoder.h" #include "../../Sources/IServerListener.h" #include "../../Sources/ServerJobs/IStorageCommitmentFactory.h" +#include "PluginMemoryBuffer64.h" #include "PluginsManager.h" #include <list> @@ -88,11 +89,14 @@ class HttpClientChunkedAnswer; class HttpServerChunkedReader; class IDicomInstance; - class DicomInstanceFromCallback; class DicomInstanceFromBuffer; class DicomInstanceFromParsed; class WebDavCollection; - + +public: + class DicomInstanceFromCallback; + +private: void RegisterRestCallback(const void* parameters, bool lock); @@ -220,6 +224,28 @@ void ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& parameters); + void ApplyAdoptDicomInstance(const _OrthancPluginAdoptDicomInstance& parameters); + + void ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters); + + void ApplySetAttachmentCustomData(const _OrthancPluginSetAttachmentCustomData& parameters); + + void ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters); + + void ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters); + + void ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters); + + void ApplyCreateKeysValuesIterator(const _OrthancPluginCreateKeysValuesIterator& parameters); + + void ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters); + + void ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters); + + void ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters); + + void ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters); + void ComputeHash(_OrthancPluginService service, const void* parameters); @@ -284,14 +310,14 @@ const DicomInstanceToStore& instance, const Json::Value& simplified) ORTHANC_OVERRIDE; - OrthancPluginReceivedInstanceAction ApplyReceivedInstanceCallbacks(MallocMemoryBuffer& modified, + OrthancPluginReceivedInstanceAction ApplyReceivedInstanceCallbacks(PluginMemoryBuffer64& modified, const void* receivedDicomBuffer, size_t receivedDicomBufferSize, RequestOrigin origin); bool HasStorageArea() const; - IStorageArea* CreateStorageArea(); // To be freed after use + IPluginStorageArea* CreateStorageArea(); // To be freed after use const SharedLibrary& GetStorageAreaLibrary() const;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,200 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Sources/PrecompiledHeadersServer.h" +#include "PluginMemoryBuffer32.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../OrthancFramework/Sources/Toolbox.h" + +#define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc SDK is necessary to use buffers > 4GB, but is currently not available" + + +namespace Orthanc +{ + void PluginMemoryBuffer32::Clear() + { + if (buffer_.size != 0) + { + ::free(buffer_.data); + } + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer32::SanityCheck() const + { + if ((buffer_.data == NULL && buffer_.size != 0) || + (buffer_.data != NULL && buffer_.size == 0)) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + PluginMemoryBuffer32::PluginMemoryBuffer32() + { + buffer_.size = 0; + buffer_.data = NULL; + } + + + void PluginMemoryBuffer32::MoveToString(std::string& target) + { + SanityCheck(); + + target.resize(buffer_.size); + + if (buffer_.size != 0) + { + memcpy(&target[0], buffer_.data, buffer_.size); + } + + Clear(); + } + + + const void* PluginMemoryBuffer32::GetData() const + { + SanityCheck(); + + if (buffer_.size == 0) + { + return NULL; + } + else + { + return buffer_.data; + } + } + + + size_t PluginMemoryBuffer32::GetSize() const + { + SanityCheck(); + return buffer_.size; + } + + + void PluginMemoryBuffer32::Release(OrthancPluginMemoryBuffer* target) + { + SanityCheck(); + + if (target == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + target->data = buffer_.data; + target->size = buffer_.size; + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer32::Release(OrthancPluginMemoryBuffer64* target) + { + SanityCheck(); + + if (target == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + target->data = buffer_.data; + target->size = buffer_.size; + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer32::Resize(size_t size) + { + if (static_cast<size_t>(static_cast<uint32_t>(size)) != size) + { + throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT); + } + + if (size != buffer_.size) + { + Clear(); + + if (size == 0) + { + buffer_.data = NULL; + } + else + { + buffer_.data = ::malloc(size); + + if (buffer_.data == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + + buffer_.size = size; + } + } + + + void PluginMemoryBuffer32::Assign(const void* data, + size_t size) + { + Resize(size); + + if (size != 0) + { + memcpy(buffer_.data, data, size); + } + } + + + void PluginMemoryBuffer32::Assign(const std::string& data) + { + if (data.empty()) + { + Assign(NULL, 0); + } + else + { + Assign(data.c_str(), data.size()); + } + } + + + void PluginMemoryBuffer32::ToJsonObject(Json::Value& target) const + { + SanityCheck(); + + if (!Toolbox::ReadJson(target, buffer_.data, buffer_.size) || + target.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_Plugin, "The plugin has not provided a valid JSON object"); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#if ORTHANC_ENABLE_PLUGINS != 1 +# error The plugin support is disabled +#endif + +#include "../../../OrthancFramework/Sources/MallocMemoryBuffer.h" +#include "../Include/orthanc/OrthancCPlugin.h" + +#include <json/value.h> + +namespace Orthanc +{ + class PluginMemoryBuffer32 : public IMemoryBuffer + { + private: + OrthancPluginMemoryBuffer buffer_; + + void SanityCheck() const; + + public: + PluginMemoryBuffer32(); + + virtual ~PluginMemoryBuffer32() + { + Clear(); + } + + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; + + virtual const void* GetData() const ORTHANC_OVERRIDE; + + virtual size_t GetSize() const ORTHANC_OVERRIDE; + + OrthancPluginMemoryBuffer* GetObject() + { + return &buffer_; + } + + void Release(OrthancPluginMemoryBuffer* target); + + void Release(OrthancPluginMemoryBuffer64* target); + + void Clear(); + + void Resize(size_t size); + + void Assign(const void* data, + size_t size); + + void Assign(const std::string& data); + + void ToJsonObject(Json::Value& target) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,163 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Sources/PrecompiledHeadersServer.h" +#include "PluginMemoryBuffer64.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + + +namespace Orthanc +{ + void PluginMemoryBuffer64::Clear() + { + if (buffer_.size != 0) + { + ::free(buffer_.data); + } + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer64::SanityCheck() const + { + if ((buffer_.data == NULL && buffer_.size != 0) || + (buffer_.data != NULL && buffer_.size == 0)) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + PluginMemoryBuffer64::PluginMemoryBuffer64() + { + buffer_.size = 0; + buffer_.data = NULL; + } + + + void PluginMemoryBuffer64::MoveToString(std::string& target) + { + SanityCheck(); + + target.resize(buffer_.size); + + if (buffer_.size != 0) + { + memcpy(&target[0], buffer_.data, buffer_.size); + } + + Clear(); + } + + + const void* PluginMemoryBuffer64::GetData() const + { + SanityCheck(); + + if (buffer_.size == 0) + { + return NULL; + } + else + { + return buffer_.data; + } + } + + + size_t PluginMemoryBuffer64::GetSize() const + { + SanityCheck(); + return buffer_.size; + } + + + void PluginMemoryBuffer64::Release(OrthancPluginMemoryBuffer64* target) + { + SanityCheck(); + + if (target == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + target->data = buffer_.data; + target->size = buffer_.size; + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer64::Resize(size_t size) + { + if (size != buffer_.size) + { + Clear(); + + if (size == 0) + { + buffer_.data = NULL; + } + else + { + buffer_.data = ::malloc(size); + + if (buffer_.data == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + + buffer_.size = size; + } + } + + + void PluginMemoryBuffer64::Assign(const void* data, + size_t size) + { + Resize(size); + + if (size != 0) + { + memcpy(buffer_.data, data, size); + } + } + + + void PluginMemoryBuffer64::Assign(const std::string& data) + { + if (data.empty()) + { + Assign(NULL, 0); + } + else + { + Assign(data.c_str(), data.size()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,74 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#if ORTHANC_ENABLE_PLUGINS != 1 +# error The plugin support is disabled +#endif + +#include "../../../OrthancFramework/Sources/MallocMemoryBuffer.h" +#include "../Include/orthanc/OrthancCPlugin.h" + +#include <json/value.h> + +namespace Orthanc +{ + class PluginMemoryBuffer64 : public IMemoryBuffer + { + private: + OrthancPluginMemoryBuffer64 buffer_; + + void SanityCheck() const; + + public: + PluginMemoryBuffer64(); + + virtual ~PluginMemoryBuffer64() + { + Clear(); + } + + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; + + virtual const void* GetData() const ORTHANC_OVERRIDE; + + virtual size_t GetSize() const ORTHANC_OVERRIDE; + + OrthancPluginMemoryBuffer64* GetObject() + { + return &buffer_; + } + + void Release(OrthancPluginMemoryBuffer64* target); + + void Clear(); + + void Resize(size_t size); + + void Assign(const void* data, + size_t size); + + void Assign(const std::string& data); + }; +}
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -664,5 +664,116 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + OrthancPluginCompressionType Convert(CompressionType type) + { + switch (type) + { + case CompressionType_None: + return OrthancPluginCompressionType_None; + + case CompressionType_ZlibWithSize: + return OrthancPluginCompressionType_ZlibWithSize; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + CompressionType Convert(OrthancPluginCompressionType type) + { + switch (type) + { + case OrthancPluginCompressionType_None: + return CompressionType_None; + + case OrthancPluginCompressionType_ZlibWithSize: + return CompressionType_ZlibWithSize; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + OrthancPluginStoreStatus Convert(StoreStatus status) + { + switch (status) + { + case StoreStatus_Success: + return OrthancPluginStoreStatus_Success; + + case StoreStatus_AlreadyStored: + return OrthancPluginStoreStatus_AlreadyStored; + + case StoreStatus_Failure: + return OrthancPluginStoreStatus_Failure; + + case StoreStatus_FilteredOut: + return OrthancPluginStoreStatus_FilteredOut; + + case StoreStatus_StorageFull: + return OrthancPluginStoreStatus_StorageFull; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + StoreStatus Convert(OrthancPluginStoreStatus status) + { + switch (status) + { + case OrthancPluginStoreStatus_Success: + return StoreStatus_Success; + + case OrthancPluginStoreStatus_AlreadyStored: + return StoreStatus_AlreadyStored; + + case OrthancPluginStoreStatus_Failure: + return StoreStatus_Failure; + + case OrthancPluginStoreStatus_FilteredOut: + return StoreStatus_FilteredOut; + + case OrthancPluginStoreStatus_StorageFull: + return StoreStatus_StorageFull; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + OrthancPluginQueueOrigin Convert(QueueOrigin origin) + { + switch (origin) + { + case QueueOrigin_Front: + return OrthancPluginQueueOrigin_Front; + + case QueueOrigin_Back: + return OrthancPluginQueueOrigin_Back; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + QueueOrigin Convert(OrthancPluginQueueOrigin origin) + { + switch (origin) + { + case OrthancPluginQueueOrigin_Front: + return QueueOrigin_Front; + + case OrthancPluginQueueOrigin_Back: + return QueueOrigin_Back; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h Fri Jun 27 15:00:33 2025 +0200 @@ -73,6 +73,18 @@ ResourceType Convert(OrthancPluginResourceType type); OrthancPluginConstraintType Convert(ConstraintType constraint); + + OrthancPluginCompressionType Convert(CompressionType type); + + CompressionType Convert(OrthancPluginCompressionType type); + + OrthancPluginStoreStatus Convert(StoreStatus type); + + StoreStatus Convert(OrthancPluginStoreStatus type); + + OrthancPluginQueueOrigin Convert(QueueOrigin type); + + QueueOrigin Convert(OrthancPluginQueueOrigin type); } }
--- a/OrthancServer/Plugins/Engine/PluginsJob.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsJob.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -32,6 +32,7 @@ #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/OrthancException.h" #include "../../../OrthancFramework/Sources/Toolbox.h" +#include "PluginMemoryBuffer32.h" #include <cassert> @@ -152,53 +153,11 @@ return parameters_.getProgress(parameters_.job); } - - namespace - { - class MemoryBufferRaii : public boost::noncopyable - { - private: - OrthancPluginMemoryBuffer buffer_; - - public: - MemoryBufferRaii() - { - buffer_.size = 0; - buffer_.data = NULL; - } - - ~MemoryBufferRaii() - { - if (buffer_.size != 0) - { - free(buffer_.data); - } - } - - OrthancPluginMemoryBuffer* GetObject() - { - return &buffer_; - } - - void ToJsonObject(Json::Value& target) const - { - if ((buffer_.data == NULL && buffer_.size != 0) || - (buffer_.data != NULL && buffer_.size == 0) || - !Toolbox::ReadJson(target, buffer_.data, buffer_.size) || - target.type() != Json::objectValue) - { - throw OrthancException(ErrorCode_Plugin, - "A job plugin must provide a JSON object as its public content and as its serialization"); - } - } - }; - } - void PluginsJob::GetPublicContent(Json::Value& value) const { if (parameters_.getContent != NULL) { - MemoryBufferRaii target; + PluginMemoryBuffer32 target; OrthancPluginErrorCode code = parameters_.getContent(target.GetObject(), parameters_.job); @@ -236,7 +195,7 @@ { if (parameters_.getSerialized != NULL) { - MemoryBufferRaii target; + PluginMemoryBuffer32 target; int32_t code = parameters_.getContent(target.GetObject(), parameters_.job);
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Fri Jun 27 15:00:33 2025 +0200 @@ -80,6 +80,23 @@ } _OrthancPluginDatabaseAnswerType; + typedef enum + { + OrthancPluginDatabaseTransactionType_ReadOnly = 1, + OrthancPluginDatabaseTransactionType_ReadWrite = 2, + OrthancPluginDatabaseTransactionType_INTERNAL = 0x7fffffff + } OrthancPluginDatabaseTransactionType; + + + typedef enum + { + OrthancPluginDatabaseEventType_DeletedAttachment = 1, + OrthancPluginDatabaseEventType_DeletedResource = 2, + OrthancPluginDatabaseEventType_RemainingAncestor = 3, + OrthancPluginDatabaseEventType_INTERNAL = 0x7fffffff + } OrthancPluginDatabaseEventType; + + typedef struct { const char* uuid; @@ -899,7 +916,9 @@ OrthancPluginDatabaseContext* result = NULL; _OrthancPluginRegisterDatabaseBackend params; - if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType)) + if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType)) { return NULL; } @@ -951,7 +970,9 @@ OrthancPluginDatabaseContext* result = NULL; _OrthancPluginRegisterDatabaseBackendV2 params; - if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType)) + if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType)) { return NULL; } @@ -982,23 +1003,6 @@ **/ /*<! @cond Doxygen_Suppress */ - typedef enum - { - OrthancPluginDatabaseTransactionType_ReadOnly = 1, - OrthancPluginDatabaseTransactionType_ReadWrite = 2, - OrthancPluginDatabaseTransactionType_INTERNAL = 0x7fffffff - } OrthancPluginDatabaseTransactionType; - - - typedef enum - { - OrthancPluginDatabaseEventType_DeletedAttachment = 1, - OrthancPluginDatabaseEventType_DeletedResource = 2, - OrthancPluginDatabaseEventType_RemainingAncestor = 3, - OrthancPluginDatabaseEventType_INTERNAL = 0x7fffffff - } OrthancPluginDatabaseEventType; - - typedef struct { OrthancPluginDatabaseEventType type; @@ -1327,8 +1331,6 @@ } OrthancPluginDatabaseBackendV3; -/*<! @endcond */ - typedef struct { @@ -1348,7 +1350,9 @@ { _OrthancPluginRegisterDatabaseBackendV3 params; - if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType)) + if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType)) { return OrthancPluginErrorCode_Plugin; } @@ -1361,7 +1365,10 @@ return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, ¶ms); } - + +/*<! @endcond */ + + #ifdef __cplusplus } #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri Jun 27 15:00:33 2025 +0200 @@ -16,7 +16,7 @@ * - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback(). * - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback(). * - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback(). - * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea2(). + * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea3(). * - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV4(). * - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback(). * - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback(). @@ -121,7 +121,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 12 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 6 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 9 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -421,7 +421,7 @@ } OrthancPluginHttpRequest; - typedef enum + typedef enum { /* Generic services */ _OrthancPluginService_LogInfo = 1, @@ -469,7 +469,21 @@ _OrthancPluginService_SetMetricsIntegerValue = 43, /* New in Orthanc 1.12.1 */ _OrthancPluginService_SetCurrentThreadName = 44, /* New in Orthanc 1.12.2 */ _OrthancPluginService_LogMessage = 45, /* New in Orthanc 1.12.4 */ - + _OrthancPluginService_AdoptDicomInstance = 46, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetAttachmentCustomData = 47, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_SetAttachmentCustomData = 48, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_StoreKeyValue = 49, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DeleteKeyValue = 50, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetKeyValue = 51, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_CreateKeysValuesIterator = 52, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_FreeKeysValuesIterator = 53, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorNext = 54, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetKey = 55, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetValue = 56, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_EnqueueValue = 57, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DequeueValue = 58, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetQueueSize = 59, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_SetStableStatus = 60, /* New in Orthanc 1.12.9 */ /* Registration of callbacks */ _OrthancPluginService_RegisterRestCallback = 1000, @@ -492,6 +506,7 @@ _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017, /* New in Orthanc 1.10.0 */ _OrthancPluginService_RegisterReceivedInstanceCallback = 1018, /* New in Orthanc 1.10.0 */ _OrthancPluginService_RegisterWebDavCollection = 1019, /* New in Orthanc 1.10.1 */ + _OrthancPluginService_RegisterStorageArea3 = 1020, /* New in Orthanc 1.12.8 */ /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -562,7 +577,7 @@ _OrthancPluginService_StorageAreaRemove = 5005, _OrthancPluginService_RegisterDatabaseBackendV3 = 5006, /* New in Orthanc 1.9.2 */ _OrthancPluginService_RegisterDatabaseBackendV4 = 5007, /* New in Orthanc 1.12.0 */ - + /* Primitives for handling images */ _OrthancPluginService_GetImagePixelFormat = 6000, _OrthancPluginService_GetImageWidth = 6001, @@ -752,7 +767,7 @@ /** * The supported types of changes that can be signaled to the change callback. - * Note: this enum is not used to store changes in the DB ! + * Note: This enumeration is not used to store changes in the database! * @ingroup Callbacks **/ typedef enum @@ -791,6 +806,7 @@ OrthancPluginCompressionType_ZlibWithSize = 1, /*!< zlib, prefixed with uncompressed size (uint64_t) */ OrthancPluginCompressionType_Gzip = 2, /*!< Standard gzip compression */ OrthancPluginCompressionType_GzipWithSize = 3, /*!< gzip, prefixed with uncompressed size (uint64_t) */ + OrthancPluginCompressionType_None = 4, /*!< No compression (new in Orthanc 1.12.8) */ _OrthancPluginCompressionType_INTERNAL = 0x7fffffff } OrthancPluginCompressionType; @@ -1130,6 +1146,43 @@ /** + * The store status related to the adoption of a DICOM instance. + **/ + typedef enum + { + OrthancPluginStoreStatus_Success = 0, /*!< The file has been stored/adopted */ + OrthancPluginStoreStatus_AlreadyStored = 1, /*!< The file has already been stored/adopted (only if OverwriteInstances is set to false)*/ + OrthancPluginStoreStatus_Failure = 2, /*!< The file could not be stored/adopted */ + OrthancPluginStoreStatus_FilteredOut = 3, /*!< The file has been filtered out by a Lua script or a plugin */ + OrthancPluginStoreStatus_StorageFull = 4, /*!< The storage is full (only if MaximumStorageSize/MaximumPatientCount is set and MaximumStorageMode is Reject)*/ + + _OrthancPluginStoreStatus_INTERNAL = 0x7fffffff + } OrthancPluginStoreStatus; + + /** + * The supported modes to remove an element from a queue. + **/ + typedef enum + { + OrthancPluginQueueOrigin_Front = 0, /*!< Dequeue from the front of the queue */ + OrthancPluginQueueOrigin_Back = 1, /*!< Dequeue from the back of the queue */ + + _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff + } OrthancPluginQueueOrigin; + + /** + * The "stable" status of a resource. + **/ + typedef enum + { + OrthancPluginStableStatus_Stable = 0, /*!< The resource is stable */ + OrthancPluginStableStatus_Unstable = 1, /*!< The resource is unstable */ + + _OrthancPluginStableStatus_INTERNAL = 0x7fffffff + } OrthancPluginStableStatus; + + + /** * @brief A 32-bit memory buffer allocated by the core system of Orthanc. * * A memory buffer allocated by the core system of Orthanc. When the @@ -1367,8 +1420,7 @@ * @param type The content type corresponding to this file. * @return 0 if success, other value if error. * @ingroup Callbacks - * @deprecated New plugins should use OrthancPluginStorageRead2 - * + * * @warning The "content" buffer *must* have been allocated using * the "malloc()" function of your C standard library (i.e. nor * "new[]", neither a pointer to a buffer). The "free()" function of @@ -1443,6 +1495,83 @@ /** + * @brief Callback for writing to the storage area. + * + * Signature of a callback function that is triggered when Orthanc writes a file to the storage area. + * + * @param customData Custom, plugin-specific data associated with the attachment (out). + * It must be allocated by the plugin using OrthancPluginCreateMemoryBuffer(). The core of Orthanc will free it. + * If the plugin does not generate custom data, leave `customData` unchanged; it will default to an empty value. + * @param uuid The UUID of the file. + * @param content The content of the file (might be compressed data). + * @param size The size of the file. + * @param type The content type corresponding to this file. + * @param compressionType The compression algorithm that was used to encode `content` + * (the absence of compression is indicated using `OrthancPluginCompressionType_None`). + * @param dicomInstance The DICOM instance being stored. Equals `NULL` if not storing a DICOM instance. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate2) ( + OrthancPluginMemoryBuffer* customData, + const char* uuid, + const void* content, + uint64_t size, + OrthancPluginContentType type, + OrthancPluginCompressionType compressionType, + const OrthancPluginDicomInstance* dicomInstance); + + + + /** + * @brief Callback for reading a range of a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * reads a portion of a file from the storage area. Orthanc + * indicates the start position and the length of the range. + * + * @param target Memory buffer where to store the content of the range. + * The memory buffer is allocated and freed by Orthanc. The length of the range + * of interest corresponds to the size of this buffer. + * @param uuid The UUID of the file of interest. + * @param type The content type corresponding to this file. + * @param rangeStart Start position of the requested range in the file. + * @param customData The custom data of the file of interest. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + uint64_t rangeStart, + const void* customData, + uint32_t customDataSize); + + + + /** + * @brief Callback for removing a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * deletes a file from the storage area. + * + * @param uuid The UUID of the file to be removed. + * @param type The content type corresponding to this file. + * @param customData The custom data of the file to be removed. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) ( + const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint32_t customDataSize); + + + /** * @brief Callback to handle the C-Find SCP requests for worklists. * * Signature of a callback function that is triggered when Orthanc @@ -1509,9 +1638,9 @@ * concurrently by different threads of the Web server of * Orthanc. You must implement proper locking if applicable. * - * Note that if you are using HTTP basic authentication, you can extract - * the username from the "Authorization" HTTP header. The value of that header - * contains username:pwd encoded in base64. + * Note that if you are using HTTP basic authentication, you can + * extract the username from the "Authorization" HTTP header. The + * value of that header contains username:pwd encoded in base64. * * @param method The HTTP method used by the request. * @param uri The URI of interest. @@ -2014,13 +2143,17 @@ sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) || sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) || sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) || + sizeof(int32_t) != sizeof(OrthancPluginJobStopReason) || sizeof(int32_t) != sizeof(OrthancPluginConstraintType) || sizeof(int32_t) != sizeof(OrthancPluginMetricsType) || sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) || + sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction) || sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) || sizeof(int32_t) != sizeof(OrthancPluginLogLevel) || - sizeof(int32_t) != sizeof(OrthancPluginLogCategory)) + sizeof(int32_t) != sizeof(OrthancPluginLogCategory) || + sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) || + sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin)) { /* Mismatch in the size of the enumerations */ return 0; @@ -3327,7 +3460,7 @@ * @param read The callback function to read a file from the custom storage area. * @param remove The callback function to remove a file from the custom storage area. * @ingroup Callbacks - * @deprecated Please use OrthancPluginRegisterStorageArea2() + * @deprecated New plugins should use OrthancPluginRegisterStorageArea3() **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea( OrthancPluginContext* context, @@ -4915,6 +5048,8 @@ * @ingroup Callbacks * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiPut()" on * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaCreate( OrthancPluginContext* context, @@ -4959,6 +5094,8 @@ * @ingroup Callbacks * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiGet()" on * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRead( OrthancPluginContext* context, @@ -4998,6 +5135,8 @@ * @ingroup Callbacks * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiDelete()" on * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRemove( OrthancPluginContext* context, @@ -8917,6 +9056,7 @@ * If this feature is not supported by the plugin, this value can be set to NULL. * @param remove The callback function to remove a file from the custom storage area. * @ingroup Callbacks + * @deprecated New plugins should use OrthancPluginRegisterStorageArea3() **/ ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea2( OrthancPluginContext* context, @@ -9368,6 +9508,40 @@ } + typedef struct + { + OrthancPluginStorageCreate2 create; + OrthancPluginStorageReadRange2 readRange; + OrthancPluginStorageRemove2 remove; + } _OrthancPluginRegisterStorageArea3; + + /** + * @brief Register a custom storage area, with support for custom data. + * + * This function registers a custom storage area, to replace the + * built-in way Orthanc stores its files on the filesystem. This + * function must be called during the initialization of the plugin, + * i.e. inside the OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param create The callback function to store a file on the custom storage area. + * @param readRange The callback function to read some range of a file from the custom storage area. + * @param remove The callback function to remove a file from the custom storage area. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3( + OrthancPluginContext* context, + OrthancPluginStorageCreate2 create, + OrthancPluginStorageReadRange2 readRange, + OrthancPluginStorageRemove2 remove) + { + _OrthancPluginRegisterStorageArea3 params; + params.create = create; + params.readRange = readRange; + params.remove = remove; + context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, ¶ms); + } + /** * @brief Signature of a callback function that is triggered when * the Orthanc core requests an operation from the database plugin. @@ -9635,6 +9809,536 @@ } + typedef struct + { + OrthancPluginMemoryBuffer* instanceId; + OrthancPluginMemoryBuffer* attachmentUuid; + OrthancPluginStoreStatus* storeStatus; + const void* dicom; + uint64_t dicomSize; + const void* customData; + uint32_t customDataSize; + } _OrthancPluginAdoptDicomInstance; + + /** + * @brief Adopt a DICOM instance read from the filesystem. + * + * This function requests Orthanc to create a DICOM resource at the + * "Instance" level in its database, using the content of a DICOM + * instance read from the filesystem. The newly created DICOM + * resource is associated with an attachment whose content type is + * "OrthancPluginContentType_Dicom". The attachment is associated + * with the provided custom data. + * + * This function should only be used in combination with a custom + * storage area featuring support for custom data (i.e., installed + * using OrthancPluginRegisterStorageArea3()). The custom storage + * area is responsible for *not* duplicating the DICOM file into the + * storage area of Orthanc, hence the name "Adopt". The support for + * custom data is necessary for the custom storage area to + * distinguish between adopted and non-adopted DICOM instances. + * + * Check out the "AdoptDicomInstance" plugin in the source + * distribution of Orthanc for a working sample: + * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Plugins/Samples/AdoptDicomInstance/ + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instanceId The target memory buffer that will be filled by + * the Orthanc core with the public identifier of the newly created + * instance. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param attachmentUuid The target memory buffer that will be + * filled by the Orthanc core with the UUID of the newly created + * attachment corresponding to the adopted DICOM instance. It must + * be freed with OrthancPluginFreeMemoryBuffer(). + * @param storeStatus Variable that will be filled by the Orthanc core + * with the status of store operation. + * @param dicom Pointer to the DICOM instance read from the filesystem. + * @param dicomSize Size of the DICOM instance. + * @param customData The custom data to associated with the attachment. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginAdoptDicomInstance( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* instanceId, /* out */ + OrthancPluginMemoryBuffer* attachmentUuid, /* out */ + OrthancPluginStoreStatus* storeStatus, /* out */ + const void* dicom, + uint64_t dicomSize, + const void* customData, + uint32_t customDataSize) + { + _OrthancPluginAdoptDicomInstance params; + params.instanceId = instanceId; + params.attachmentUuid = attachmentUuid; + params.storeStatus = storeStatus; + params.dicom = dicom; + params.dicomSize = dicomSize; + params.customData = customData; + params.customDataSize = customDataSize; + + return context->InvokeService(context, _OrthancPluginService_AdoptDicomInstance, ¶ms); + } + + + typedef struct + { + OrthancPluginMemoryBuffer* customData; + const char* attachmentUuid; + } _OrthancPluginGetAttachmentCustomData; + + /** + * @brief Retrieve the custom data associated with an attachment in the Orthanc database. + * + * If no custom data is associated with the attachment of interest, + * the target memory buffer is filled with the NULL value and a zero size. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param customData Memory buffer where to store the retrieved value. It must be freed + * by the plugin by calling OrthancPluginFreeMemoryBuffer(). + * @param attachmentUuid The UUID of the attachment of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetAttachmentCustomData( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* customData, /* out */ + const char* attachmentUuid /* in */) + { + _OrthancPluginGetAttachmentCustomData params; + params.customData = customData; + params.attachmentUuid = attachmentUuid; + + return context->InvokeService(context, _OrthancPluginService_GetAttachmentCustomData, ¶ms); + } + + + typedef struct + { + const char* attachmentUuid; + const void* customData; + uint32_t customDataSize; + } _OrthancPluginSetAttachmentCustomData; + + /** + * @brief Update the custom data associated with an attachment in the Orthanc database. + * + * This function is notably used in the "orthanc-advanced-storage" + * when the plugin moves an attachment. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param attachmentUuid The UUID of the attachment of interest. + * @param customData The value to store. + * @param customDataSize The size of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetAttachmentCustomData( + OrthancPluginContext* context, + const char* attachmentUuid, /* in */ + const void* customData, /* in */ + uint32_t customDataSize /* in */) + { + _OrthancPluginSetAttachmentCustomData params; + params.attachmentUuid = attachmentUuid; + params.customData = customData; + params.customDataSize = customDataSize; + + return context->InvokeService(context, _OrthancPluginService_SetAttachmentCustomData, ¶ms); + } + + + typedef struct + { + const char* storeId; + const char* key; + const void* value; + uint32_t valueSize; + } _OrthancPluginStoreKeyValue; + + /** + * @brief Store a key-value pair in the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to store (note: storeId + key must be unique). + * @param value The value to store. + * @param valueSize The length of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStoreKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key, /* in */ + const void* value, /* in */ + uint32_t valueSize /* in */) + { + _OrthancPluginStoreKeyValue params; + params.storeId = storeId; + params.key = key; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_StoreKeyValue, ¶ms); + } + + + typedef struct + { + const char* storeId; + const char* key; + } _OrthancPluginDeleteKeyValue; + + /** + * @brief Delete a key-value pair from the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to store (note: storeId + key must be unique). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDeleteKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginDeleteKeyValue params; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_DeleteKeyValue, ¶ms); + } + + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* storeId; + const char* key; + } _OrthancPluginGetKeyValue; + + /** + * @brief Get the value associated with a key in the Orthanc key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. the key exists in the key-value store. + * @param target Memory buffer where to store the retrieved value. It must be freed + * by the plugin by calling OrthancPluginFreeMemoryBuffer(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to retrieve from the store (note: storeId + key must be unique). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetKeyValue( + OrthancPluginContext* context, + uint8_t* found, /* out */ + OrthancPluginMemoryBuffer* target, /* out */ + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginGetKeyValue params; + params.found = found; + params.target = target; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_GetKeyValue, ¶ms); + } + + + /** + * @brief Opaque structure that represents an iterator over the keys and values of + * a key-value store. + * @ingroup Callbacks + **/ + typedef struct _OrthancPluginKeysValuesIterator_t OrthancPluginKeysValuesIterator; + + + typedef struct + { + OrthancPluginKeysValuesIterator** target; + const char* storeId; + } _OrthancPluginCreateKeysValuesIterator; + + + /** + * @brief Create an iterator over the key-value pairs of a key-value store in the Orthanc database. + * + * The iterator loops over the keys according to the lexicographical order. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @return The newly allocated iterator, or NULL in the case of an error. + * The iterator must be freed by calling OrthancPluginFreeKeysValuesIterator(). + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginKeysValuesIterator* OrthancPluginCreateKeysValuesIterator( + OrthancPluginContext* context, + const char* storeId) + { + OrthancPluginKeysValuesIterator* target = NULL; + + _OrthancPluginCreateKeysValuesIterator params; + params.target = ⌖ + params.storeId = storeId; + + if (context->InvokeService(context, _OrthancPluginService_CreateKeysValuesIterator, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginFreeKeysValuesIterator; + + /** + * @brief Free an iterator over a key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param iterator The iterator of interest. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeKeysValuesIterator( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + _OrthancPluginFreeKeysValuesIterator params; + params.iterator = iterator; + + context->InvokeService(context, _OrthancPluginService_FreeKeysValuesIterator, ¶ms); + } + + + typedef struct + { + uint8_t* done; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorNext; + + /** + * @brief Read the next element of an iterator over a key-value store. + * + * The iterator loops over the keys according to the lexicographical order. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param done Pointer to a Boolean that is set to "true" iff. the iterator has reached the end of the store. + * @param iterator The iterator of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorNext( + OrthancPluginContext* context, + uint8_t* done, /* out */ + OrthancPluginKeysValuesIterator* iterator /* in */) + { + _OrthancPluginKeysValuesIteratorNext params; + params.done = done; + params.iterator = iterator; + + return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorNext, ¶ms); + } + + + typedef struct + { + const char** target; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorGetKey; + + /** + * @brief Get the current key of an iterator over a key-value store. + * + * Before using this function, the function OrthancPluginKeysValuesIteratorNext() + * must have been called at least once. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param iterator The iterator of interest. + * @return The current key, or NULL in the case of an error. + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginKeysValuesIteratorGetKey( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + const char* target = NULL; + + _OrthancPluginKeysValuesIteratorGetKey params; + params.target = ⌖ + params.iterator = iterator; + + if (context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetKey, ¶ms) == OrthancPluginErrorCode_Success) + { + return target; + } + else + { + return NULL; + } + } + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorGetValue; + + /** + * @brief Get the current value of an iterator over a key-value store. + * + * Before using this function, the function OrthancPluginKeysValuesIteratorNext() + * must have been called at least once. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target Memory buffer where to store the value that has been retrieved from the key-value store. + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param iterator The iterator of interest. + * @return The current value, or NULL in the case of an error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorGetValue( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target /* out */, + OrthancPluginKeysValuesIterator* iterator /* in */) + { + _OrthancPluginKeysValuesIteratorGetValue params; + params.target = target; + params.iterator = iterator; + + return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetValue, ¶ms); + } + + + typedef struct + { + const char* queueId; + const void* value; + uint32_t valueSize; + } _OrthancPluginEnqueueValue; + + /** + * @brief Append a value to the back of a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param value The value to store. + * @param valueSize The size of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginEnqueueValue( + OrthancPluginContext* context, + const char* queueId, /* in */ + const void* value, /* in */ + uint32_t valueSize /* in */) + { + _OrthancPluginEnqueueValue params; + params.queueId = queueId; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_EnqueueValue, ¶ms); + } + + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* queueId; + OrthancPluginQueueOrigin origin; + } _OrthancPluginDequeueValue; + + /** + * @brief Dequeue a value from a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. a value has been dequeued. + * @param target Memory buffer where to store the value that has been retrieved from the queue. + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param origin The position from where the value is dequeued (back for LIFO, front for FIFO). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDequeueValue( + OrthancPluginContext* context, + uint8_t* found, /* out */ + OrthancPluginMemoryBuffer* target, /* out */ + const char* queueId, /* in */ + OrthancPluginQueueOrigin origin /* in */) + { + _OrthancPluginDequeueValue params; + params.found = found; + params.target = target; + params.queueId = queueId; + params.origin = origin; + + return context->InvokeService(context, _OrthancPluginService_DequeueValue, ¶ms); + } + + + typedef struct + { + const char* queueId; + uint64_t* size; + } _OrthancPluginGetQueueSize; + + /** + * @brief Get the number of elements in a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param size The number of elements in the queue. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetQueueSize( + OrthancPluginContext* context, + const char* queueId, /* in */ + uint64_t* size /* out */) + { + _OrthancPluginGetQueueSize params; + params.queueId = queueId; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_GetQueueSize, ¶ms); + } + + + typedef struct + { + const char* resourceId; + OrthancPluginStableStatus stableStatus; + int32_t* statusHasChanged; + } _OrthancPluginSetStableStatus; + + /** + * @brief Change the "Stable" status of a resource. + * Forcing a resource to "Stable" if it is currently "Unstable" will change + * its Stable status AND trigger a new Stable change which will also trigger + * listener callbacks. + * Forcing a resource to "Stable" if it is already "Stable" is a no-op. + * Forcing a resource to "Unstable" will change its Stable status to "Unstable" + * AND reset its stabilization period, no matter of its initial state. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param statusHasChanged Wheter the status has changed (1) or not (0) during the execution of this command. + * @param resourceId The Orthanc identifier of the DICOM resource of interest. + * @param stableStatus The new stable status of the resource of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetStableStatus( + OrthancPluginContext* context, + int32_t* statusHasChanged, /* out */ + const char* resourceId, /* in */ + OrthancPluginStableStatus stableStatus /* in */) + { + _OrthancPluginSetStableStatus params; + params.resourceId = resourceId; + params.stableStatus= stableStatus; + params.statusHasChanged = statusHasChanged; + + return context->InvokeService(context, _OrthancPluginService_SetStableStatus, ¶ms); + } + #ifdef __cplusplus } #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Fri Jun 27 15:00:33 2025 +0200 @@ -55,6 +55,7 @@ int32 compression_type = 5; // opaque "CompressionType" in Orthanc uint64 compressed_size = 6; string compressed_hash = 7; + bytes custom_data = 8; // New in 1.12.8 } enum ResourceType { @@ -94,6 +95,11 @@ ORDERING_CAST_FLOAT = 2; } +enum QueueOrigin { + QUEUE_ORIGIN_FRONT = 0; + QUEUE_ORIGIN_BACK = 1; +} + message ServerIndexChange { int64 seq = 1; int32 change_type = 2; // opaque "ChangeType" in Orthanc @@ -166,6 +172,9 @@ bool has_measure_latency = 7; bool supports_find = 8; // New in Orthanc 1.12.5 bool has_extended_changes = 9; // New in Orthanc 1.12.5 + bool supports_key_value_stores = 10; // New in Orthanc 1.12.8 + bool supports_queues = 11; // New in Orthanc 1.12.8 + bool has_attachment_custom_data = 12; // New in Orthanc 1.12.8 } } @@ -321,6 +330,16 @@ OPERATION_FIND = 50; // New in Orthanc 1.12.5 OPERATION_GET_CHANGES_EXTENDED = 51; // New in Orthanc 1.12.5 OPERATION_COUNT_RESOURCES = 52; // New in Orthanc 1.12.5 + OPERATION_STORE_KEY_VALUE = 53; // New in Orthanc 1.12.8 + OPERATION_DELETE_KEY_VALUE = 54; // New in Orthanc 1.12.8 + OPERATION_GET_KEY_VALUE = 55; // New in Orthanc 1.12.8 + OPERATION_LIST_KEY_VALUES = 56; // New in Orthanc 1.12.8 + OPERATION_ENQUEUE_VALUE = 57; // New in Orthanc 1.12.8 + OPERATION_DEQUEUE_VALUE = 58; // New in Orthanc 1.12.8 + OPERATION_GET_QUEUE_SIZE = 59; // New in Orthanc 1.12.8 + OPERATION_GET_ATTACHMENT_CUSTOM_DATA = 60; // New in Orthanc 1.12.8 + OPERATION_SET_ATTACHMENT_CUSTOM_DATA = 61; // New in Orthanc 1.12.8 + } message Rollback { @@ -974,6 +993,108 @@ } } +message StoreKeyValue { + message Request { + string store_id = 1; + string key = 2; + bytes value = 3; + } + + message Response { + } +} + +message DeleteKeyValue { + message Request { + string store_id = 1; + string key = 2; + } + + message Response { + } +} + +message GetKeyValue { + message Request { + string store_id = 1; + string key = 2; + } + + message Response { + bool found = 1; + bytes value = 2; + } +} + +message ListKeysValues { + message Request { + string store_id = 1; + bool from_first = 2; + string from_key = 3; // Only meaningful if "from_first == false" + uint64 limit = 4; + } + + message Response { + message KeyValue { + string key = 1; + bytes value = 2; + } + repeated KeyValue keys_values = 1; + } +} + +message EnqueueValue { + message Request { + string queue_id = 1; + bytes value = 2; + } + + message Response { + } +} + +message DequeueValue { + message Request { + string queue_id = 1; + QueueOrigin origin = 2; + } + + message Response { + bool found = 1; + bytes value = 2; + } +} + +message GetQueueSize { + message Request { + string queue_id = 1; + } + + message Response { + uint64 size = 1; + } +} + +message GetAttachmentCustomData { + message Request { + string uuid = 1; + } + + message Response { + bytes custom_data = 1; + } +} + +message SetAttachmentCustomData { + message Request { + string uuid = 1; + bytes custom_data = 2; + } + + message Response { + } +} + message TransactionRequest { sfixed64 transaction = 1; TransactionOperation operation = 2; @@ -1031,6 +1152,15 @@ Find.Request find = 150; GetChangesExtended.Request get_changes_extended = 151; Find.Request count_resources = 152; + StoreKeyValue.Request store_key_value = 153; + DeleteKeyValue.Request delete_key_value = 154; + GetKeyValue.Request get_key_value = 155; + ListKeysValues.Request list_keys_values = 156; + EnqueueValue.Request enqueue_value = 157; + DequeueValue.Request dequeue_value = 158; + GetQueueSize.Request get_queue_size = 159; + GetAttachmentCustomData.Request get_attachment_custom_data = 160; + SetAttachmentCustomData.Request set_attachment_custom_data = 161; } message TransactionResponse { @@ -1087,6 +1217,15 @@ repeated Find.Response find = 150; // One message per found resource GetChangesExtended.Response get_changes_extended = 151; CountResources.Response count_resources = 152; + StoreKeyValue.Response store_key_value = 153; + DeleteKeyValue.Response delete_key_value = 154; + GetKeyValue.Response get_key_value = 155; + ListKeysValues.Response list_keys_values = 156; + EnqueueValue.Response enqueue_value = 157; + DequeueValue.Response dequeue_value = 158; + GetQueueSize.Response get_queue_size = 159; + GetAttachmentCustomData.Response get_attachment_custom_data = 160; + SetAttachmentCustomData.Response set_attachment_custom_data = 161; } enum RequestType {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/CMakeLists.txt Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,88 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +cmake_minimum_required(VERSION 2.8...4.0) +cmake_policy(SET CMP0058 NEW) + +project(AdoptDicomInstance) + +SET(PLUGIN_NAME "sample-adopt" CACHE STRING "Name of the plugin") +SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin") + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) + +set(ENABLE_SQLITE OFF) +set(ENABLE_MODULE_IMAGES OFF) +set(ENABLE_MODULE_JOBS OFF) +set(ENABLE_MODULE_DICOM OFF) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) + +include(${CMAKE_CURRENT_LIST_DIR}/../Common/OrthancPluginsExports.cmake) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/WindowsResources.py + ${PLUGIN_VERSION} AdoptDicomInstance AdoptDicomInstance.dll "Sample Orthanc plugin illustrating how to adopt DICOM instances" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/AdoptDicomInstance.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND ADDITIONAL_RESOURCES ${AUTOGENERATED_DIR}/AdoptDicomInstance.rc) +endif() + +add_definitions( + -DHAS_ORTHANC_EXCEPTION=1 + -DPLUGIN_NAME="${PLUGIN_NAME}" + -DPLUGIN_VERSION="${PLUGIN_VERSION}" + -DORTHANC_ENABLE_LOGGING=1 + -DORTHANC_ENABLE_PLUGINS=1 + ) + +include_directories( + ${CMAKE_SOURCE_DIR}/../../Include/ + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/ + ) + +add_library(AdoptDicomInstance SHARED + ${ADDITIONAL_RESOURCES} + ${ORTHANC_CORE_SOURCES} + ${CMAKE_SOURCE_DIR}/Plugin.cpp + ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp + ) + +DefineSourceBasenameForTarget(AdoptDicomInstance) + +set_target_properties( + AdoptDicomInstance PROPERTIES + VERSION ${PLUGIN_VERSION} + SOVERSION ${PLUGIN_VERSION} + ) + +install( + TARGETS AdoptDicomInstance + DESTINATION . + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/Plugin.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,266 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../../../../OrthancFramework/Sources/Logging.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" +#include "../../../../OrthancFramework/Sources/Toolbox.h" +#include "../Common/OrthancPluginCppWrapper.h" + +#include <boost/filesystem.hpp> + + +static boost::filesystem::path storageDirectory_; + + +static std::string GetStorageDirectoryPath(const char* uuid) +{ + if (uuid == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + else + { + return (storageDirectory_ / std::string(uuid)).string(); + } +} + + +#define CATCH_EXCEPTIONS(errorValue) \ + catch (Orthanc::OrthancException& e) \ + { \ + LOG(ERROR) << "Orthanc exception: " << e.What(); \ + return errorValue; \ + } \ + catch (std::runtime_error& e) \ + { \ + LOG(ERROR) << "Native exception: " << e.what(); \ + return errorValue; \ + } \ + catch (...) \ + { \ + return errorValue; \ + } + + +OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData, + const char* uuid, + const void* content, + uint64_t size, + OrthancPluginContentType type, + OrthancPluginCompressionType compressionType, + const OrthancPluginDicomInstance* dicomInstance) +{ + try + { + Json::Value info; + info["IsAdopted"] = false; + + OrthancPlugins::MemoryBuffer buffer; + buffer.Assign(info.toStyledString()); + *customData = buffer.Release(); + + const std::string path = GetStorageDirectoryPath(uuid); + LOG(WARNING) << "Creating non-adopted file: " << path; + Orthanc::SystemToolbox::WriteFile(content, size, path); + + return OrthancPluginErrorCode_Success; + } + CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin); +} + + +OrthancPluginErrorCode StorageReadRange(OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + uint64_t rangeStart, + const void* customData, + uint32_t customDataSize) +{ + try + { + Json::Value info; + if (!Orthanc::Toolbox::ReadJson(info, customData, customDataSize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + std::string path; + + if (info["IsAdopted"].asBool()) + { + path = info["AdoptedPath"].asString(); + LOG(WARNING) << "Reading adopted file from: " << path; + } + else + { + path = GetStorageDirectoryPath(uuid); + LOG(WARNING) << "Reading non-adopted file from: " << path; + } + + std::string range; + Orthanc::SystemToolbox::ReadFileRange(range, path, rangeStart, rangeStart + target->size, true); + + assert(range.size() == target->size); + + if (target->size != 0) + { + memcpy(target->data, range.c_str(), target->size); + } + + return OrthancPluginErrorCode_Success; + } + CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin); +} + + +OrthancPluginErrorCode StorageRemove(const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint32_t customDataSize) +{ + try + { + Json::Value info; + if (!Orthanc::Toolbox::ReadJson(info, customData, customDataSize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + // Only remove non-adopted files (i.e., whose for which Orthanc has the ownership) + if (info["IsAdopted"].asBool()) + { + LOG(WARNING) << "Don't removing adopted file: " << info["AdoptedPath"].asString(); + } + else + { + const std::string path = GetStorageDirectoryPath(uuid); + LOG(WARNING) << "Removing non-adopted file from: " << path; + Orthanc::SystemToolbox::RemoveFile(path); + } + + return OrthancPluginErrorCode_Success; + } + CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin); +} + + +OrthancPluginErrorCode Adopt(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + try + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + return OrthancPluginErrorCode_Success; + } + else + { + const std::string path(reinterpret_cast<const char*>(request->body), request->bodySize); + LOG(WARNING) << "Adopting DICOM instance from path: " << path; + + std::string dicom; + Orthanc::SystemToolbox::ReadFile(dicom, path); + + Json::Value info; + info["IsAdopted"] = true; + info["AdoptedPath"] = path; + + const std::string customData = info.toStyledString(); + + OrthancPluginStoreStatus status; + OrthancPlugins::MemoryBuffer instanceId, attachmentUuid; + + OrthancPluginErrorCode code = OrthancPluginAdoptDicomInstance( + OrthancPlugins::GetGlobalContext(), *instanceId, *attachmentUuid, &status, + dicom.empty() ? NULL : dicom.c_str(), dicom.size(), + customData.empty() ? NULL : customData.c_str(), customData.size()); + + if (code == OrthancPluginErrorCode_Success) + { + const std::string answer = "OK\n"; + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, answer.c_str(), answer.size(), "text/plain"); + return OrthancPluginErrorCode_Success; + } + else + { + return code; + } + } + } + CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin); +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) + { + OrthancPlugins::SetGlobalContext(context, PLUGIN_NAME); + Orthanc::Logging::InitializePluginContext(context, PLUGIN_NAME); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(context) == 0) + { + char info[1024]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + context->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(context, info); + return -1; + } + + OrthancPluginSetDescription2(context, PLUGIN_NAME, "Sample plugin illustrating the adoption of DICOM instances."); + OrthancPluginRegisterStorageArea3(context, StorageCreate, StorageReadRange, StorageRemove); + + try + { + OrthancPlugins::OrthancConfiguration config; + storageDirectory_ = config.GetStringValue("StorageDirectory", "OrthancStorage"); + + Orthanc::SystemToolbox::MakeDirectory(storageDirectory_.string()); + + OrthancPluginRegisterRestCallback(context, "/adopt", Adopt); + } + CATCH_EXCEPTIONS(-1) + + return 0; + } + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + } + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return PLUGIN_NAME; + } + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return PLUGIN_VERSION; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/README Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,13 @@ +This sample plugin illustrates how to use the +"OrthancPluginAdoptDicomInstance()" primitive in the Orthanc SDK. + +The plugin replaces the built-in storage area of Orthanc, by a flat +directory "./OrthancStorage" containing the attachments. + +DICOM instances can then be adopted by typing: + +$ curl http://localhost:8042/adopt -d /tmp/sample.dcm + +An adopted DICOM instance is not copied inside the "OrthancStorage" +folder, but is read from its original location (in the example above, +from "/tmp/sample.dcm").
--- a/OrthancServer/Plugins/Samples/Basic/Plugin.c Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c Fri Jun 27 15:00:33 2025 +0200 @@ -226,6 +226,44 @@ } + +OrthancPluginErrorCode CallbackStabilizeStudy(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(context, output, "POST"); + } + else + { + const char* studyId = request->groups[0]; + int32_t statusHasChanged = 0; + + if (strcmp(request->groups[1], "stabilize") == 0) + { + OrthancPluginSetStableStatus(context, &statusHasChanged, studyId, OrthancPluginStableStatus_Stable); + } + else + { + OrthancPluginSetStableStatus(context, &statusHasChanged, studyId, OrthancPluginStableStatus_Unstable); + } + + if (statusHasChanged) + { + OrthancPluginAnswerBuffer(context, output, "CHANGED\n", 8, "text/plain"); + } + else + { + OrthancPluginAnswerBuffer(context, output, "UNCHANGED\n", 10, "text/plain"); + } + } + + return OrthancPluginErrorCode_Success; +} + + OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request) @@ -572,6 +610,7 @@ OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5); OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom); OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/dicom-web", CallbackDicomWeb); + OrthancPluginRegisterRestCallback(context, "/studies/([^/]+)/(stabilize|unstabilize)", CallbackStabilizeStudy); OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback); OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -220,28 +220,6 @@ } -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) - MemoryBuffer::MemoryBuffer(const void* buffer, - size_t size) - { - uint32_t s = static_cast<uint32_t>(size); - if (static_cast<size_t>(s) != size) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); - } - else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) != - OrthancPluginErrorCode_Success) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); - } - else - { - memcpy(buffer_.data, buffer, size); - } - } -#endif - - void MemoryBuffer::Clear() { if (buffer_.data != NULL) @@ -253,6 +231,41 @@ } +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void MemoryBuffer::Assign(const void* buffer, + size_t size) + { + uint32_t s = static_cast<uint32_t>(size); + if (static_cast<size_t>(s) != size) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + Clear(); + + if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) != + OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + else + { + if (size > 0) + { + memcpy(buffer_.data, buffer, size); + } + } + } +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void MemoryBuffer::Assign(const std::string& s) + { + Assign(s.empty() ? NULL : s.c_str(), s.size()); + } +#endif + + void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other) { Clear(); @@ -673,7 +686,7 @@ { OrthancString str; str.Assign(OrthancPluginDicomBufferToJson - (GetGlobalContext(), GetData(), GetSize(), format, flags, maxStringLength)); + (GetGlobalContext(), reinterpret_cast<const char*>(GetData()), GetSize(), format, flags, maxStringLength)); str.ToJson(target); } @@ -1566,7 +1579,7 @@ { if (!answer.IsEmpty()) { - result.assign(answer.GetData(), answer.GetSize()); + result.assign(reinterpret_cast<const char*>(answer.GetData()), answer.GetSize()); } return true; } @@ -4347,4 +4360,209 @@ } } #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator *iterator) : + iterator_(iterator) + { + if (iterator_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::~Iterator() + { + OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::Iterator::Next() + { + uint8_t done; + OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorNext(OrthancPlugins::GetGlobalContext(), &done, iterator_); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return (done != 0); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + std::string KeyValueStore::Iterator::GetKey() const + { + const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + else + { + return s; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::Iterator::GetValue(std::string& value) const + { + OrthancPlugins::MemoryBuffer valueBuffer; + OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorGetValue(OrthancPlugins::GetGlobalContext(), *valueBuffer, iterator_); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + valueBuffer.ToString(value); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::Store(const std::string& key, + const void* value, + size_t valueSize) + { + if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(), storeId_.c_str(), + key.c_str(), value, static_cast<uint32_t>(valueSize)); + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::GetValue(std::string& value, + const std::string& key) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + OrthancPluginErrorCode code = OrthancPluginGetKeyValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::DeleteKey(const std::string& key) + { + OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(), + storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator* KeyValueStore::CreateIterator() + { + return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str())); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + void Queue::Enqueue(const void* value, + size_t valueSize) + { + if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(), + queueId_.c_str(), value, static_cast<uint32_t>(valueSize)); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + bool Queue::DequeueInternal(std::string& value, + OrthancPluginQueueOrigin origin) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + + OrthancPluginErrorCode code = OrthancPluginDequeueValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, queueId_.c_str(), origin); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + uint64_t Queue::GetSize() + { + uint64_t size = 0; + OrthancPluginErrorCode code = OrthancPluginGetQueueSize(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), &size); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return size; + } + } +#endif }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Fri Jun 27 15:00:33 2025 +0200 @@ -134,6 +134,14 @@ # define HAS_ORTHANC_PLUGIN_LOG_MESSAGE 0 #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 1 +# define HAS_ORTHANC_PLUGIN_QUEUES 1 +#else +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 0 +# define HAS_ORTHANC_PLUGIN_QUEUES 0 +#endif + // Macro to tag a function as having been deprecated #if (__cplusplus >= 201402L) // C++14 @@ -203,13 +211,6 @@ public: MemoryBuffer(); -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) - // This constructor makes a copy of the given buffer in the memory - // handled by the Orthanc core - MemoryBuffer(const void* buffer, - size_t size); -#endif - ~MemoryBuffer() { Clear(); @@ -220,6 +221,16 @@ return &buffer_; } +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + // Copy of the given buffer into the memory managed by the Orthanc core + void Assign(const void* buffer, + size_t size); +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void Assign(const std::string& s); +#endif + // This transfers ownership from "other" to "this" void Assign(OrthancPluginMemoryBuffer& other); @@ -227,11 +238,11 @@ OrthancPluginMemoryBuffer Release(); - const char* GetData() const + const void* GetData() const { if (buffer_.size > 0) { - return reinterpret_cast<const char*>(buffer_.data); + return buffer_.data; } else { @@ -1618,4 +1629,101 @@ bool GetAnswerJson(Json::Value& output) const; }; #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + class KeyValueStore : public boost::noncopyable + { + public: + class Iterator : public boost::noncopyable + { + private: + OrthancPluginKeysValuesIterator *iterator_; + + public: + Iterator(OrthancPluginKeysValuesIterator *iterator); + + ~Iterator(); + + bool Next(); + + std::string GetKey() const; + + void GetValue(std::string& target) const; + }; + + private: + std::string storeId_; + + public: + explicit KeyValueStore(const std::string& storeId) : + storeId_(storeId) + { + } + + const std::string& GetStoreId() const + { + return storeId_; + } + + void Store(const std::string& key, + const void* value, + size_t valueSize); + + void Store(const std::string& key, + const std::string& value) + { + Store(key, value.empty() ? NULL : value.c_str(), value.size()); + } + + bool GetValue(std::string& value, + const std::string& key); + + void DeleteKey(const std::string& key); + + Iterator* CreateIterator(); + }; +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + class Queue : public boost::noncopyable + { + private: + std::string queueId_; + + bool DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin); + + public: + explicit Queue(const std::string& queueId) : + queueId_(queueId) + { + } + + const std::string& GetQueueId() const + { + return queueId_; + } + + void Enqueue(const void* value, + size_t valueSize); + + void Enqueue(const std::string& value) + { + Enqueue(value.empty() ? NULL : value.c_str(), value.size()); + } + + bool DequeueBack(std::string& value) + { + return DequeueInternal(value, OrthancPluginQueueOrigin_Back); + } + + bool DequeueFront(std::string& value) + { + return DequeueInternal(value, OrthancPluginQueueOrigin_Front); + } + + uint64_t GetSize(); + }; +#endif }
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -113,7 +113,7 @@ { try { - std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->Read(uuid, Convert(type))); + std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->ReadWhole(uuid, Convert(type))); // copy from a buffer allocated on plugin's heap into a buffer allocated on core's heap if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, buffer->GetSize()) != OrthancPluginErrorCode_Success)
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -41,6 +41,7 @@ static int globalPropertyId_ = 0; static bool force_ = false; +static bool forceReconstructFiles_ = false; static unsigned int throttleDelay_ = 0; static std::unique_ptr<boost::thread> workerThread_; static bool workerThreadShouldStop_ = false; @@ -568,10 +569,10 @@ { Json::Value result; - if (needsReconstruct || needsReingest ||force_) + if (needsReconstruct || needsReingest || force_) { Json::Value request; - if (needsReingest) + if (needsReingest || forceReconstructFiles_) { request["ReconstructFiles"] = true; } @@ -856,6 +857,13 @@ // any changes in configuration "Force": false, + // New in 1.12.9 + // If "Force" is set to true, forces the "ReconstructFiles" + // option when reconstructing resources even if the plugin + // did not detect any changes in the configuration that + // should trigger a Reconstruct. + "ForceReconstructFiles": false, + // Delay (in seconds) between reconstruction of 2 studies // This avoids overloading Orthanc with the housekeeping // process and leaves room for other operations. @@ -898,6 +906,7 @@ globalPropertyId_ = housekeeper.GetIntegerValue("GlobalPropertyId", 1025); force_ = housekeeper.GetBooleanValue("Force", false); + forceReconstructFiles_ = housekeeper.GetBooleanValue("ForceReconstructFiles", false); throttleDelay_ = housekeeper.GetUnsignedIntegerValue("ThrottleDelay", 5); if (housekeeper.GetJson().isMember("Triggers"))
--- a/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -221,7 +221,7 @@ OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Last-Modified", t.c_str()); - Answer(output, content.GetData(), content.GetSize(), mime); + Answer(output, reinterpret_cast<const char*>(content.GetData()), content.GetSize(), mime); } } }
--- a/OrthancServer/Resources/RunCppCheck-2.17.0.sh Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Resources/RunCppCheck-2.17.0.sh Fri Jun 27 15:00:33 2025 +0200 @@ -106,5 +106,6 @@ ../../OrthancServer/Plugins/Samples/Housekeeper \ ../../OrthancServer/Plugins/Samples/ModalityWorklists \ ../../OrthancServer/Plugins/Samples/MultitenantDicom \ + ../../OrthancServer/Plugins/Samples/AdoptDicomInstance \ \ 2>&1
--- a/OrthancServer/Resources/RunCppCheck.sh Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Fri Jun 27 15:00:33 2025 +0200 @@ -102,5 +102,6 @@ ../../OrthancServer/Plugins/Samples/Housekeeper \ ../../OrthancServer/Plugins/Samples/ModalityWorklists \ ../../OrthancServer/Plugins/Samples/MultitenantDicom \ + ../../OrthancServer/Plugins/Samples/AdoptDicomInstance \ \ 2>&1
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Fri Jun 27 15:00:33 2025 +0200 @@ -56,6 +56,9 @@ bool hasMeasureLatency_; bool hasFindSupport_; bool hasExtendedChanges_; + bool hasAttachmentCustomDataSupport_; + bool hasKeyValueStoresSupport_; + bool hasQueuesSupport_; public: Capabilities() : @@ -66,7 +69,10 @@ hasUpdateAndGetStatistics_(false), hasMeasureLatency_(false), hasFindSupport_(false), - hasExtendedChanges_(false) + hasExtendedChanges_(false), + hasAttachmentCustomDataSupport_(false), + hasKeyValueStoresSupport_(false), + hasQueuesSupport_(false) { } @@ -100,6 +106,16 @@ return hasLabelsSupport_; } + void SetAttachmentCustomDataSupport(bool value) + { + hasAttachmentCustomDataSupport_ = value; + } + + bool HasAttachmentCustomDataSupport() const + { + return hasAttachmentCustomDataSupport_; + } + void SetHasExtendedChanges(bool value) { hasExtendedChanges_ = value; @@ -149,6 +165,26 @@ { return hasFindSupport_; } + + void SetKeyValueStoresSupport(bool value) + { + hasKeyValueStoresSupport_ = value; + } + + bool HasKeyValueStoresSupport() const + { + return hasKeyValueStoresSupport_; + } + + void SetQueuesSupport(bool value) + { + hasQueuesSupport_ = value; + } + + bool HasQueuesSupport() const + { + return hasQueuesSupport_; + } }; @@ -250,6 +286,13 @@ int64_t id, FileContentType contentType) = 0; + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) = 0; + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) = 0; + /** * If "shared" is "true", the property is shared by all the * Orthanc servers that access the same database. If "shared" is @@ -390,6 +433,42 @@ int64_t to, uint32_t limit, const std::set<ChangeType>& filterType) = 0; + + // New in Orthanc 1.12.8 + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) = 0; + + // New in Orthanc 1.12.8 + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) = 0; + + // New in Orthanc 1.12.8 + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) = 0; + + // New in Orthanc 1.12.8 + virtual void ListKeysValues(std::list<std::string>& keys /* out */, + std::list<std::string>& values /* out */, + const std::string& storeId, + bool first, + const std::string& from /* exclusive bound, only used if "first == false" */, + uint64_t limit /* maximum number of elements */) = 0; + + // New in Orthanc 1.12.8 + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) = 0; + + // New in Orthanc 1.12.8 + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) = 0; + + // New in Orthanc 1.12.8, for statistics only + virtual uint64_t GetQueueSize(const std::string& queueId) = 0; }; @@ -456,7 +535,7 @@ virtual unsigned int GetDatabaseVersion() = 0; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) = 0; + IPluginStorageArea& storageArea) = 0; virtual const Capabilities GetDatabaseCapabilities() const = 0;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallDeletedFiles.sql Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,52 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2023 Osimis S.A., Belgium +-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +-- Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +-- +-- This program is free software: you can redistribute it and/or +-- modify it under the terms of the GNU 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 +-- General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. + + +CREATE TABLE DeletedFiles( + uuid TEXT NOT NULL, -- 0 + customData BLOB -- 1 +); + +-- We need to use another AttachedFileDeleted trigger than the legacy one in "Upgrade4To5.sql". +-- +-- We want to keep backward compatibility and avoid changing the database version number (which would force +-- users to upgrade the DB). By keeping backward compatibility, we mean "allow a user to run a previous Orthanc +-- version after it has run this update script". +-- We must preserve the signature of the initial trigger (it is impossible to have 2 triggers on the same event). +-- We tried adding a trigger on "BEFORE DELETE" but then it is being called when running the previous Orthanc +-- which makes it fail. +-- But, we need the customData in the C++ function that is called when a AttachedFiles is deleted. +-- The trick is then to save the customData in a DeletedFiles table. +-- The SignalFileDeleted C++ function will then get the customData from this table and delete the entry. +-- Drawback: if you downgrade Orthanc, the DeletedFiles table will remain and will be populated by the trigger +-- but not consumed by the C++ function -> we consider this is an acceptable drawback for a few people compared +-- to the burden of upgrading the DB. + +DROP TRIGGER IF EXISTS AttachedFileDeleted; + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + INSERT INTO DeletedFiles VALUES(old.uuid, old.customData); + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize, + old.uncompressedMD5, old.compressedMD5 + ); +END;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallKeyValueStoresAndQueues.sql Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,35 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2023 Osimis S.A., Belgium +-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +-- Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +-- +-- This program is free software: you can redistribute it and/or +-- modify it under the terms of the GNU 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 +-- General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. + + +CREATE TABLE KeyValueStores( + storeId TEXT NOT NULL, + key TEXT NOT NULL, + value BLOB NOT NULL, + PRIMARY KEY(storeId, key) -- Prevents duplicates + ); + +CREATE TABLE Queues ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + queueId TEXT NOT NULL, + value BLOB NOT NULL +); + +CREATE INDEX QueuesIndex ON Queues (queueId, id);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql Fri Jun 27 15:00:33 2025 +0200 @@ -0,0 +1,35 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2023 Osimis S.A., Belgium +-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +-- Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +-- +-- This program is free software: you can redistribute it and/or +-- modify it under the terms of the GNU 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 +-- General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. + + +-- +-- This SQLite script installs revision and customData without changing the Orthanc database version +-- + +-- Add new columns for revision +ALTER TABLE Metadata ADD COLUMN revision INTEGER; +ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER; + +-- Add new column for customData +ALTER TABLE AttachedFiles ADD COLUMN customData BLOB; + +-- Record that this upgrade has been performed + +INSERT INTO GlobalProperties VALUES (7, 1); -- GlobalProperty_SQLiteHasCustomDataAndRevision
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Fri Jun 27 15:00:33 2025 +0200 @@ -55,6 +55,7 @@ id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, type INTEGER, value TEXT, + revision INTEGER, -- New in Orthanc 1.12.8 (added in InstallRevisionAndCustomData.sql) PRIMARY KEY(id, type) ); @@ -67,6 +68,8 @@ compressionType INTEGER, uncompressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) compressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) + revision INTEGER, -- New in Orthanc 1.12.8 (added in InstallRevisionAndCustomData.sql) + customData BLOB, -- New in Orthanc 1.12.8 (added in InstallDeletedFiles.sql) PRIMARY KEY(id, fileType) ); @@ -95,22 +98,12 @@ patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE ); --- New in Orthanc 1.12.0 -CREATE TABLE Labels( - id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, - label TEXT NOT NULL, - PRIMARY KEY(id, label) -- Prevents duplicates - ); - CREATE INDEX ChildrenIndex ON Resources(parentId); CREATE INDEX PublicIndex ON Resources(publicId); CREATE INDEX ResourceTypeIndex ON Resources(resourceType); CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); --- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up --- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); --- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); -- The 3 following indexes were added in Orthanc 0.8.5 (database v5) CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); @@ -119,18 +112,6 @@ CREATE INDEX ChangesIndex ON Changes(internalId); --- New in Orthanc 1.12.0 -CREATE INDEX LabelsIndex1 ON Labels(id); -CREATE INDEX LabelsIndex2 ON Labels(label); -- This index allows efficient lookups - -CREATE TRIGGER AttachedFileDeleted -AFTER DELETE ON AttachedFiles -BEGIN - SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, - old.compressionType, old.compressedSize, - -- These 2 arguments are new in Orthanc 0.7.3 (database v4) - old.uncompressedMD5, old.compressedMD5); -END; CREATE TRIGGER ResourceDeleted AFTER DELETE ON Resources @@ -156,6 +137,27 @@ END; +-- new in Orthanc 1.5.1 -------------------------- equivalent to InstallTrackAttachmentsSize.sql +${INSTALL_TRACK_ATTACHMENTS_SIZE} + + +-- new in Orthanc 1.12.0 ------------------------- equivalent to InstallLabelsTable.sql +${INSTALL_LABELS_TABLE} + + +-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallDeletedFiles.sql +${INSTALL_DELETED_FILES} + + +-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallKeyValueStoresAndQueues.sql +${INSTALL_KEY_VALUE_STORES_AND_QUEUES} + + +-- Track the fact that the "revision" column exists in the "Metadata" and "AttachedFiles" +-- tables, and that the "customData" column exists in the "AttachedFiles" table +INSERT INTO GlobalProperties VALUES (7, 1); -- GlobalProperty_SQLiteHasCustomDataAndRevision + + -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration INSERT INTO GlobalProperties VALUES (1, "6");
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -39,8 +39,10 @@ #include <OrthancServerResources.h> #include <stdio.h> +#include <boost/algorithm/string/replace.hpp> #include <boost/lexical_cast.hpp> + namespace Orthanc { static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec) @@ -384,19 +386,22 @@ } } - boost::mutex::scoped_lock lock_; + boost::recursive_mutex::scoped_lock lock_; IDatabaseListener& listener_; SignalRemainingAncestor& signalRemainingAncestor_; + bool hasFastTotalSize_; public: - TransactionBase(boost::mutex& mutex, + TransactionBase(boost::recursive_mutex& mutex, SQLite::Connection& db, IDatabaseListener& listener, - SignalRemainingAncestor& signalRemainingAncestor) : + SignalRemainingAncestor& signalRemainingAncestor, + bool hasFastTotalSize) : UnitTestsTransaction(db), lock_(mutex), listener_(listener), - signalRemainingAncestor_(signalRemainingAncestor) + signalRemainingAncestor_(signalRemainingAncestor), + hasFastTotalSize_(hasFastTotalSize) { } @@ -410,8 +415,9 @@ const FileInfo& attachment, int64_t revision) ORTHANC_OVERRIDE { - // TODO - REVISIONS - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5) VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5, revision, customData) " + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); s.BindInt64(0, id); s.BindInt(1, attachment.GetContentType()); s.BindString(2, attachment.GetUuid()); @@ -420,10 +426,11 @@ s.BindInt(5, attachment.GetCompressionType()); s.BindString(6, attachment.GetUncompressedMD5()); s.BindString(7, attachment.GetCompressedMD5()); + s.BindInt(8, revision); + s.BindBlob(9, attachment.GetCustomData()); s.Run(); } - virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, const DatabaseDicomTagConstraints& lookup, @@ -473,10 +480,12 @@ #define C3_STRING_1 3 #define C4_STRING_2 4 #define C5_STRING_3 5 -#define C6_INT_1 6 -#define C7_INT_2 7 -#define C8_BIG_INT_1 8 -#define C9_BIG_INT_2 9 +#define C6_STRING_4 6 +#define C7_INT_1 7 +#define C8_INT_2 8 +#define C9_INT_3 9 +#define C10_BIG_INT_1 10 +#define C11_BIG_INT_2 11 #define QUERY_LOOKUP 1 #define QUERY_MAIN_DICOM_TAGS 2 @@ -525,6 +534,19 @@ } + static void ReadCustomData(FileInfo& info, + SQLite::Statement& statement, + int column) + { + std::string customData; + if (!statement.ColumnIsNull(column) && + statement.ColumnBlobAsString(column, &customData)) + { + info.SwapCustomData(customData); + } + } + + virtual void ExecuteFind(FindResponse& response, const FindRequest& request, const Capabilities& capabilities) ORTHANC_OVERRIDE @@ -588,10 +610,12 @@ " Lookup.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " " FROM Lookup "; // need one instance info ? (part 2: execute the queries) @@ -605,10 +629,12 @@ " instancePublicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " instanceInternalId AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " instanceInternalId AS c10_big_int1, " + " NULL AS c11_big_int2 " " FROM OneInstance "; sql += " UNION SELECT" @@ -618,10 +644,12 @@ " Metadata.value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " Metadata.type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " Metadata.type AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " " FROM OneInstance " " INNER JOIN Metadata ON Metadata.id = OneInstance.instanceInternalId "; @@ -632,10 +660,12 @@ " uuid AS c3_string1, " " uncompressedMD5 AS c4_string2, " " compressedMD5 AS c5_string3, " - " fileType AS c6_int1, " - " compressionType AS c7_int2, " - " compressedSize AS c8_big_int1, " - " uncompressedSize AS c9_big_int2 " + " customData AS c6_string4, " + " fileType AS c7_int1, " + " compressionType AS c8_int2, " + " revision AS c9_int3, " + " compressedSize AS c10_big_int1, " + " uncompressedSize AS c11_big_int2 " " FROM OneInstance " " INNER JOIN AttachedFiles ON AttachedFiles.id = OneInstance.instanceInternalId "; @@ -651,10 +681,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId "; } @@ -669,10 +701,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Metadata ON Metadata.id = Lookup.internalId "; } @@ -687,10 +721,12 @@ " uuid AS c3_string1, " " uncompressedMD5 AS c4_string2, " " compressedMD5 AS c5_string3, " - " fileType AS c6_int1, " - " compressionType AS c7_int2, " - " compressedSize AS c8_big_int1, " - " uncompressedSize AS c9_big_int2 " + " customData AS c6_string4, " + " fileType AS c7_int1, " + " compressionType AS c8_int2, " + " revision AS c9_int3, " + " compressedSize AS c10_big_int1, " + " uncompressedSize AS c11_big_int2 " "FROM Lookup " "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId "; } @@ -706,10 +742,12 @@ " label AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Labels ON Labels.id = Lookup.internalId "; } @@ -726,10 +764,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId "; @@ -745,10 +785,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId "; @@ -766,10 +808,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " @@ -786,10 +830,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " @@ -808,10 +854,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))); @@ -827,10 +875,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " @@ -847,10 +897,12 @@ " parentLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId " " INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "; @@ -866,10 +918,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))) + ") "; @@ -885,10 +939,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " @@ -907,10 +963,12 @@ " childLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "; } @@ -926,10 +984,12 @@ " NULL AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " COUNT(*) AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " COUNT(*) AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId GROUP BY Lookup.internalId "; } @@ -945,10 +1005,12 @@ " grandChildLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "; @@ -964,10 +1026,12 @@ " NULL AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " COUNT(*) AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " COUNT(*) AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId GROUP BY Lookup.internalId "; @@ -983,10 +1047,12 @@ " grandGrandChildLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " @@ -1002,10 +1068,12 @@ " NULL AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " COUNT(*) AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " COUNT(*) AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " @@ -1043,19 +1111,21 @@ case QUERY_ATTACHMENTS: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); - FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), - s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2), - static_cast<CompressionType>(s.ColumnInt(C7_INT_2)), - s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3)); - res.AddAttachment(file, 0 /* TODO - REVISIONS */); + FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)), + s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2), + static_cast<CompressionType>(s.ColumnInt(C8_INT_2)), + s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3)); + ReadCustomData(file, s, C6_STRING_4); + + res.AddAttachment(file, s.ColumnInt(C9_INT_3)); }; break; case QUERY_MAIN_DICOM_TAGS: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddStringDicomTag(requestLevel, - static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), - static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C8_INT_2)), s.ColumnString(C3_STRING_1)); }; break; @@ -1063,8 +1133,8 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 1), - static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), - static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C8_INT_2)), s.ColumnString(C3_STRING_1)); }; break; @@ -1072,8 +1142,8 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 2), - static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), - static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C8_INT_2)), s.ColumnString(C3_STRING_1)); }; break; @@ -1081,7 +1151,7 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 1), - DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))), + DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))), s.ColumnString(C3_STRING_1)); }; break; @@ -1089,7 +1159,7 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 2), - DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))), + DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))), s.ColumnString(C3_STRING_1)); }; break; @@ -1097,31 +1167,31 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddMetadata(static_cast<ResourceType>(requestLevel), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), - s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), + s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2)); }; break; case QUERY_PARENT_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddMetadata(static_cast<ResourceType>(requestLevel - 1), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), - s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), + s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2)); }; break; case QUERY_GRAND_PARENT_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddMetadata(static_cast<ResourceType>(requestLevel - 2), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), - s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), + s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2)); }; break; case QUERY_CHILDREN_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 1), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1)); }; break; @@ -1129,7 +1199,7 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 2), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1)); }; break; @@ -1170,21 +1240,21 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 1), - static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + static_cast<uint64_t>(s.ColumnInt64(C7_INT_1))); }; break; case QUERY_GRAND_CHILDREN_COUNT: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 2), - static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + static_cast<uint64_t>(s.ColumnInt64(C7_INT_1))); }; break; case QUERY_GRAND_GRAND_CHILDREN_COUNT: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 3), - static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + static_cast<uint64_t>(s.ColumnInt64(C7_INT_1))); }; break; case QUERY_ONE_INSTANCE_IDENTIFIER: @@ -1196,16 +1266,18 @@ case QUERY_ONE_INSTANCE_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); - res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), s.ColumnString(C3_STRING_1)); + res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1)); }; break; case QUERY_ONE_INSTANCE_ATTACHMENTS: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); - FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), - s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2), - static_cast<CompressionType>(s.ColumnInt(C7_INT_2)), - s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3)); + FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)), + s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2), + static_cast<CompressionType>(s.ColumnInt(C8_INT_2)), + s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3)); + ReadCustomData(file, s, C6_STRING_4); + res.AddOneInstanceAttachment(file); }; break; @@ -1312,6 +1384,32 @@ } } + void DeleteDeletedFile(const std::string& uuid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DeletedFiles WHERE uuid=?"); + s.BindString(0, uuid); + s.Run(); + } + + void GetDeletedFileCustomData(std::string& customData, const std::string& uuid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT customData FROM DeletedFiles WHERE uuid=?"); + s.BindString(0, uuid); + + if (s.Step()) + { + if (s.ColumnIsNull(0) || + !s.ColumnBlobAsString(0, &customData)) + { + customData.clear(); + } + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } virtual void GetAllMetadata(std::map<MetadataType, std::string>& target, int64_t id) ORTHANC_OVERRIDE @@ -1597,23 +1695,39 @@ virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE { - // Old SQL query that was used in Orthanc <= 1.5.0: - // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0"); - s.Run(); - return static_cast<uint64_t>(s.ColumnInt64(0)); + std::unique_ptr<SQLite::Statement> statement; + + if (hasFastTotalSize_) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0")); + } + else + { + // Old SQL query that was used in Orthanc <= 1.5.0: + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles")); + } + + statement->Run(); + return static_cast<uint64_t>(statement->ColumnInt64(0)); } virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE { - // Old SQL query that was used in Orthanc <= 1.5.0: - // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1"); - s.Run(); - return static_cast<uint64_t>(s.ColumnInt64(0)); + std::unique_ptr<SQLite::Statement> statement; + + if (hasFastTotalSize_) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1")); + } + else + { + // Old SQL query that was used in Orthanc <= 1.5.0: + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles")); + } + + statement->Run(); + return static_cast<uint64_t>(statement->ColumnInt64(0)); } @@ -1687,7 +1801,7 @@ { SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT uuid, uncompressedSize, compressionType, compressedSize, " - "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); + "uncompressedMD5, compressedMD5, revision, customData FROM AttachedFiles WHERE id=? AND fileType=?"); s.BindInt64(0, id); s.BindInt(1, contentType); @@ -1704,11 +1818,46 @@ static_cast<CompressionType>(s.ColumnInt(2)), s.ColumnInt64(3), s.ColumnString(5)); - revision = 0; // TODO - REVISIONS + ReadCustomData(attachment, s, 7); + revision = s.ColumnInt(6); return true; } } + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT customData FROM AttachedFiles WHERE uuid=?"); + s.BindString(0, attachmentUuid); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + if (s.ColumnIsNull(0)) + { + customData.clear(); + } + else if (!s.ColumnBlobAsString(0, &customData)) + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "UPDATE AttachedFiles SET customData=? WHERE uuid=?"); + s.BindBlob(0, customData, customDataSize); + s.BindString(1, attachmentUuid); + s.Run(); + } virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, @@ -1739,7 +1888,7 @@ MetadataType type) ORTHANC_OVERRIDE { SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM Metadata WHERE id=? AND type=?"); + "SELECT value, revision FROM Metadata WHERE id=? AND type=?"); s.BindInt64(0, id); s.BindInt(1, type); @@ -1750,7 +1899,7 @@ else { target = s.ColumnString(0); - revision = 0; // TODO - REVISIONS + revision = s.ColumnInt(1); return true; } } @@ -1922,11 +2071,11 @@ const std::string& value, int64_t revision) ORTHANC_OVERRIDE { - // TODO - REVISIONS - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata (id, type, value) VALUES(?, ?, ?)"); + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata (id, type, value, revision) VALUES(?, ?, ?, ?)"); s.BindInt64(0, id); s.BindInt(1, type); s.BindString(2, value); + s.BindInt(3, revision); s.Run(); } @@ -2027,6 +2176,170 @@ target.insert(s.ColumnString(0)); } } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO KeyValueStores (storeId, key, value) VALUES(?, ?, ?)"); + s.BindString(0, storeId); + s.BindString(1, key); + s.BindBlob(2, value, valueSize); + s.Run(); + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM KeyValueStores WHERE storeId = ? AND key = ?"); + s.BindString(0, storeId); + s.BindString(1, key); + s.Run(); + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM KeyValueStores WHERE storeId=? AND key=?"); + s.BindString(0, storeId); + s.BindString(1, key); + + if (!s.Step()) + { + // No value found + return false; + } + else + { + if (!s.ColumnBlobAsString(0, &value)) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + else + { + return true; + } + } + } + + // New in Orthanc 1.12.8 + virtual void ListKeysValues(std::list<std::string>& keys /* out */, + std::list<std::string>& values /* out */, + const std::string& storeId, + bool first, + const std::string& from /* only used if "first == false" */, + uint64_t limit) ORTHANC_OVERRIDE + { + int64_t actualLimit = limit; + if (limit == 0) + { + actualLimit = -1; // In SQLite, "if negative, there is no upper bound on the number of rows returned" + } + + std::unique_ptr<SQLite::Statement> statement; + + if (first) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? ORDER BY key ASC LIMIT ?")); + statement->BindString(0, storeId); + statement->BindInt64(1, actualLimit); + } + else + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? AND key>? ORDER BY key ASC LIMIT ?")); + statement->BindString(0, storeId); + statement->BindString(1, from); + statement->BindInt64(2, actualLimit); + } + + while (statement->Step()) + { + std::string value; + if (!statement->ColumnBlobAsString(1, &value)) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + keys.push_back(statement->ColumnString(0)); + values.push_back(value); + } + } + + + // New in Orthanc 1.12.8 + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + if (static_cast<size_t>(static_cast<int>(valueSize)) != valueSize) + { + throw OrthancException(ErrorCode_NotEnoughMemory, "Value is too large for a SQLite database"); + } + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO Queues (queueId, value) VALUES (?, ?)"); + s.BindString(0, queueId); + s.BindBlob(1, value, valueSize); + s.Run(); + } + + // New in Orthanc 1.12.8 + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + int64_t rowId; + std::unique_ptr<SQLite::Statement> s; + + switch (origin) + { + case QueueOrigin_Front: + s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id ASC LIMIT 1")); + break; + + case QueueOrigin_Back: + s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id DESC LIMIT 1")); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + s->BindString(0, queueId); + if (!s->Step()) + { + // No value found + return false; + } + else + { + rowId = s->ColumnInt64(0); + + if (!s->ColumnBlobAsString(1, &value)) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + SQLite::Statement s2(db_, SQLITE_FROM_HERE, + "DELETE FROM Queues WHERE id = ?"); + s2.BindInt64(0, rowId); + s2.Run(); + + return true; + } + } + + // New in Orthanc 1.12.8 + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT COUNT(*) FROM Queues WHERE queueId=?"); + s.BindString(0, queueId); + s.Step(); + return s.ColumnInt64(0); + } }; @@ -2055,6 +2368,11 @@ { if (sqlite_.activeTransaction_ != NULL) { + std::string id = context.GetStringValue(0); + + std::string customData; + sqlite_.activeTransaction_->GetDeletedFileCustomData(customData, id); + std::string uncompressedMD5, compressedMD5; if (!context.IsNullValue(5)) @@ -2074,8 +2392,10 @@ static_cast<CompressionType>(context.GetIntValue(3)), static_cast<uint64_t>(context.GetInt64Value(4)), compressedMD5); + info.SwapCustomData(customData); sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info); + sqlite_.activeTransaction_->DeleteDeletedFile(id); } } }; @@ -2120,20 +2440,40 @@ SQLiteDatabaseWrapper& that_; std::unique_ptr<SQLite::Transaction> transaction_; int64_t initialDiskSize_; + bool isNested_; + + // Rationale for the isNested_ field: + // This was added while implementing the DelayedDeletion part of the advanced-storage plugin. + // When Orthanc deletes an attachment, a SQLite transaction is created to delete the attachment from + // the SQLite DB and, while the transaction is still active, the StorageRemove callback is called. + // The DelayedDeleter does not delete the file directly but, instead, it queues it for deletion. + // Queuing is done through the Orthanc SDK that creates a RW transaction (because it is a generic function). + // Since there is already an active RW transaction, this "nested" transaction does not need to perform anything + // in its Begin/Commit since this will be performed at higher level by the current activeTransaction_. + // However, in case of Rollback, this nested transaction must call the top level transaction Rollback. public: ReadWriteTransaction(SQLiteDatabaseWrapper& that, - IDatabaseListener& listener) : - TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), + IDatabaseListener& listener, + bool hasFastTotalSize) : + TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_, hasFastTotalSize), that_(that), - transaction_(new SQLite::Transaction(that_.db_)) + transaction_(new SQLite::Transaction(that_.db_)), + isNested_(false) { if (that_.activeTransaction_ != NULL) { - throw OrthancException(ErrorCode_InternalError); + if (dynamic_cast<SQLiteDatabaseWrapper::ReadWriteTransaction*>(that_.activeTransaction_) == NULL) + { + throw OrthancException(ErrorCode_InternalError, "Unable to create a nested RW transaction, the current transaction is not a RW transaction"); + } + + isNested_ = true; } - - that_.activeTransaction_ = this; + else + { + that_.activeTransaction_ = this; + } #if defined(NDEBUG) // Release mode @@ -2146,26 +2486,42 @@ virtual ~ReadWriteTransaction() { - assert(that_.activeTransaction_ != NULL); - that_.activeTransaction_ = NULL; + if (!isNested_) + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } } - void Begin() + virtual void Begin() { - transaction_->Begin(); + if (!isNested_) + { + transaction_->Begin(); + } } virtual void Rollback() ORTHANC_OVERRIDE { - transaction_->Rollback(); + if (isNested_) + { + that_.activeTransaction_->Rollback(); + } + else + { + transaction_->Rollback(); + } } virtual void Commit(int64_t fileSizeDelta /* only used in debug */) ORTHANC_OVERRIDE { - transaction_->Commit(); - - assert(initialDiskSize_ + fileSizeDelta >= 0 && - initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(GetTotalCompressedSize())); + if (!isNested_) + { + transaction_->Commit(); + + assert(initialDiskSize_ + fileSizeDelta >= 0 && + initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(GetTotalCompressedSize())); + } } }; @@ -2174,25 +2530,34 @@ { private: SQLiteDatabaseWrapper& that_; + bool isNested_; // see explanation on the ReadWriteTransaction public: ReadOnlyTransaction(SQLiteDatabaseWrapper& that, - IDatabaseListener& listener) : - TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), - that_(that) + IDatabaseListener& listener, + bool hasFastTotalSize) : + TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_, hasFastTotalSize), + that_(that), + isNested_(false) { if (that_.activeTransaction_ != NULL) { - throw OrthancException(ErrorCode_InternalError); + isNested_ = true; + // throw OrthancException(ErrorCode_InternalError); } - - that_.activeTransaction_ = this; + else + { + that_.activeTransaction_ = this; + } } virtual ~ReadOnlyTransaction() { - assert(that_.activeTransaction_ != NULL); - that_.activeTransaction_ = NULL; + if (!isNested_) + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } } virtual void Rollback() ORTHANC_OVERRIDE @@ -2214,11 +2579,14 @@ signalRemainingAncestor_(NULL), version_(0) { - // TODO: implement revisions in SQLite + dbCapabilities_.SetRevisionsSupport(true); dbCapabilities_.SetFlushToDisk(true); dbCapabilities_.SetLabelsSupport(true); dbCapabilities_.SetHasExtendedChanges(true); dbCapabilities_.SetHasFindSupport(HasIntegratedFind()); + dbCapabilities_.SetKeyValueStoresSupport(true); + dbCapabilities_.SetQueuesSupport(true); + dbCapabilities_.SetAttachmentCustomDataSupport(true); db_.Open(path); } @@ -2228,11 +2596,14 @@ signalRemainingAncestor_(NULL), version_(0) { - // TODO: implement revisions in SQLite + dbCapabilities_.SetRevisionsSupport(true); dbCapabilities_.SetFlushToDisk(true); dbCapabilities_.SetLabelsSupport(true); dbCapabilities_.SetHasExtendedChanges(true); dbCapabilities_.SetHasFindSupport(HasIntegratedFind()); + dbCapabilities_.SetKeyValueStoresSupport(true); + dbCapabilities_.SetQueuesSupport(true); + dbCapabilities_.SetAttachmentCustomDataSupport(true); db_.OpenInMemory(); } @@ -2245,10 +2616,29 @@ } + static void ExecuteEmbeddedScript(SQLite::Connection& db, + ServerResources::FileResourceId resourceId) + { + std::string script; + ServerResources::GetFileResource(script, resourceId); + db.Execute(script); + } + + + static void InjectEmbeddedScript(std::string& sql, + const std::string& name, + ServerResources::FileResourceId resourceId) + { + std::string script; + ServerResources::GetFileResource(script, resourceId); + boost::replace_all(sql, name, script); + } + + void SQLiteDatabaseWrapper::Open() { { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); if (signalRemainingAncestor_ != NULL) { @@ -2283,6 +2673,12 @@ LOG(INFO) << "Creating the database"; std::string query; ServerResources::GetFileResource(query, ServerResources::PREPARE_DATABASE); + + InjectEmbeddedScript(query, "${INSTALL_TRACK_ATTACHMENTS_SIZE}", ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); + InjectEmbeddedScript(query, "${INSTALL_LABELS_TABLE}", ServerResources::INSTALL_LABELS_TABLE); + InjectEmbeddedScript(query, "${INSTALL_DELETED_FILES}", ServerResources::INSTALL_DELETED_FILES); + InjectEmbeddedScript(query, "${INSTALL_KEY_VALUE_STORES_AND_QUEUES}", ServerResources::INSTALL_KEY_VALUE_STORES_AND_QUEUES); + db_.Execute(query); } @@ -2317,18 +2713,35 @@ tmp != "1") { LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments"; - std::string query; - ServerResources::GetFileResource(query, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); - db_.Execute(query); + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); } // New in Orthanc 1.12.0 if (!db_.DoesTableExist("Labels")) { LOG(INFO) << "Installing the \"Labels\" table"; - std::string query; - ServerResources::GetFileResource(query, ServerResources::INSTALL_LABELS_TABLE); - db_.Execute(query); + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_LABELS_TABLE); + } + + // New in Orthanc 1.12.8 + if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasRevisionAndCustomData, true /* unused in SQLite */) + || tmp != "1") + { + LOG(INFO) << "Upgrading SQLite schema to support revision and customData"; + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA); + } + + // New in Orthanc 1.12.8 + if (!db_.DoesTableExist("DeletedFiles")) + { + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_DELETED_FILES); + } + + // New in Orthanc 1.12.8 + if (!db_.DoesTableExist("KeyValueStores")) + { + LOG(INFO) << "Installing the \"KeyValueStores\" and \"Queues\" tables"; + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_KEY_VALUE_STORES_AND_QUEUES); } } @@ -2339,7 +2752,7 @@ void SQLiteDatabaseWrapper::Close() { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); // close and delete the WAL when exiting properly -> the DB is stored in a single file (no more -wal and -shm files) db_.Execute("PRAGMA JOURNAL_MODE=DELETE;"); db_.Close(); @@ -2358,9 +2771,9 @@ void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); if (targetVersion != 6) { @@ -2401,33 +2814,59 @@ VoidDatabaseListener listener; { - std::unique_ptr<ITransaction> transaction(StartTransaction(TransactionType_ReadWrite, listener)); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Patient); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Study); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Series); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Instance); + ReadWriteTransaction transaction(*this, listener, false /* GetTotalSizeIsFast necessitates the table "GlobalIntegers" */); + transaction.Begin(); + + // ReconstructMaindDicomTags uses LookupAttachment that needs revision and customData. Since we don't want to maintain a legacy version + // of LookupAttachment, we modify the table now) + LOG(INFO) << "First upgrading SQLite schema to support revision and customData to be able to reconstruct main DICOM tags"; + std::string query; + ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA); + db_.Execute(query); + + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Patient); + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Study); + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Series); + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Instance); db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" + boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";"); - transaction->Commit(0); + transaction.Commit(0); } version_ = 6; } + } + // class RaiiTransactionLogger + // { + // TransactionType type_; + // public: + // RaiiTransactionLogger(TransactionType type) + // : type_(type) + // { + // LOG(INFO) << "IN " << (type_ == TransactionType_ReadOnly ? "RO" : "RW"); + // } + // ~RaiiTransactionLogger() + // { + // LOG(INFO) << "OUT " << (type_ == TransactionType_ReadOnly ? "RO" : "RW"); + // } + // }; IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction(TransactionType type, IDatabaseListener& listener) { + // RaiiTransactionLogger logger(type); + switch (type) { case TransactionType_ReadOnly: - return new ReadOnlyTransaction(*this, listener); // This is a no-op transaction in SQLite (thanks to mutex) + return new ReadOnlyTransaction(*this, listener, true); // This is a no-op transaction in SQLite (thanks to mutex) case TransactionType_ReadWrite: { std::unique_ptr<ReadWriteTransaction> transaction; - transaction.reset(new ReadWriteTransaction(*this, listener)); + transaction.reset(new ReadWriteTransaction(*this, listener, true)); transaction->Begin(); return transaction.release(); } @@ -2440,7 +2879,7 @@ void SQLiteDatabaseWrapper::FlushToDisk() { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); db_.FlushToDisk(); }
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Fri Jun 27 15:00:33 2025 +0200 @@ -27,7 +27,7 @@ #include "../../../OrthancFramework/Sources/SQLite/Connection.h" -#include <boost/thread/mutex.hpp> +#include <boost/thread/recursive_mutex.hpp> namespace Orthanc { @@ -47,7 +47,7 @@ class ReadWriteTransaction; class LookupFormatter; - boost::mutex mutex_; + boost::recursive_mutex mutex_; SQLite::Connection db_; TransactionBase* activeTransaction_; SignalRemainingAncestor* signalRemainingAncestor_; @@ -88,7 +88,7 @@ } virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE {
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -1394,28 +1394,36 @@ } - bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type, - const std::string& publicId) + bool StatelessDatabaseOperations::LookupResource(int64_t& internalId, + ResourceType& type, + const std::string& publicId) { - class Operations : public ReadOnlyOperationsT3<bool&, ResourceType&, const std::string&> + class Operations : public ReadOnlyOperationsT4<bool&, int64_t&, ResourceType&, const std::string&> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { // TODO - CANDIDATE FOR "TransactionType_Implicit" - int64_t id; - tuple.get<0>() = transaction.LookupResource(id, tuple.get<1>(), tuple.get<2>()); + tuple.get<0>() = transaction.LookupResource(tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); } }; bool found; Operations operations; - operations.Apply(*this, found, type, publicId); + operations.Apply(*this, found, internalId, type, publicId); return found; } + bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type, + const std::string& publicId) + { + int64_t internalId; + return LookupResource(internalId, type, publicId); + } + + bool StatelessDatabaseOperations::LookupParent(std::string& target, const std::string& publicId, ResourceType parentType) @@ -3200,6 +3208,24 @@ return db_.GetDatabaseCapabilities().HasFindSupport(); } + bool StatelessDatabaseOperations::HasAttachmentCustomDataSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport(); + } + + bool StatelessDatabaseOperations::HasKeyValueStoresSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasKeyValueStoresSupport(); + } + + bool StatelessDatabaseOperations::HasQueuesSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasQueuesSupport(); + } + void StatelessDatabaseOperations::ExecuteCount(uint64_t& count, const FindRequest& request) { @@ -3320,4 +3346,413 @@ } } } + + void StatelessDatabaseOperations::StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (value == NULL && + valueSize > 0) + { + throw OrthancException(ErrorCode_NullPointer); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& storeId_; + const std::string& key_; + const void* value_; + size_t valueSize_; + + public: + Operations(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) : + storeId_(storeId), + key_(key), + value_(value), + valueSize_(valueSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.StoreKeyValue(storeId_, key_, value_, valueSize_); + } + }; + + Operations operations(storeId, key, value, valueSize); + Apply(operations); + } + + void StatelessDatabaseOperations::DeleteKeyValue(const std::string& storeId, + const std::string& key) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& storeId_; + const std::string& key_; + + public: + Operations(const std::string& storeId, + const std::string& key) : + storeId_(storeId), + key_(key) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.DeleteKeyValue(storeId_, key_); + } + }; + + Operations operations(storeId, key); + Apply(operations); + } + + bool StatelessDatabaseOperations::GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public ReadOnlyOperationsT3<std::string&, const std::string&, const std::string& > + { + bool found_; + public: + Operations(): + found_(false) + {} + + bool HasFound() + { + return found_; + } + + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + found_ = transaction.GetKeyValue(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + Operations operations; + operations.Apply(*this, value, storeId, key); + + return operations.HasFound(); + } + + void StatelessDatabaseOperations::EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) + { + if (queueId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (value == NULL && + valueSize > 0) + { + throw OrthancException(ErrorCode_NullPointer); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& queueId_; + const void* value_; + size_t valueSize_; + + public: + Operations(const std::string& queueId, + const void* value, + size_t valueSize) : + queueId_(queueId), + value_(value), + valueSize_(valueSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.EnqueueValue(queueId_, value_, valueSize_); + } + }; + + Operations operations(queueId, value, valueSize); + Apply(operations); + } + + bool StatelessDatabaseOperations::DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) + { + if (queueId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& queueId_; + std::string& value_; + QueueOrigin origin_; + bool found_; + + public: + Operations(std::string& value, + const std::string& queueId, + QueueOrigin origin) : + queueId_(queueId), + value_(value), + origin_(origin), + found_(false) + { + } + + bool HasFound() + { + return found_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + found_ = transaction.DequeueValue(value_, queueId_, origin_); + } + }; + + Operations operations(value, queueId, origin); + Apply(operations); + + return operations.HasFound(); + } + + uint64_t StatelessDatabaseOperations::GetQueueSize(const std::string& queueId) + { + if (queueId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public ReadOnlyOperationsT2<uint64_t&, const std::string& > + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + tuple.get<0>() = transaction.GetQueueSize(tuple.get<1>()); + } + }; + + uint64_t size; + + Operations operations; + operations.Apply(*this, size, queueId); + + return size; + } + + + void StatelessDatabaseOperations::GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) + { + class Operations : public ReadOnlyOperationsT2<std::string&, const std::string& > + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.GetAttachmentCustomData(tuple.get<0>(), tuple.get<1>()); + } + }; + + Operations operations; + operations.Apply(*this, customData, attachmentUuid); + } + + + void StatelessDatabaseOperations::SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& attachmentUuid_; + const void* customData_; + size_t customDataSize_; + + public: + Operations(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) : + attachmentUuid_(attachmentUuid), + customData_(customData), + customDataSize_(customDataSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.SetAttachmentCustomData(attachmentUuid_, customData_, customDataSize_); + } + }; + + Operations operations(attachmentUuid, customData, customDataSize); + Apply(operations); + } + + + StatelessDatabaseOperations::KeysValuesIterator::KeysValuesIterator(StatelessDatabaseOperations& db, + const std::string& storeId) : + db_(db), + state_(State_Waiting), + storeId_(storeId), + limit_(100) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + bool StatelessDatabaseOperations::KeysValuesIterator::Next() + { + if (state_ == State_Done) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (state_ == State_Available) + { + assert(currentKey_ != keys_.end()); + assert(currentValue_ != values_.end()); + ++currentKey_; + ++currentValue_; + + if (currentKey_ != keys_.end() && + currentValue_ != values_.end()) + { + // A value is still available in the last keys-values block fetched from the database + return true; + } + else if (currentKey_ != keys_.end() || + currentValue_ != values_.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + class Operations : public ReadOnlyOperationsT6<std::list<std::string>&, std::list<std::string>&, const std::string&, bool, const std::string&, uint64_t> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ListKeysValues(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>()); + } + }; + + if (state_ == State_Waiting) + { + keys_.clear(); + values_.clear(); + + Operations operations; + operations.Apply(db_, keys_, values_, storeId_, true, "", limit_); + } + else + { + assert(state_ == State_Available); + if (keys_.empty()) + { + state_ = State_Done; + return false; + } + else + { + const std::string lastKey = keys_.back(); + keys_.clear(); + values_.clear(); + + Operations operations; + operations.Apply(db_, keys_, values_, storeId_, false, lastKey, limit_); + } + } + + if (keys_.size() != values_.size()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (limit_ != 0 && + keys_.size() > limit_) + { + // The database plugin has returned too many key-value pairs + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (keys_.empty() && + values_.empty()) + { + state_ = State_Done; + return false; + } + else if (!keys_.empty() && + !values_.empty()) + { + state_ = State_Available; + currentKey_ = keys_.begin(); + currentValue_ = values_.begin(); + return true; + } + else + { + throw OrthancException(ErrorCode_InternalError); // Should never happen + } + } + + const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetKey() const + { + if (state_ == State_Available) + { + return *currentKey_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetValue() const + { + if (state_ == State_Available) + { + return *currentValue_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Fri Jun 27 15:00:33 2025 +0200 @@ -226,6 +226,12 @@ return transaction_.LookupAttachment(attachment, revision, id, contentType); } + void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) + { + return transaction_.GetAttachmentCustomData(customData, attachmentUuid); + } + bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) @@ -293,6 +299,28 @@ { transaction_.ExecuteExpand(response, capabilities, request, identifier); } + + bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) + { + return transaction_.GetKeyValue(value, storeId, key); + } + + uint64_t GetQueueSize(const std::string& queueId) + { + return transaction_.GetQueueSize(queueId); + } + + void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) + { + return transaction_.ListKeysValues(keys, values, storeId, first, from, limit); + } }; @@ -428,6 +456,41 @@ { transaction_.RemoveLabel(id, label); } + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) + { + transaction_.StoreKeyValue(storeId, key, value, valueSize); + } + + void DeleteKeyValue(const std::string& storeId, + const std::string& key) + { + transaction_.DeleteKeyValue(storeId, key); + } + + void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) + { + transaction_.EnqueueValue(queueId, value, valueSize); + } + + bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) + { + return transaction_.DequeueValue(value, queueId, origin); + } + + void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) + { + return transaction_.SetAttachmentCustomData(attachmentUuid, customData, customDataSize); + } }; @@ -523,6 +586,13 @@ /* out */ uint64_t& countSeries, /* out */ uint64_t& countInstances); + void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid); + + void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize); + bool LookupAttachment(FileInfo& attachment, int64_t& revision, ResourceType level, @@ -544,6 +614,12 @@ bool HasExtendedChanges(); bool HasFindSupport(); + + bool HasAttachmentCustomDataSupport(); + + bool HasKeyValueStoresSupport(); + + bool HasQueuesSupport(); void GetExportedResources(Json::Value& target, int64_t since, @@ -615,6 +691,10 @@ bool GetAllMainDicomTags(DicomMap& result, const std::string& instancePublicId); + bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId); + bool LookupResourceType(ResourceType& type, const std::string& publicId); @@ -724,5 +804,80 @@ void ExecuteCount(uint64_t& count, const FindRequest& request); + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize); + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) + { + StoreKeyValue(storeId, key, value.empty() ? NULL : value.c_str(), value.size()); + } + + void DeleteKeyValue(const std::string& storeId, + const std::string& key); + + bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key); + + void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize); + + void EnqueueValue(const std::string& queueId, + const std::string& value) + { + EnqueueValue(queueId, value.empty() ? NULL : value.c_str(), value.size()); + } + + bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin); + + uint64_t GetQueueSize(const std::string& queueId); + + class KeysValuesIterator : public boost::noncopyable + { + private: + enum State + { + State_Waiting, + State_Available, + State_Done + }; + + StatelessDatabaseOperations& db_; + State state_; + std::string storeId_; + uint64_t limit_; + std::list<std::string> keys_; + std::list<std::string> values_; + std::list<std::string>::const_iterator currentKey_; + std::list<std::string>::const_iterator currentValue_; + + public: + KeysValuesIterator(StatelessDatabaseOperations& db, + const std::string& storeId); + + void SetLimit(uint64_t limit) + { + limit_ = limit; + } + + uint64_t GetLimit() const + { + return limit_; + } + + bool Next(); + + const std::string& GetKey() const; + + const std::string& GetValue() const; + }; }; }
--- a/OrthancServer/Sources/LuaScripting.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/LuaScripting.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -570,6 +570,60 @@ return 1; } + // Syntax in Lua: SetStableStatus(resourceId, true) + int LuaScripting::SetStableStatus(lua_State* state) + { + ServerContext* serverContext = GetServerContext(state); + if (serverContext == NULL) + { + LOG(ERROR) << "Lua: The Orthanc API is unavailable"; + lua_pushnil(state); + return 1; + } + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs < 1 || nArgs > 3 || + !lua_isstring(state, 1) || // Resource + !lua_isboolean(state, 2)) // newStateIsStable + { + LOG(ERROR) << "Lua: Bad parameters to SetStableStatus()"; + lua_pushnil(state); + return 1; + } + + const char* resourceId = lua_tostring(state, 1); + bool newStateIsStable = lua_toboolean(state, 2); + + try + { + bool hasStateChanged = false; + + if (serverContext->GetIndex().SetStableStatus(hasStateChanged, resourceId, newStateIsStable)) + { + if (hasStateChanged) + { + lua_pushboolean(state, 1); + } + else + { + lua_pushboolean(state, 0); + } + + return 1; + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Lua: " << e.What(); + } + + LOG(ERROR) << "Lua: Error in SetStableStatus() for Resource: " << resourceId; + lua_pushnil(state); + + return 1; + } + // Syntax in Lua: GetOrthancConfiguration() int LuaScripting::GetOrthancConfiguration(lua_State *state) @@ -760,6 +814,7 @@ lua_.RegisterFunction("RestApiPut", RestApiPut); lua_.RegisterFunction("RestApiDelete", RestApiDelete); lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration); + lua_.RegisterFunction("SetStableStatus", SetStableStatus); LOG(INFO) << "Initializing Lua for the event handler"; LoadGlobalConfiguration();
--- a/OrthancServer/Sources/LuaScripting.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/LuaScripting.h Fri Jun 27 15:00:33 2025 +0200 @@ -62,6 +62,7 @@ static int RestApiPut(lua_State *state); static int RestApiDelete(lua_State *state); static int GetOrthancConfiguration(lua_State *state); + static int SetStableStatus(lua_State* state); size_t ParseOperation(LuaJobManager::Lock& lock, const std::string& operation,
--- a/OrthancServer/Sources/OrthancInitialization.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -56,6 +56,7 @@ #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/HttpClient.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/OrthancException.h" @@ -482,19 +483,6 @@ } } - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE - { - if (type != FileContentType_Dicom) - { - return storage_.Read(uuid, type); - } - else - { - throw OrthancException(ErrorCode_UnknownResource); - } - } - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, @@ -510,9 +498,9 @@ } } - virtual bool HasReadRange() const ORTHANC_OVERRIDE + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE { - return storage_.HasReadRange(); + return storage_.HasEfficientReadRange(); } virtual void Remove(const std::string& uuid, @@ -527,7 +515,7 @@ } - static IStorageArea* CreateFilesystemStorage() + static IPluginStorageArea* CreateFilesystemStorage() { static const char* const SYNC_STORAGE_AREA = "SyncStorageArea"; static const char* const STORE_DICOM = "StoreDicom"; @@ -547,12 +535,12 @@ if (lock.GetConfiguration().GetBooleanParameter(STORE_DICOM, true)) { - return new FilesystemStorage(storageDirectory.string(), fsyncOnWrite); + return new PluginStorageAreaAdapter(new FilesystemStorage(storageDirectory.string(), fsyncOnWrite)); } else { LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode"; - return new FilesystemStorageWithoutDicom(storageDirectory.string(), fsyncOnWrite); + return new PluginStorageAreaAdapter(new FilesystemStorageWithoutDicom(storageDirectory.string(), fsyncOnWrite)); } } @@ -563,7 +551,7 @@ } - IStorageArea* CreateStorageArea() + IPluginStorageArea* CreateStorageArea() { return CreateFilesystemStorage(); }
--- a/OrthancServer/Sources/OrthancInitialization.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.h Fri Jun 27 15:00:33 2025 +0200 @@ -35,7 +35,7 @@ IDatabaseWrapper* CreateDatabaseWrapper(); - IStorageArea* CreateStorageArea(); + IPluginStorageArea* CreateStorageArea(); void SetGlobalVerbosity(Verbosity verbosity);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -2666,7 +2666,7 @@ } int64_t newRevision; - context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(), + context.AddAttachment(newRevision, publicId, level, StringToContentType(name), call.GetBodyData(), call.GetBodySize(), hasOldRevision, oldRevision, oldMD5); SetBufferContentETag(call.GetOutput(), newRevision, call.GetBodyData(), call.GetBodySize()); // New in Orthanc 1.9.2
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -95,6 +95,8 @@ static const char* const HAS_LABELS = "HasLabels"; static const char* const CAPABILITIES = "Capabilities"; static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges"; + static const char* const HAS_KEY_VALUE_STORES = "HasKeyValueStores"; + static const char* const HAS_QUEUES = "HasQueues"; static const char* const HAS_EXTENDED_FIND = "HasExtendedFind"; static const char* const READ_ONLY = "ReadOnly"; @@ -211,6 +213,8 @@ result[CAPABILITIES] = Json::objectValue; result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges(); result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport(); + result[CAPABILITIES][HAS_KEY_VALUE_STORES] = OrthancRestApi::GetIndex(call).HasKeyValueStoresSupport(); + result[CAPABILITIES][HAS_QUEUES] = OrthancRestApi::GetIndex(call).HasQueuesSupport(); call.GetOutput().AnswerJson(result); }
--- a/OrthancServer/Sources/ServerContext.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -356,7 +356,7 @@ ServerContext::ServerContext(IDatabaseWrapper& database, - IStorageArea& area, + IPluginStorageArea& area, bool unitTesting, size_t maxCompletedJobs, bool readOnly, @@ -613,10 +613,11 @@ void ServerContext::RemoveFile(const std::string& fileUuid, - FileContentType type) + FileContentType type, + const std::string& customData) { StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - accessor.Remove(fileUuid, type); + accessor.Remove(fileUuid, type, customData); } @@ -625,6 +626,37 @@ StoreInstanceMode mode, bool isReconstruct) { + FileInfo adoptedFileNotUsed; + + return StoreAfterTranscoding(resultPublicId, + dicom, + mode, + isReconstruct, + false, + adoptedFileNotUsed); + } + + ServerContext::StoreResult ServerContext::AdoptDicomInstance(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + const FileInfo& adoptedFile) + { + return StoreAfterTranscoding(resultPublicId, + dicom, + mode, + false, + true, + adoptedFile); + } + + + ServerContext::StoreResult ServerContext::StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + bool isReconstruct, + bool isAdoption, + const FileInfo& adoptedFile) + { bool overwrite; switch (mode) { @@ -727,19 +759,25 @@ // TODO Should we use "gzip" instead? CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); - FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), - FileContentType_Dicom, compression, storeMD5_); + StatelessDatabaseOperations::Attachments attachments; + FileInfo dicomInfo; - ServerIndex::Attachments attachments; - attachments.push_back(dicomInfo); + if (!isAdoption) + { + accessor.Write(dicomInfo, dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom, compression, storeMD5_, &dicom); + attachments.push_back(dicomInfo); + } + else + { + attachments.push_back(adoptedFile); + } FileInfo dicomUntilPixelData; if (hasPixelDataOffset && - (!area_.HasReadRange() || + (!area_.HasEfficientReadRange() || compressionEnabled_)) { - dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, - FileContentType_DicomUntilPixelData, compression, storeMD5_); + accessor.Write(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL); attachments.push_back(dicomUntilPixelData); } @@ -784,7 +822,10 @@ if (result.GetStatus() != StoreStatus_Success) { - accessor.Remove(dicomInfo); + if (!isAdoption) + { + accessor.Remove(dicomInfo); + } if (dicomUntilPixelData.IsValid()) { @@ -798,7 +839,14 @@ switch (result.GetStatus()) { case StoreStatus_Success: - LOG(INFO) << "New instance stored (" << resultPublicId << ")"; + if (isAdoption) + { + LOG(INFO) << "New instance adopted (" << resultPublicId << ")"; + } + else + { + LOG(INFO) << "New instance stored (" << resultPublicId << ")"; + } break; case StoreStatus_AlreadyStored: @@ -846,7 +894,7 @@ { if (e.GetErrorCode() == ErrorCode_InexistentTag) { - summary.LogMissingTagsForStore(); + LOG(ERROR) << summary.FormatMissingTagsForStore(); } throw; @@ -860,12 +908,12 @@ { DicomInstanceToStore* dicom = &receivedDicom; +#if ORTHANC_ENABLE_PLUGINS == 1 // WARNING: The scope of "modifiedBuffer" and "modifiedDicom" must // be the same as that of "dicom" - MallocMemoryBuffer modifiedBuffer; + PluginMemoryBuffer64 modifiedBuffer; std::unique_ptr<DicomInstanceToStore> modifiedDicom; -#if ORTHANC_ENABLE_PLUGINS == 1 if (HasPlugins()) { // New in Orthanc 1.10.0 @@ -1038,8 +1086,8 @@ StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); accessor.Read(content, attachment); - FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(), - content.size(), attachmentType, compression, storeMD5_); + FileInfo modified; + accessor.Write(modified, content.empty() ? NULL : content.c_str(), content.size(), attachmentType, compression, storeMD5_, NULL); try { @@ -1221,7 +1269,7 @@ if (hasPixelDataOffset && - area_.HasReadRange() && + area_.HasEfficientReadRange() && LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments) && attachment.GetCompressionType() == CompressionType_None) { @@ -1299,13 +1347,13 @@ index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset, boost::lexical_cast<std::string>(pixelDataOffset)); - if (!area_.HasReadRange() || + if (!area_.HasEfficientReadRange() || compressionEnabled_) { int64_t newRevision; - AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData, + AddAttachment(newRevision, instancePublicId, ResourceType_Instance, FileContentType_DicomUntilPixelData, dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset, - false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); + false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); } } } @@ -1374,7 +1422,7 @@ return true; } - if (!area_.HasReadRange()) + if (!area_.HasEfficientReadRange()) { return false; } @@ -1533,6 +1581,7 @@ bool ServerContext::AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -1546,7 +1595,11 @@ CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_); + + assert(attachmentType != FileContentType_Dicom && attachmentType != FileContentType_DicomUntilPixelData); // this method can not be used to store instances + + FileInfo attachment; + accessor.Write(attachment, data, size, attachmentType, compression, storeMD5_, NULL); try {
--- a/OrthancServer/Sources/ServerContext.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/ServerContext.h Fri Jun 27 15:00:33 2025 +0200 @@ -43,7 +43,7 @@ namespace Orthanc { class DicomInstanceToStore; - class IStorageArea; + class IPluginStorageArea; class JobsEngine; class MetricsRegistry; class OrthancPlugins; @@ -193,7 +193,7 @@ virtual void SignalJobFailure(const std::string& jobId) ORTHANC_OVERRIDE; ServerIndex index_; - IStorageArea& area_; + IPluginStorageArea& area_; StorageCache storageCache_; bool compressionEnabled_; @@ -269,9 +269,17 @@ StoreInstanceMode mode, bool isReconstruct); + StoreResult StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + bool isReconstruct, + bool isAdoption, + const FileInfo& adoptedFile); + // This method must only be called from "ServerIndex"! void RemoveFile(const std::string& fileUuid, - FileContentType type); + FileContentType type, + const std::string& customData); // This DicomModification object is intended to be used as a // "rules engine" when de-identifying logs for C-Find, C-Get, and @@ -305,7 +313,7 @@ }; ServerContext(IDatabaseWrapper& database, - IStorageArea& area, + IPluginStorageArea& area, bool unitTesting, size_t maxCompletedJobs, bool readOnly, @@ -344,6 +352,7 @@ bool AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -355,6 +364,11 @@ DicomInstanceToStore& dicom, StoreInstanceMode mode); + StoreResult AdoptDicomInstance(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + const FileInfo& adoptedFile); + StoreResult TranscodeAndStore(std::string& resultPublicId, DicomInstanceToStore* dicom, StoreInstanceMode mode,
--- a/OrthancServer/Sources/ServerEnumerations.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Fri Jun 27 15:00:33 2025 +0200 @@ -173,6 +173,7 @@ GlobalProperty_AnonymizationSequence = 3, GlobalProperty_JobsRegistry = 5, GlobalProperty_GetTotalSizeIsFast = 6, // New in Orthanc 1.5.2 + GlobalProperty_SQLiteHasRevisionAndCustomData = 7, // New in Orthanc 1.12.8 GlobalProperty_Modalities = 20, // New in Orthanc 1.5.0 GlobalProperty_Peers = 21, // New in Orthanc 1.5.0 @@ -262,6 +263,11 @@ Warnings_007_MissingRequestedTagsNotReadFromDisk // new in Orthanc 1.12.5 }; + enum QueueOrigin + { + QueueOrigin_Front, + QueueOrigin_Back + }; void InitializeServerEnumerations();
--- a/OrthancServer/Sources/ServerIndex.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -45,12 +45,14 @@ struct FileToRemove { private: - std::string uuid_; - FileContentType type_; + std::string uuid_; + std::string customData_; + FileContentType type_; public: explicit FileToRemove(const FileInfo& info) : - uuid_(info.GetUuid()), + uuid_(info.GetUuid()), + customData_(info.GetCustomData()), type_(info.GetContentType()) { } @@ -60,6 +62,11 @@ return uuid_; } + const std::string& GetCustomData() const + { + return customData_; + } + FileContentType GetContentType() const { return type_; @@ -93,7 +100,7 @@ { try { - context_.RemoveFile(it->GetUuid(), it->GetContentType()); + context_.RemoveFile(it->GetUuid(), it->GetContentType(), it->GetCustomData()); } catch (OrthancException& e) { @@ -305,7 +312,7 @@ bool ServerIndex::IsUnstableResource(ResourceType type, int64_t id) { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); return unstableResources_.Contains(std::make_pair(type, id)); } @@ -387,7 +394,7 @@ void ServerIndex::SetMaximumPatientCount(unsigned int count) { { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumPatients_ = count; if (count == 0) @@ -407,7 +414,7 @@ void ServerIndex::SetMaximumStorageSize(uint64_t size) { { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumStorageSize_ = size; if (size == 0) @@ -426,7 +433,7 @@ void ServerIndex::SetMaximumStorageMode(MaxStorageMode mode) { { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumStorageMode_ = mode; if (mode == MaxStorageMode_Recycle) @@ -479,7 +486,7 @@ int64_t stableId; { - boost::mutex::scoped_lock lock(that->monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(that->monitoringMutex_); if (!that->unstableResources_.IsEmpty() && that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge)) @@ -498,43 +505,52 @@ } } - try - { - /** - * WARNING: Don't protect the calls to "LogChange()" using - * "monitoringMutex_", as this could lead to deadlocks in - * other threads (typically, if "Store()" is being running in - * another thread, which leads to calls to "MarkAsUnstable()", - * which leads to two lockings of "monitoringMutex_"). - **/ - switch (stableLevel) - { - case ResourceType_Patient: - that->LogChange(stableId, ChangeType_StablePatient, stablePayload.GetPublicId(), ResourceType_Patient); - break; - - case ResourceType_Study: - that->LogChange(stableId, ChangeType_StableStudy, stablePayload.GetPublicId(), ResourceType_Study); - break; - - case ResourceType_Series: - that->LogChange(stableId, ChangeType_StableSeries, stablePayload.GetPublicId(), ResourceType_Series); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - catch (OrthancException& e) - { - LOG(ERROR) << "Cannot log a change about a stable resource into the database"; - } + // must not be protected by monitoringMutex_ + that->LogStableChange(stableLevel, stableId, stablePayload.GetPublicId()); } } LOG(INFO) << "Closing the monitor thread for stable resources"; } + void ServerIndex::LogStableChange(ResourceType stableLevel, + int64_t stableId, + const std::string& publicId) + { + try + { + /** + * WARNING: Don't protect the calls to "LogChange()" using + * "monitoringMutex_", as this could lead to deadlocks in + * other threads (typically, if "Store()" is being running in + * another thread, which leads to calls to "MarkAsUnstable()", + * which leads to two lockings of "monitoringMutex_"). + **/ + switch (stableLevel) + { + case ResourceType_Patient: + LogChange(stableId, ChangeType_StablePatient, publicId, ResourceType_Patient); + break; + + case ResourceType_Study: + LogChange(stableId, ChangeType_StableStudy, publicId, ResourceType_Study); + break; + + case ResourceType_Series: + LogChange(stableId, ChangeType_StableSeries, publicId, ResourceType_Series); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Cannot log a change about a stable resource into the database"; + } + } + + void ServerIndex::MarkAsUnstable(ResourceType type, int64_t id, @@ -545,13 +561,59 @@ type == ResourceType_Series); { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); UnstableResourcePayload payload(publicId); unstableResources_.AddOrMakeMostRecent(std::make_pair(type, id), payload); //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id; } } + bool ServerIndex::SetStableStatus(bool& statusHasChanged, + const std::string& resourceId, + bool setNewStatusToStable) + { + int64_t id; + ResourceType type; + + if (LookupResource(id, type, resourceId)) + { + if (setNewStatusToStable) + { + { + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); + + if (IsUnstableResource(type, id)) + { + unstableResources_.Invalidate(std::pair<ResourceType, int64_t>(type, id)); + statusHasChanged = true; + } + } + + if (statusHasChanged) + { + // must not be protected by monitoringMutex_ + LogStableChange(type, id, resourceId); + } + } + else + { + { + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); + + statusHasChanged = !IsUnstableResource(type, id); + + // no matter what was the status, we mark it as unstable to reset its stabilization period + MarkAsUnstable(type, id, resourceId); + } + + } + + return true; + } + + return false; + } + StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata, const DicomMap& dicomSummary, @@ -571,7 +633,7 @@ MaxStorageMode maximumStorageMode; { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumStorageSize = maximumStorageSize_; maximumPatients = maximumPatients_; maximumStorageMode = maximumStorageMode_; @@ -595,7 +657,7 @@ unsigned int maximumPatients; { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumStorageSize = maximumStorageSize_; maximumPatients = maximumPatients_; }
--- a/OrthancServer/Sources/ServerIndex.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/ServerIndex.h Fri Jun 27 15:00:33 2025 +0200 @@ -40,7 +40,7 @@ class UnstableResourcePayload; bool done_; - boost::mutex monitoringMutex_; + boost::recursive_mutex monitoringMutex_; boost::thread flushThread_; boost::thread unstableResourcesMonitorThread_; @@ -61,6 +61,10 @@ int64_t id, const std::string& publicId); + void LogStableChange(ResourceType type, + int64_t id, + const std::string& publicId); + public: ServerIndex(ServerContext& context, IDatabaseWrapper& database, @@ -101,5 +105,9 @@ bool IsUnstableResource(ResourceType type, int64_t id); + + bool SetStableStatus(bool& statusHasChanged, + const std::string& resourceId, + bool setNewStatusToStable); }; }
--- a/OrthancServer/Sources/ServerToolbox.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -96,7 +96,7 @@ void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, ResourceType level) { // WARNING: The database should be locked with a transaction!
--- a/OrthancServer/Sources/ServerToolbox.h Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/ServerToolbox.h Fri Jun 27 15:00:33 2025 +0200 @@ -32,7 +32,7 @@ namespace Orthanc { class ServerContext; - class IStorageArea; + class IPluginStorageArea; namespace ServerToolbox { @@ -42,7 +42,7 @@ ResourceType type); void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, ResourceType level); void LoadIdentifiers(const DicomTag*& tags,
--- a/OrthancServer/Sources/main.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/Sources/main.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -30,6 +30,7 @@ #include "../../OrthancFramework/Sources/DicomNetworking/DicomServer.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h" #include "../../OrthancFramework/Sources/HttpServer/HttpServer.h" #include "../../OrthancFramework/Sources/Logging.h" @@ -1426,7 +1427,7 @@ static void UpgradeDatabase(IDatabaseWrapper& database, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { // Upgrade the schema of the database, if needed unsigned int currentVersion = database.GetDatabaseVersion(); @@ -1529,7 +1530,7 @@ static bool ConfigureServerContext(IDatabaseWrapper& database, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, OrthancPlugins *plugins, bool loadJobsFromDatabase) { @@ -1667,7 +1668,7 @@ static bool ConfigureDatabase(IDatabaseWrapper& database, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, OrthancPlugins *plugins, bool upgradeDatabase, bool loadJobsFromDatabase) @@ -1746,7 +1747,7 @@ bool loadJobsFromDatabase) { std::unique_ptr<IDatabaseWrapper> databasePtr; - std::unique_ptr<IStorageArea> storage; + std::unique_ptr<IPluginStorageArea> storage; #if ORTHANC_ENABLE_PLUGINS == 1 std::string databaseServerIdentifier; @@ -1997,7 +1998,7 @@ { SQLiteDatabaseWrapper inMemoryDatabase; inMemoryDatabase.Open(); - MemoryStorageArea inMemoryStorage; + PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */); OrthancRestApi restApi(context, false /* no Orthanc Explorer */); restApi.GenerateOpenApiDocumentation(openapi); @@ -2048,7 +2049,7 @@ { SQLiteDatabaseWrapper inMemoryDatabase; inMemoryDatabase.Open(); - MemoryStorageArea inMemoryStorage; + PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */); OrthancRestApi restApi(context, false /* no Orthanc Explorer */); restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html");
--- a/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -26,9 +26,8 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" -#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/Logging.h" -#include "../../OrthancFramework/Sources/SerializationToolbox.h" #include "../Sources/Database/SQLiteDatabaseWrapper.h" #include "../Sources/ServerContext.h" @@ -39,7 +38,7 @@ { const std::string path = "UnitTestsStorage"; - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false, 1);
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -27,6 +27,7 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/Images/Image.h" #include "../../OrthancFramework/Sources/Logging.h" @@ -196,6 +197,74 @@ transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */); } }; + + class DummyTransactionContextFactory : public StatelessDatabaseOperations::ITransactionContextFactory + { + public: + virtual StatelessDatabaseOperations::ITransactionContext* Create() + { + class DummyTransactionContext : public StatelessDatabaseOperations::ITransactionContext + { + public: + virtual void SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalResourceDeleted(ResourceType type, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void Commit() ORTHANC_OVERRIDE + { + } + + virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE + { + return 0; + } + + virtual bool IsUnstableResource(Orthanc::ResourceType type, + int64_t id) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */, + ResourceType& remainingLevel /* out */) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void MarkAsUnstable(Orthanc::ResourceType type, + int64_t id, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalAttachmentsAdded(uint64_t compressedSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + }; + + return new DummyTransactionContext; + } + }; } @@ -295,12 +364,18 @@ transaction_->GetAllMetadata(md, a[4]); ASSERT_EQ(0u, md.size()); - transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", - CompressionType_ZlibWithSize, 21, "compressedMD5"), 42); - transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"), 43); - transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"), 44); + FileInfo attachment1("my json file", FileContentType_DicomAsJson, 42, "md5", + CompressionType_ZlibWithSize, 21, "compressedMD5"); + attachment1.SetCustomData("hello"); + transaction_->AddAttachment(a[4], attachment1, 42); + + FileInfo attachment2("my dicom file", FileContentType_Dicom, 43, "md5_2"); + transaction_->AddAttachment(a[4], attachment2, 43); + + FileInfo attachment3("world", FileContentType_Dicom, 44, "md5_3"); + attachment3.SetCustomData("world"); + transaction_->AddAttachment(a[6], attachment3, 44); - // TODO - REVISIONS - "42" is revision number, that is not currently stored (*) transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42); transaction_->GetAllMetadata(md, a[4]); @@ -326,8 +401,8 @@ ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]); - ASSERT_EQ(21u + 42u + 44u, transaction_->GetTotalCompressedSize()); - ASSERT_EQ(42u + 42u + 44u, transaction_->GetTotalUncompressedSize()); + ASSERT_EQ(21u + 43u + 44u, transaction_->GetTotalCompressedSize()); + ASSERT_EQ(42u + 43u + 44u, transaction_->GetTotalUncompressedSize()); transaction_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName"); @@ -339,17 +414,17 @@ int64_t revision; ASSERT_TRUE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_RemoteAet)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(42, revision); ASSERT_FALSE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_Instance_IndexInSeries)); - ASSERT_EQ(0, revision); + ASSERT_EQ(42, revision); ASSERT_EQ("PINNACLE", s); std::string u; ASSERT_TRUE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_RemoteAet)); - ASSERT_EQ(0, revision); + ASSERT_EQ(42, revision); ASSERT_EQ("PINNACLE", u); ASSERT_FALSE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_Instance_IndexInSeries)); - ASSERT_EQ(0, revision); + ASSERT_EQ(42, revision); ASSERT_TRUE(transaction_->LookupGlobalProperty(s, GlobalProperty_FlushSleep, true)); ASSERT_FALSE(transaction_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42), true)); @@ -357,22 +432,34 @@ FileInfo att; ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(42, revision); ASSERT_EQ("my json file", att.GetUuid()); ASSERT_EQ(21u, att.GetCompressedSize()); ASSERT_EQ("md5", att.GetUncompressedMD5()); ASSERT_EQ("compressedMD5", att.GetCompressedMD5()); ASSERT_EQ(42u, att.GetUncompressedSize()); ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType()); + ASSERT_EQ("hello", att.GetCustomData()); + + ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_Dicom)); + ASSERT_EQ(43, revision); + ASSERT_EQ("my dicom file", att.GetUuid()); + ASSERT_EQ(43u, att.GetCompressedSize()); + ASSERT_EQ("md5_2", att.GetUncompressedMD5()); + ASSERT_EQ("md5_2", att.GetCompressedMD5()); + ASSERT_EQ(43u, att.GetUncompressedSize()); + ASSERT_EQ(CompressionType_None, att.GetCompressionType()); + ASSERT_TRUE(att.GetCustomData().empty()); ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(44, revision); ASSERT_EQ("world", att.GetUuid()); ASSERT_EQ(44u, att.GetCompressedSize()); - ASSERT_EQ("md5", att.GetUncompressedMD5()); - ASSERT_EQ("md5", att.GetCompressedMD5()); + ASSERT_EQ("md5_3", att.GetUncompressedMD5()); + ASSERT_EQ("md5_3", att.GetCompressedMD5()); ASSERT_EQ(44u, att.GetUncompressedSize()); ASSERT_EQ(CompressionType_None, att.GetCompressionType()); + ASSERT_EQ("world", att.GetCustomData()); ASSERT_EQ(0u, listener_->deletedFiles_.size()); ASSERT_EQ(0u, listener_->deletedResources_.size()); @@ -402,7 +489,7 @@ CheckTableRecordCount(0, "Resources"); CheckTableRecordCount(0, "AttachedFiles"); - CheckTableRecordCount(3, "GlobalProperties"); + CheckTableRecordCount(4, "GlobalProperties"); std::string tmp; ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true)); @@ -618,7 +705,7 @@ const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); - FilesystemStorage storage(path); + PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -700,7 +787,7 @@ const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); - FilesystemStorage storage(path); + PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -817,7 +904,7 @@ { bool overwrite = (i == 0); - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -982,7 +1069,7 @@ { const bool compression = (i == 0); - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -1058,3 +1145,215 @@ ASSERT_FALSE(ServerToolbox::IsValidLabel("&")); ASSERT_FALSE(ServerToolbox::IsValidLabel(".")); } + + +TEST(SQLiteDatabaseWrapper, KeyValueStores) +{ + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + + { + StatelessDatabaseOperations op(db, false); + op.SetTransactionContextFactory(new DummyTransactionContextFactory); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.StoreKeyValue("test", "hello", "world"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.StoreKeyValue("test", "hello2", "world2"); + op.StoreKeyValue("test", "hello3", "world3"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello2", it.GetKey()); + ASSERT_EQ("world2", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello3", it.GetKey()); + ASSERT_EQ("world3", it.GetValue()); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.DeleteKeyValue("test", "hello2"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello3", it.GetKey()); + ASSERT_EQ("world3", it.GetValue()); + ASSERT_FALSE(it.Next()); + } + + std::string s; + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("world", s); + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello3")); ASSERT_EQ("world3", s); + ASSERT_FALSE(op.GetKeyValue(s, "test", "hello2")); + + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("world", s); + op.StoreKeyValue("test", "hello", "overwritten"); + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("overwritten", s); + + op.DeleteKeyValue("test", "nope"); + + op.DeleteKeyValue("test", "hello"); + op.DeleteKeyValue("test", "hello3"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_FALSE(it.Next()); + } + + { + std::string blob; + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); + op.StoreKeyValue("test", "blob", blob); // Storing binary values + } + + ASSERT_TRUE(op.GetKeyValue(s, "test", "blob")); + ASSERT_EQ(4u, s.size()); + ASSERT_EQ(0, static_cast<uint8_t>(s[0])); + ASSERT_EQ(1, static_cast<uint8_t>(s[1])); + ASSERT_EQ(0, static_cast<uint8_t>(s[2])); + ASSERT_EQ(2, static_cast<uint8_t>(s[3])); + op.DeleteKeyValue("test", "blob"); + ASSERT_FALSE(op.GetKeyValue(s, "test", "blob")); + } + + db.Close(); +} + + +TEST(SQLiteDatabaseWrapper, Queues) +{ + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + + { + StatelessDatabaseOperations op(db, false); + op.SetTransactionContextFactory(new DummyTransactionContextFactory); + + ASSERT_EQ(0u, op.GetQueueSize("test")); + op.EnqueueValue("test", "hello"); + ASSERT_EQ(1u, op.GetQueueSize("test")); + op.EnqueueValue("test", "world"); + ASSERT_EQ(2u, op.GetQueueSize("test")); + + std::string s; + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back)); ASSERT_EQ("world", s); + ASSERT_EQ(1u, op.GetQueueSize("test")); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back)); ASSERT_EQ("hello", s); + ASSERT_EQ(0u, op.GetQueueSize("test")); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Back)); + + op.EnqueueValue("test", "hello"); + op.EnqueueValue("test", "world"); + ASSERT_EQ(2u, op.GetQueueSize("test")); + + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); ASSERT_EQ("hello", s); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); ASSERT_EQ("world", s); + ASSERT_EQ(0u, op.GetQueueSize("test")); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front)); + + { + std::string blob; + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); + op.EnqueueValue("test", blob); // Storing binary values + } + + ASSERT_EQ(1u, op.GetQueueSize("test")); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); + ASSERT_EQ(0u, op.GetQueueSize("test")); + ASSERT_EQ(4u, s.size()); + ASSERT_EQ(0, static_cast<uint8_t>(s[0])); + ASSERT_EQ(1, static_cast<uint8_t>(s[1])); + ASSERT_EQ(0, static_cast<uint8_t>(s[2])); + ASSERT_EQ(2, static_cast<uint8_t>(s[3])); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front)); + } + + db.Close(); +} + + +TEST_F(DatabaseWrapperTest, BinaryCustomData) +{ + int64_t patient = transaction_->CreateResource("Patient", ResourceType_Patient); + + { + FileInfo info("hello", FileContentType_Dicom, 10, "md5"); + + { + std::string blob; + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); + info.SetCustomData(blob); + } + + transaction_->AddAttachment(patient, info, 43); + } + + { + FileInfo info; + int64_t revision; + ASSERT_TRUE(transaction_->LookupAttachment(info, revision, patient, FileContentType_Dicom)); + ASSERT_EQ(43u, revision); + ASSERT_EQ("hello", info.GetUuid()); + ASSERT_EQ(CompressionType_None, info.GetCompressionType()); + ASSERT_EQ(10u, info.GetCompressedSize()); + ASSERT_EQ("md5", info.GetCompressedMD5()); + ASSERT_EQ(4u, info.GetCustomData().size()); + ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[0])); + ASSERT_EQ(1, static_cast<uint8_t>(info.GetCustomData()[1])); + ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[2])); + ASSERT_EQ(2, static_cast<uint8_t>(info.GetCustomData()[3])); + } + + transaction_->DeleteResource(patient); +} \ No newline at end of file
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Fri Jun 27 14:59:41 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Fri Jun 27 15:00:33 2025 +0200 @@ -26,6 +26,7 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/SerializationToolbox.h" @@ -528,12 +529,13 @@ class OrthancJobsSerialization : public testing::Test { private: - MemoryStorageArea storage_; - SQLiteDatabaseWrapper db_; // The SQLite DB is in memory - std::unique_ptr<ServerContext> context_; + PluginStorageAreaAdapter storage_; + SQLiteDatabaseWrapper db_; // The SQLite DB is in memory + std::unique_ptr<ServerContext> context_; public: - OrthancJobsSerialization() + OrthancJobsSerialization() : + storage_(new MemoryStorageArea) { db_.Open(); context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */));
--- a/TODO Fri Jun 27 14:59:41 2025 +0200 +++ b/TODO Fri Jun 27 15:00:33 2025 +0200 @@ -1,11 +1,3 @@ -current work on C-Get SCU: -- for the negotiation, limit SOPClassUID to the ones listed in a C-Find response or to a list provided in the Rest API ? -- SetupPresentationContexts -- handle progress -- handle cancellation when the job is cancelled ? - - - ======================= === Orthanc Roadmap === =======================