# HG changeset patch # User Sebastien Jodogne <s.jodogne@gmail.com> # Date 1742301560 -3600 # Node ID e83414b2b98dc1384cdc64a5d566de414fcbbddb # Parent e282d55e043c244911da6db8de4e56ae3aedc378# Parent d508d2348753def146f06ac54d635017d1a16410 integration mainline->attach-custom-data diff -r d508d2348753 -r e83414b2b98d .hgignore --- a/.hgignore Tue Mar 18 13:37:18 2025 +0100 +++ b/.hgignore Tue Mar 18 13:39:20 2025 +0100 @@ -15,3 +15,4 @@ .project Resources/Testing/Issue32/Java/bin Resources/Testing/Issue32/Java/target +build/ diff -r d508d2348753 -r e83414b2b98d NEWS --- a/NEWS Tue Mar 18 13:37:18 2025 +0100 +++ b/NEWS Tue Mar 18 13:39:20 2025 +0100 @@ -1,6 +1,15 @@ Pending changes in the mainline =============================== +General +------- + +* SQLite default DB engine now supports metadata and attachment revisions +* Upgraded the DB to allow plugins to store customData for each attachment. +* New sample Advanced Storage plugin that allows: + - using multiple disk for image storage + - use more human friendly storage structure (experimental feature) + REST API -------- @@ -12,6 +21,11 @@ accepts a "lossy-quality" url argument or a "LossyQuality" field to define the compression quality factor. If not specified, the "DicomLossyTranscodingQuality" configuration is taken into account. +Plugins +------- + +* New database plugin SDK (vX) to handle customData for attachments. +* New storage plugin SDK (v3) to handle customData for attachments, Maintenance ----------- diff -r d508d2348753 -r e83414b2b98d OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue Mar 18 13:39:20 2025 +0100 @@ -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 "27") +set(ORTHANC_API_VERSION "28") ##################################################################### diff -r d508d2348753 -r e83414b2b98d OrthancFramework/Sources/FileStorage/FileInfo.cpp --- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -42,7 +42,8 @@ FileInfo::FileInfo(const std::string& uuid, FileContentType contentType, uint64_t size, - const std::string& md5) : + const std::string& md5, + const std::string& customData) : valid_(true), uuid_(uuid), contentType_(contentType), @@ -50,7 +51,8 @@ uncompressedMD5_(md5), compressionType_(CompressionType_None), compressedSize_(size), - compressedMD5_(md5) + compressedMD5_(md5), + customData_(customData) { } @@ -61,7 +63,8 @@ const std::string& uncompressedMD5, CompressionType compressionType, uint64_t compressedSize, - const std::string& compressedMD5) : + const std::string& compressedMD5, + const std::string& customData) : valid_(true), uuid_(uuid), contentType_(contentType), @@ -69,7 +72,8 @@ uncompressedMD5_(uncompressedMD5), compressionType_(compressionType), compressedSize_(compressedSize), - compressedMD5_(compressedMD5) + compressedMD5_(compressedMD5), + customData_(customData) { } @@ -169,4 +173,16 @@ throw OrthancException(ErrorCode_BadSequenceOfCalls); } } + + const std::string& FileInfo::GetCustomData() const + { + if (valid_) + { + return customData_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } } diff -r d508d2348753 -r e83414b2b98d OrthancFramework/Sources/FileStorage/FileInfo.h --- a/OrthancFramework/Sources/FileStorage/FileInfo.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.h Tue Mar 18 13:39:20 2025 +0100 @@ -42,6 +42,7 @@ CompressionType compressionType_; uint64_t compressedSize_; std::string compressedMD5_; + std::string customData_; public: FileInfo(); @@ -52,7 +53,8 @@ FileInfo(const std::string& uuid, FileContentType contentType, uint64_t size, - const std::string& md5); + const std::string& md5, + const std::string& customData); /** * Constructor for a compressed attachment. @@ -63,7 +65,8 @@ const std::string& uncompressedMD5, CompressionType compressionType, uint64_t compressedSize, - const std::string& compressedMD5); + const std::string& compressedMD5, + const std::string& customData); bool IsValid() const; @@ -80,5 +83,7 @@ const std::string& GetCompressedMD5() const; const std::string& GetUncompressedMD5() const; + + const std::string& GetCustomData() const; }; } diff -r d508d2348753 -r e83414b2b98d OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp diff -r d508d2348753 -r e83414b2b98d OrthancFramework/Sources/FileStorage/FilesystemStorage.h --- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Tue Mar 18 13:39:20 2025 +0100 @@ -43,7 +43,7 @@ namespace Orthanc { - class ORTHANC_PUBLIC FilesystemStorage : public IStorageArea + class ORTHANC_PUBLIC FilesystemStorage : public ICoreStorageArea { // TODO REMOVE THIS friend class FilesystemHttpSender; diff -r d508d2348753 -r e83414b2b98d OrthancFramework/Sources/FileStorage/IStorageArea.h --- a/OrthancFramework/Sources/FileStorage/IStorageArea.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h Tue Mar 18 13:39:20 2025 +0100 @@ -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: @@ -40,6 +43,70 @@ { } + 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* ReadWhole(const std::string& uuid, + FileContentType type, + const std::string& customData) = 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 HasReadRange() const = 0; + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) = 0; + }; + + // storage area without customData (customData are used only in plugins) + class ICoreStorageArea : public IStorageArea + { + public: + 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 + { + customData.clear(); + Create(uuid, content, size, type); + } + + virtual IMemoryBuffer* ReadWhole(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + return Read(uuid, type); + } + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) ORTHANC_OVERRIDE + { + return ReadRange(uuid, type, start, end); + } + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + Remove(uuid, type); + } + virtual void Create(const std::string& uuid, const void* content, size_t size, @@ -53,8 +120,6 @@ uint64_t start /* inclusive */, uint64_t end /* exclusive */) = 0; - virtual bool HasReadRange() const = 0; - virtual void Remove(const std::string& uuid, FileContentType type) = 0; }; diff -r d508d2348753 -r e83414b2b98d OrthancFramework/Sources/FileStorage/MemoryStorageArea.h --- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Tue Mar 18 13:39:20 2025 +0100 @@ -33,7 +33,7 @@ namespace Orthanc { - class MemoryStorageArea : public IStorageArea + class MemoryStorageArea : public ICoreStorageArea { private: typedef std::map<std::string, std::string*> Content; diff -r d508d2348753 -r e83414b2b98d OrthancFramework/Sources/FileStorage/StorageAccessor.cpp --- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -314,9 +314,10 @@ size_t size, FileContentType type, CompressionType compression, - bool storeMd5) + bool storeMd5, + const DicomInstanceToStore* instance) { - std::string uuid = Toolbox::GenerateUuid(); + const std::string uuid = Toolbox::GenerateUuid(); std::string md5; @@ -325,13 +326,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 +348,7 @@ cacheAccessor.Add(uuid, type, data, size); } - return FileInfo(uuid, type, size, md5); + return FileInfo(uuid, type, size, md5, customData); } case CompressionType_ZlibWithSize: @@ -367,11 +370,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); } } @@ -387,7 +390,7 @@ } return FileInfo(uuid, type, size, md5, - CompressionType_ZlibWithSize, compressed.size(), compressedMD5); + CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData); } default: @@ -395,16 +398,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 +439,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadWhole(info.GetUuid(), info.GetContentType(), info.GetCustomData())); } if (metrics_ != NULL) @@ -467,7 +460,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - compressed.reset(area_.Read(info.GetUuid(), info.GetContentType())); + compressed.reset(area_.ReadWhole(info.GetUuid(), info.GetContentType(), info.GetCustomData())); } if (metrics_ != NULL) @@ -526,7 +519,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadWhole(info.GetUuid(), info.GetContentType(), info.GetCustomData())); } if (metrics_ != NULL) @@ -539,7 +532,8 @@ void StorageAccessor::Remove(const std::string& fileUuid, - FileContentType type) + FileContentType type, + const std::string& customData) { if (cache_ != NULL) { @@ -548,14 +542,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 +610,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 +676,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_.ReadWhole(info.GetUuid(), info.GetContentType(), info.GetCustomData())); } buffer->MoveToString(target); @@ -785,4 +779,5 @@ output.AnswerStream(transcoder); } #endif + } diff -r d508d2348753 -r e83414b2b98d OrthancFramework/Sources/FileStorage/StorageAccessor.h --- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Tue Mar 18 13:39:20 2025 +0100 @@ -137,12 +137,8 @@ size_t size, FileContentType type, CompressionType compression, - bool storeMd5); - - FileInfo Write(const std::string& data, - FileContentType type, - CompressionType compression, - bool storeMd5); + bool storeMd5, + const DicomInstanceToStore* instance); void Read(std::string& content, const FileInfo& info); @@ -155,7 +151,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 +182,7 @@ const std::string& mime, const std::string& contentFilename); #endif + private: void ReadStartRangeInternal(std::string& target, const FileInfo& info, diff -r d508d2348753 -r e83414b2b98d OrthancFramework/UnitTestsSources/FileStorageTests.cpp --- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -173,9 +173,9 @@ 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(data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, NULL); + std::string r; accessor.Read(r, info); @@ -195,9 +195,9 @@ 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(data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, NULL); + std::string r; accessor.Read(r, info); @@ -216,13 +216,13 @@ 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(compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, NULL); + FileInfo uncompressedInfo = accessor.Write(uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, NULL); + + std::string r; accessor.Read(r, compressedInfo); ASSERT_EQ(compressedData, r); diff -r d508d2348753 -r e83414b2b98d OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/CMakeLists.txt Tue Mar 18 13:39:20 2025 +0100 @@ -243,15 +243,16 @@ ##################################################################### 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 ) if (STANDALONE_BUILD) diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -83,13 +83,15 @@ static FileInfo Convert(const OrthancPluginAttachment& attachment) { + std::string customData; return FileInfo(attachment.uuid, static_cast<FileContentType>(attachment.contentType), attachment.uncompressedSize, attachment.uncompressedHash, static_cast<CompressionType>(attachment.compressionType), attachment.compressedSize, - attachment.compressedHash); + attachment.compressedHash, + customData); } diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/OrthancPluginDatabase.h diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -62,13 +62,15 @@ static FileInfo Convert(const OrthancPluginAttachment& attachment) { + std::string customData; return FileInfo(attachment.uuid, static_cast<FileContentType>(attachment.contentType), attachment.uncompressedSize, attachment.uncompressedHash, static_cast<CompressionType>(attachment.compressionType), attachment.compressedSize, - attachment.compressedHash); + attachment.compressedHash, + customData); } diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -108,7 +108,8 @@ source.uncompressed_hash(), static_cast<CompressionType>(source.compression_type()), source.compressed_size(), - source.compressed_hash()); + source.compressed_hash(), + source.custom_data()); } @@ -576,6 +577,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.7 request.mutable_add_attachment()->set_revision(revision); ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request); diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/OrthancPlugins.cpp --- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -59,6 +59,7 @@ #include "../../Sources/Database/VoidDatabaseListener.h" #include "../../Sources/OrthancConfiguration.h" #include "../../Sources/OrthancFindRequestHandler.h" +#include "../../Sources/Search/IDatabaseConstraint.h" #include "../../Sources/Search/HierarchicalMatcher.h" #include "../../Sources/ServerContext.h" #include "../../Sources/ServerToolbox.h" @@ -79,6 +80,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: @@ -550,8 +670,8 @@ } }; - - class StorageAreaBase : public IStorageArea + // "legacy" storage plugins don't store customData -> derive from ICoreStorageArea + class PluginStorageAreaBase : public ICoreStorageArea { private: OrthancPluginStorageCreate create_; @@ -605,9 +725,9 @@ } public: - StorageAreaBase(OrthancPluginStorageCreate create, - OrthancPluginStorageRemove remove, - PluginsErrorDictionary& errorDictionary) : + PluginStorageAreaBase(OrthancPluginStorageCreate create, + OrthancPluginStorageRemove remove, + PluginsErrorDictionary& errorDictionary) : create_(create), remove_(remove), errorDictionary_(errorDictionary) @@ -649,7 +769,7 @@ }; - class PluginStorageArea : public StorageAreaBase + class PluginStorageArea : public PluginStorageAreaBase { private: OrthancPluginStorageRead read_; @@ -666,7 +786,7 @@ public: PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks, PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), read_(callbacks.read), free_(callbacks.free) { @@ -715,7 +835,7 @@ // New in Orthanc 1.9.0 - class PluginStorageArea2 : public StorageAreaBase + class PluginStorageArea2 : public PluginStorageAreaBase { private: OrthancPluginStorageReadWhole readWhole_; @@ -724,7 +844,7 @@ public: PluginStorageArea2(const _OrthancPluginRegisterStorageArea2& callbacks, PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), readWhole_(callbacks.readWhole), readRange_(callbacks.readRange) { @@ -809,19 +929,222 @@ }; + // New in Orthanc 1.12.7 + class PluginStorageArea3 : public IStorageArea + { + private: + OrthancPluginStorageCreate2 create_; + OrthancPluginStorageReadWhole2 readWhole2_; + OrthancPluginStorageReadRange2 readRange2_; + OrthancPluginStorageRemove2 remove2_; + + PluginsErrorDictionary& errorDictionary_; + + protected: + PluginsErrorDictionary& GetErrorDictionary() const + { + return errorDictionary_; + } + + IMemoryBuffer* RangeFromWhole(const std::string& uuid, + const std::string& customData, + 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(ReadWhole(uuid, type, customData)); + + 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: + PluginStorageArea3(const _OrthancPluginRegisterStorageArea3& callbacks, + PluginsErrorDictionary& errorDictionary) : + create_(callbacks.create), + readWhole2_(callbacks.readWhole), + readRange2_(callbacks.readRange), + remove2_(callbacks.remove), + errorDictionary_(errorDictionary) + { + if (create_ == NULL || + readWhole2_ == NULL || + remove2_ == NULL) + { + throw OrthancException(ErrorCode_Plugin, "Storage area plugin doesn't implement all the required primitives (createInstance, remove, readWhole"); + } + } + + 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 + { + MemoryBufferRaii 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.ToString(customData); + } + } + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + OrthancPluginErrorCode error = remove2_(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* ReadWhole(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer); + + OrthancPluginMemoryBuffer64 buffer; + buffer.size = 0; + buffer.data = NULL; + + OrthancPluginErrorCode error = readWhole2_(&buffer, uuid.c_str(), Plugins::Convert(type), + customData.empty() ? NULL : customData.c_str(), customData.size()); + + 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 */, + uint64_t end /* exclusive */, + const std::string& customData) ORTHANC_OVERRIDE + { + if (readRange2_ == NULL) + { + return RangeFromWhole(uuid, customData, type, start, end); + } + else + { + if (start > end) + { + throw OrthancException(ErrorCode_BadRange); + } + else if (start == end) + { + return new StringMemoryBuffer; + } + 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()); + + OrthancPluginErrorCode error = + readRange2_(&buffer, uuid.c_str(), Plugins::Convert(type), start, customData.empty() ? NULL : customData.c_str(), customData.size()); + + if (error == OrthancPluginErrorCode_Success) + { + return StringMemoryBuffer::CreateFromSwap(range); + } + else + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + } + } + + virtual bool HasReadRange() const ORTHANC_OVERRIDE + { + return (readRange2_ != NULL); + } + + }; + + class StorageAreaFactory : public boost::noncopyable { private: enum Version { Version1, - Version2 + Version2, + Version3 }; SharedLibrary& sharedLibrary_; Version version_; _OrthancPluginRegisterStorageArea callbacks_; _OrthancPluginRegisterStorageArea2 callbacks2_; + _OrthancPluginRegisterStorageArea3 callbacks3_; PluginsErrorDictionary& errorDictionary_; static void WarnNoReadRange() @@ -855,6 +1178,20 @@ } } + 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_; @@ -870,6 +1207,9 @@ case Version2: return new PluginStorageArea2(callbacks2_, errorDictionary_); + case Version3: + return new PluginStorageArea3(callbacks3_, errorDictionary_); + default: throw OrthancException(ErrorCode_InternalError); } @@ -2527,125 +2867,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) @@ -3611,6 +3832,12 @@ break; } + case OrthancPluginCompressionType_None: + { + CopyToMemoryBuffer(*p.target, p.source, p.size); + return; + } + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -5069,7 +5296,7 @@ { const _OrthancPluginStorageAreaCreate& p = *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); + PluginStorageAreaBase& storage = *reinterpret_cast<PluginStorageAreaBase*>(p.storageArea); storage.Create(p.uuid, p.content, static_cast<size_t>(p.size), Plugins::Convert(p.type)); return true; } @@ -5079,7 +5306,8 @@ 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))); + std::string customDataNotUsed; + std::unique_ptr<IMemoryBuffer> content(storage.ReadWhole(p.uuid, Plugins::Convert(p.type), customDataNotUsed)); CopyToMemoryBuffer(*p.target, content->GetData(), content->GetSize()); return true; } @@ -5089,7 +5317,8 @@ const _OrthancPluginStorageAreaRemove& p = *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters); IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - storage.Remove(p.uuid, Plugins::Convert(p.type)); + std::string customDataNotUsed; + storage.Remove(p.uuid, Plugins::Convert(p.type), customDataNotUsed); return true; } @@ -5670,23 +5899,34 @@ 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())); + } else { throw OrthancException(ErrorCode_InternalError); @@ -5809,7 +6049,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); diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/OrthancPlugins.h --- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Tue Mar 18 13:39:20 2025 +0100 @@ -88,11 +88,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); diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/PluginsEnumerations.cpp --- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -664,5 +664,21 @@ 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); + } + } } } diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Engine/PluginsEnumerations.h --- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h Tue Mar 18 13:39:20 2025 +0100 @@ -73,6 +73,8 @@ ResourceType Convert(OrthancPluginResourceType type); OrthancPluginConstraintType Convert(ConstraintType constraint); + + OrthancPluginCompressionType Convert(CompressionType type); } } diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Tue Mar 18 13:39:20 2025 +0100 @@ -1361,7 +1361,7 @@ return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, ¶ms); } - + #ifdef __cplusplus } #endif diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue Mar 18 13:39:20 2025 +0100 @@ -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 7 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -492,6 +492,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.7 */ /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -562,7 +563,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, @@ -791,6 +792,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.7) */ _OrthancPluginCompressionType_INTERNAL = 0x7fffffff } OrthancPluginCompressionType; @@ -1367,8 +1369,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 +1444,101 @@ /** + * @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 OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it. + * @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 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 whole file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * reads a whole file from the storage area. + * + * @param target Memory buffer where to store the content of the file. It must be allocated by the + * plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it. + * @param uuid The UUID of the file of interest. + * @param customData The custom data of the file to be removed. + * @param type The content type corresponding to this file. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadWhole2) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint64_t customDataSize); + + + + /** + * @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 customData The custom data of the file to be removed. + * @param type The content type corresponding to this file. + * @param rangeStart Start position of the requested range in the file. + * @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, + uint64_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 customData The custom data of the file to be removed. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) ( + const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint64_t customDataSize); + + + /** * @brief Callback to handle the C-Find SCP requests for worklists. * * Signature of a callback function that is triggered when Orthanc @@ -3323,7 +3419,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, @@ -8913,6 +9009,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, @@ -9364,6 +9461,45 @@ } + typedef struct + { + OrthancPluginStorageCreate2 create; + OrthancPluginStorageReadWhole2 readWhole; + 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 readWhole The callback function to read a whole file from the custom storage area. + * @param readRange The callback function to read some range of a file from the custom storage area. + * 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 + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3( + OrthancPluginContext* context, + OrthancPluginStorageCreate2 create, + OrthancPluginStorageReadWhole2 readWhole, + OrthancPluginStorageReadRange2 readRange, + OrthancPluginStorageRemove2 remove) + { + _OrthancPluginRegisterStorageArea3 params; + params.create = create; + params.readWhole = readWhole; + 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. diff -r d508d2348753 -r e83414b2b98d OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto --- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Tue Mar 18 13:39:20 2025 +0100 @@ -55,6 +55,7 @@ int32 compression_type = 5; // opaque "CompressionType" in Orthanc uint64 compressed_size = 6; string compressed_hash = 7; + string custom_data = 8; // added in v 1.12.7 } enum ResourceType { diff -r d508d2348753 -r e83414b2b98d OrthancServer/Resources/Configuration.json diff -r d508d2348753 -r e83414b2b98d OrthancServer/Resources/RunCppCheck.sh --- a/OrthancServer/Resources/RunCppCheck.sh Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Resources/RunCppCheck.sh Tue Mar 18 13:39:20 2025 +0100 @@ -32,8 +32,8 @@ useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:91 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99 useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275 -assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:277 -assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026 +assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:279 +assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1028 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391 assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3058 diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/Database/IDatabaseWrapper.h --- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Mar 18 13:39:20 2025 +0100 @@ -56,6 +56,7 @@ bool hasMeasureLatency_; bool hasFindSupport_; bool hasExtendedChanges_; + bool hasAttachmentCustomDataSupport_; public: Capabilities() : @@ -66,7 +67,8 @@ hasUpdateAndGetStatistics_(false), hasMeasureLatency_(false), hasFindSupport_(false), - hasExtendedChanges_(false) + hasExtendedChanges_(false), + hasAttachmentCustomDataSupport_(false) { } @@ -100,6 +102,16 @@ return hasLabelsSupport_; } + void SetAttachmentCustomDataSupport(bool value) + { + hasAttachmentCustomDataSupport_ = value; + } + + bool HasAttachmentCustomDataSupport() const + { + return hasAttachmentCustomDataSupport_; + } + void SetHasExtendedChanges(bool value) { hasExtendedChanges_ = value; diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql Tue Mar 18 13:39:20 2025 +0100 @@ -0,0 +1,66 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2022 Osimis S.A., Belgium +-- Copyright (C) 2021-2022 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 TEXT; + + +-- add another AttachedFileDeleted trigger +-- 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 keep 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. + +CREATE TABLE DeletedFiles( + uuid TEXT NOT NULL, -- 0 + customData TEXT -- 1 +); + +DROP TRIGGER 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; + +-- Record that this upgrade has been performed + +INSERT INTO GlobalProperties VALUES (7, 1); -- GlobalProperty_SQLiteHasCustomDataAndRevision diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/Database/PrepareDatabase.sql --- a/OrthancServer/Sources/Database/PrepareDatabase.sql Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Tue Mar 18 13:39:20 2025 +0100 @@ -55,6 +55,7 @@ id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, type INTEGER, value TEXT, + -- revision INTEGER, -- New in Orthanc 1.12.7 (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.7 (added in InstallRevisionAndCustomData.sql) + -- customData TEXT, -- New in Orthanc 1.12.7 (added in InstallRevisionAndCustomData.sql) PRIMARY KEY(id, fileType) ); @@ -129,7 +132,8 @@ 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); + old.uncompressedMD5, old.compressedMD5 + ); END; CREATE TRIGGER ResourceDeleted diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -41,6 +41,8 @@ #include <stdio.h> #include <boost/lexical_cast.hpp> +static std::map<std::string, std::string> filesToDeleteCustomData; + namespace Orthanc { static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec) @@ -410,8 +412,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 +423,11 @@ s.BindInt(5, attachment.GetCompressionType()); s.BindString(6, attachment.GetUncompressedMD5()); s.BindString(7, attachment.GetCompressedMD5()); + s.BindInt(8, revision); + s.BindString(9, attachment.GetCustomData()); s.Run(); } - virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, const DatabaseDicomTagConstraints& lookup, @@ -473,10 +477,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 @@ -588,10 +594,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 +613,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 +628,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 +644,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 +665,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 +685,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 +705,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 +726,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 +748,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 +769,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 +792,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 +814,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 +838,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 +859,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 +881,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 +902,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 +923,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 +947,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 +968,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 +989,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 +1010,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 +1031,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 +1052,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 +1095,19 @@ 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), s.ColumnString(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 +1115,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 +1124,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 +1133,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 +1141,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 +1149,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 +1181,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 +1222,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 +1248,16 @@ 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), s.ColumnString(C6_STRING_4)); res.AddOneInstanceAttachment(file); }; break; @@ -1312,6 +1364,28 @@ } } + 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()) + { + customData = s.ColumnString(0); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } virtual void GetAllMetadata(std::map<MetadataType, std::string>& target, int64_t id) ORTHANC_OVERRIDE @@ -1687,7 +1761,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); @@ -1703,8 +1777,9 @@ s.ColumnString(4), static_cast<CompressionType>(s.ColumnInt(2)), s.ColumnInt64(3), - s.ColumnString(5)); - revision = 0; // TODO - REVISIONS + s.ColumnString(5), + s.ColumnString(7)); + revision = s.ColumnInt(6); return true; } } @@ -1739,7 +1814,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 +1825,7 @@ else { target = s.ColumnString(0); - revision = 0; // TODO - REVISIONS + revision = s.ColumnInt(1); return true; } } @@ -1922,11 +1997,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(); } @@ -2055,6 +2130,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)) @@ -2073,9 +2153,11 @@ uncompressedMD5, static_cast<CompressionType>(context.GetIntValue(3)), static_cast<uint64_t>(context.GetInt64Value(4)), - compressedMD5); + compressedMD5, + customData); sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info); + sqlite_.activeTransaction_->DeleteDeletedFile(id); } } }; @@ -2332,6 +2414,19 @@ } } + // New in Orthanc 1.12.7 + if (version_ >= 6) + { + if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasCustomDataAndRevision, true /* unused in SQLite */) + || tmp != "1") + { + LOG(INFO) << "Upgrading SQLite schema to support revision and customData"; + std::string query; + ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA); + db_.Execute(query); + } + } + transaction->Commit(0); } } @@ -2362,7 +2457,7 @@ { boost::mutex::scoped_lock lock(mutex_); - if (targetVersion != 6) + if (targetVersion != 7) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } @@ -2372,7 +2467,8 @@ if (version_ != 3 && version_ != 4 && version_ != 5 && - version_ != 6) + version_ != 6 && + version_ != 7) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } @@ -2413,6 +2509,7 @@ version_ = 6; } + } diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/OrthancInitialization.cpp --- a/OrthancServer/Sources/OrthancInitialization.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -453,7 +453,7 @@ { // Anonymous namespace to avoid clashes between compilation modules - class FilesystemStorageWithoutDicom : public IStorageArea + class FilesystemStorageWithoutDicom : public ICoreStorageArea { private: FilesystemStorage storage_; diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -2664,7 +2664,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 diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -593,10 +593,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); } @@ -707,8 +708,7 @@ // 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_); + FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom, compression, storeMD5_, &dicom); ServerIndex::Attachments attachments; attachments.push_back(dicomInfo); @@ -718,8 +718,7 @@ (!area_.HasReadRange() || compressionEnabled_)) { - dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, - FileContentType_DicomUntilPixelData, compression, storeMD5_); + dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL); attachments.push_back(dicomUntilPixelData); } @@ -1018,8 +1017,7 @@ 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(content.empty() ? NULL : content.c_str(), content.size(), attachmentType, compression, storeMD5_, NULL); try { @@ -1283,9 +1281,9 @@ 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 */); } } } @@ -1513,6 +1511,7 @@ bool ServerContext::AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -1526,7 +1525,10 @@ 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(data, size, attachmentType, compression, storeMD5_, NULL); try { diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.h Tue Mar 18 13:39:20 2025 +0100 @@ -271,7 +271,8 @@ // 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 @@ -344,6 +345,7 @@ bool AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/ServerEnumerations.h --- a/OrthancServer/Sources/ServerEnumerations.h Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Sources/ServerEnumerations.h Tue Mar 18 13:39:20 2025 +0100 @@ -171,6 +171,7 @@ GlobalProperty_AnonymizationSequence = 3, GlobalProperty_JobsRegistry = 5, GlobalProperty_GetTotalSizeIsFast = 6, // New in Orthanc 1.5.2 + GlobalProperty_SQLiteHasCustomDataAndRevision = 7, // New in Orthanc 1.12.7 GlobalProperty_Modalities = 20, // New in Orthanc 1.5.0 GlobalProperty_Peers = 21, // New in Orthanc 1.5.0 diff -r d508d2348753 -r e83414b2b98d OrthancServer/Sources/ServerIndex.cpp --- a/OrthancServer/Sources/ServerIndex.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/Sources/ServerIndex.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -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) { diff -r d508d2348753 -r e83414b2b98d OrthancServer/UnitTestsSources/ServerIndexTests.cpp --- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Mar 18 13:37:18 2025 +0100 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Mar 18 13:39:20 2025 +0100 @@ -296,11 +296,10 @@ 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); + CompressionType_ZlibWithSize, 21, "compressedMD5", "customData"), 42); + transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5", "customData"), 43); + transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5", "customData"), 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]); @@ -339,17 +338,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,7 +356,7 @@ 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()); @@ -366,7 +365,7 @@ ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType()); 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()); @@ -402,7 +401,7 @@ CheckTableRecordCount(0, "Resources"); CheckTableRecordCount(0, "AttachedFiles"); - CheckTableRecordCount(3, "GlobalProperties"); + CheckTableRecordCount(4, "GlobalProperties"); std::string tmp; ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true)); @@ -478,7 +477,7 @@ std::string p = "Patient " + boost::lexical_cast<std::string>(i); patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast<std::string>(i)), 42); + "md5-" + boost::lexical_cast<std::string>(i), "customData"), 42); ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } @@ -539,7 +538,7 @@ std::string p = "Patient " + boost::lexical_cast<std::string>(i); patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast<std::string>(i)), 42); + "md5-" + boost::lexical_cast<std::string>(i), "customData"), 42); ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } @@ -782,7 +781,7 @@ for (size_t i = 0; i < ids.size(); i++) { - FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5"); + FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5", "customData"); int64_t revision = -1; index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1, ""); ASSERT_EQ(0, revision); diff -r d508d2348753 -r e83414b2b98d TODO --- a/TODO Tue Mar 18 13:37:18 2025 +0100 +++ b/TODO Tue Mar 18 13:39:20 2025 +0100 @@ -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 === =======================