Mercurial > hg > orthanc
changeset 6143:d6c777a2511f attach-custom-data
integration mainline->attach-custom-data
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 30 May 2025 12:29:41 +0200 |
parents | 93d408173903 (diff) 66609557a7ee (current diff) |
children | f9d6955e22dd |
files | OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp |
diffstat | 61 files changed, 4459 insertions(+), 739 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Fri May 30 10:30:04 2025 +0200 +++ b/.hgignore Fri May 30 12:29:41 2025 +0200 @@ -15,3 +15,4 @@ .project Resources/Testing/Issue32/Java/bin Resources/Testing/Issue32/Java/target +build/
--- a/NEWS Fri May 30 10:30:04 2025 +0200 +++ b/NEWS Fri May 30 12:29:41 2025 +0200 @@ -1,6 +1,19 @@ 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. + +Plugins +------- + +* New database plugin SDK (vX) to handle customData for attachments, key-value stores and queues. +* New storage plugin SDK (v3) to handle customData for attachments. +* New functions available to all plugins to store key-values and queues. + Maintenance ----------- @@ -9,6 +22,7 @@ - If "LimitMainDicomTagsReconstructLevel" was set, files were not transcoded if they had to. The "LimitMainDicomTagsReconstructLevel" configuration is now ignored when a full processing is required. +* Fix computation of MD5 hashes for memory buffers whose size is larger than 2^31 bytes. Version 1.12.7 (2025-04-07)
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Fri May 30 12:29:41 2025 +0200 @@ -170,6 +170,7 @@ ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Enumerations.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FileInfo.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/MemoryStorageArea.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/PluginStorageAreaAdapter.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/CStringMatcher.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpContentNegociation.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpToolbox.cpp
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp Fri May 30 12:29:41 2025 +0200 @@ -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); + } + } }
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.h Fri May 30 12:29:41 2025 +0200 @@ -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; }; }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Fri May 30 12:29:41 2025 +0200 @@ -187,8 +187,8 @@ } - IMemoryBuffer* FilesystemStorage::Read(const std::string& uuid, - FileContentType type) + IMemoryBuffer* FilesystemStorage::ReadWhole(const std::string& uuid, + FileContentType type) { Toolbox::ElapsedTimer timer; LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) @@ -221,12 +221,6 @@ } - bool FilesystemStorage::HasReadRange() const - { - return true; - } - - uintmax_t FilesystemStorage::GetSize(const std::string& uuid) const { boost::filesystem::path path = GetPath(uuid);
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Fri May 30 12:29:41 2025 +0200 @@ -80,15 +80,19 @@ size_t size, FileContentType type) ORTHANC_OVERRIDE; - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE; + // This flavor is only used in the "DelayedDeletion" plugin + IMemoryBuffer* ReadWhole(const std::string& uuid, + FileContentType type); virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) ORTHANC_OVERRIDE; - virtual bool HasReadRange() const ORTHANC_OVERRIDE; + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return true; + } virtual void Remove(const std::string& uuid, FileContentType type) ORTHANC_OVERRIDE;
--- a/OrthancFramework/Sources/FileStorage/IStorageArea.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h Fri May 30 12:29:41 2025 +0200 @@ -24,8 +24,9 @@ #pragma once +#include "../Compatibility.h" +#include "../Enumerations.h" #include "../IMemoryBuffer.h" -#include "../Enumerations.h" #include <stdint.h> #include <string> @@ -33,6 +34,8 @@ namespace Orthanc { + class DicomInstanceToStore; + class IStorageArea : public boost::noncopyable { public: @@ -45,17 +48,44 @@ size_t size, FileContentType type) = 0; - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) = 0; - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) = 0; - virtual bool HasReadRange() const = 0; + virtual bool HasEfficientReadRange() const = 0; virtual void Remove(const std::string& uuid, FileContentType type) = 0; }; + + + // storage area with customData (customData are used only in plugins) + class IPluginStorageArea : public boost::noncopyable + { + public: + virtual ~IPluginStorageArea() + { + } + + virtual void Create(std::string& customData /* out */, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance /* can be NULL if not a DICOM instance */) = 0; + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) = 0; + + virtual bool HasEfficientReadRange() const = 0; + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) = 0; + }; }
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp Fri May 30 12:29:41 2025 +0200 @@ -69,31 +69,6 @@ } - IMemoryBuffer* MemoryStorageArea::Read(const std::string& uuid, - FileContentType type) - { - LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" - << static_cast<int>(type) << "\" content type"; - - Mutex::ScopedLock lock(mutex_); - - Content::const_iterator found = content_.find(uuid); - - if (found == content_.end()) - { - throw OrthancException(ErrorCode_InexistentFile); - } - else if (found->second == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - else - { - return StringMemoryBuffer::CreateFromCopy(*found->second); - } - } - - IMemoryBuffer* MemoryStorageArea::ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, @@ -149,12 +124,6 @@ } - bool MemoryStorageArea::HasReadRange() const - { - return true; - } - - void MemoryStorageArea::Remove(const std::string& uuid, FileContentType type) {
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Fri May 30 12:29:41 2025 +0200 @@ -49,15 +49,15 @@ size_t size, FileContentType type) ORTHANC_OVERRIDE; - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE; - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) ORTHANC_OVERRIDE; - virtual bool HasReadRange() const ORTHANC_OVERRIDE; + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return true; + } virtual void Remove(const std::string& uuid, FileContentType type) ORTHANC_OVERRIDE;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.cpp Fri May 30 12:29:41 2025 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeaders.h" +#include "PluginStorageAreaAdapter.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + PluginStorageAreaAdapter::PluginStorageAreaAdapter(IStorageArea* storage) : + storage_(storage) + { + if (storage == NULL) + { + throw OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + + void PluginStorageAreaAdapter::Create(std::string& customData, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance) + { + customData.clear(); + storage_->Create(uuid, content, size, type); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h Fri May 30 12:29:41 2025 +0200 @@ -0,0 +1,69 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "IStorageArea.h" + + +namespace Orthanc +{ + class PluginStorageAreaAdapter : public IPluginStorageArea + { + private: + std::unique_ptr<IStorageArea> storage_; + + public: + explicit PluginStorageAreaAdapter(IStorageArea* storage /* takes ownership */); + + virtual void Create(std::string& customData, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance) ORTHANC_OVERRIDE; + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) ORTHANC_OVERRIDE + { + return storage_->ReadRange(uuid, type, start, end); + } + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + storage_->Remove(uuid, type); + } + + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return storage_->HasEfficientReadRange(); + } + }; +}
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Fri May 30 12:29:41 2025 +0200 @@ -275,7 +275,7 @@ }; - StorageAccessor::StorageAccessor(IStorageArea& area) : + StorageAccessor::StorageAccessor(IPluginStorageArea& area) : area_(area), cache_(NULL), metrics_(NULL) @@ -283,7 +283,7 @@ } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, StorageCache& cache) : area_(area), cache_(&cache), @@ -292,7 +292,7 @@ } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, MetricsRegistry& metrics) : area_(area), cache_(NULL), @@ -300,7 +300,7 @@ { } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, StorageCache& cache, MetricsRegistry& metrics) : area_(area), @@ -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_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), 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_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), 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_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), 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_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } buffer->MoveToString(target); @@ -785,4 +779,5 @@ output.AnswerStream(transcoder); } #endif + }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Fri May 30 12:29:41 2025 +0200 @@ -110,7 +110,7 @@ private: class MetricsTimer; - IStorageArea& area_; + IPluginStorageArea& area_; StorageCache* cache_; MetricsRegistry* metrics_; @@ -121,15 +121,15 @@ #endif public: - explicit StorageAccessor(IStorageArea& area); + explicit StorageAccessor(IPluginStorageArea& area); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, StorageCache& cache); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, MetricsRegistry& metrics); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, StorageCache& cache, MetricsRegistry& metrics); @@ -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,
--- a/OrthancFramework/Sources/SystemToolbox.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Fri May 30 12:29:41 2025 +0200 @@ -454,6 +454,64 @@ } } +#if ORTHANC_ENABLE_MD5 == 1 + void SystemToolbox::ComputeStreamMD5(std::string& result, + std::istream& inputStream) + { + Toolbox::MD5Context context; + + const size_t bufferSize = 1024; + char buffer[bufferSize]; + + while (inputStream.good()) + { + inputStream.read(buffer, bufferSize); + std::streamsize bytesRead = inputStream.gcount(); + + if (bytesRead > 0) + { + context.Append(buffer, bytesRead); + } + } + + context.Export(result); + } + + + void SystemToolbox::ComputeFileMD5(std::string& result, + const std::string& path) + { + boost::filesystem::ifstream fileStream; + fileStream.open(path, std::ifstream::in | std::ifstream::binary); + + if (!fileStream.good()) + { + throw OrthancException(ErrorCode_InexistentFile, "File not found: " + path); + } + + ComputeStreamMD5(result, fileStream); + } + + + bool SystemToolbox::CompareFilesMD5(const std::string& path1, + const std::string& path2) + { + if (GetFileSize(path1) != GetFileSize(path2)) + { + return false; + } + else + { + std::string path1md5, path2md5; + + ComputeFileMD5(path1md5, path1); + ComputeFileMD5(path2md5, path2); + + return path1md5 == path2md5; + } + } +#endif + void SystemToolbox::MakeDirectory(const std::string& path) {
--- a/OrthancFramework/Sources/SystemToolbox.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.h Fri May 30 12:29:41 2025 +0200 @@ -30,6 +30,10 @@ # error The macro ORTHANC_SANDBOXED must be defined #endif +#if !defined(ORTHANC_ENABLE_MD5) +# error The macro ORTHANC_ENABLE_MD5 must be defined +#endif + #if ORTHANC_SANDBOXED == 1 # error The namespace SystemToolbox cannot be used in sandboxed environments #endif @@ -83,6 +87,18 @@ static uint64_t GetFileSize(const std::string& path); +#if ORTHANC_ENABLE_MD5 == 1 + static void ComputeStreamMD5(std::string& result, + std::istream& stream); + + static void ComputeFileMD5(std::string& result, + const std::string& path); + + // returns true if file have the same MD5 + static bool CompareFilesMD5(const std::string& path1, + const std::string& path2); +#endif + static void MakeDirectory(const std::string& path); static bool IsExistingFile(const std::string& path);
--- a/OrthancFramework/Sources/Toolbox.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.cpp Fri May 30 12:29:41 2025 +0200 @@ -64,6 +64,7 @@ #include <boost/algorithm/string/join.hpp> #include <boost/lexical_cast.hpp> #include <boost/regex.hpp> +#include <cassert> #if BOOST_VERSION >= 106600 # include <boost/uuid/detail/sha1.hpp> @@ -207,6 +208,112 @@ namespace Orthanc { +#if ORTHANC_ENABLE_MD5 == 1 + static char GetHexadecimalCharacter(uint8_t value) + { + assert(value < 16); + + if (value < 10) + { + return value + '0'; + } + else + { + return (value - 10) + 'a'; + } + } + + + struct Toolbox::MD5Context::PImpl + { + md5_state_s state_; + bool done_; + + PImpl() : + done_(false) + { + md5_init(&state_); + } + }; + + + Toolbox::MD5Context::MD5Context() : + pimpl_(new PImpl) + { + } + + + void Toolbox::MD5Context::Append(const void* data, + size_t size) + { + static const size_t MAX_SIZE = 128 * 1024 * 1024; + + if (pimpl_->done_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + const uint8_t *p = reinterpret_cast<const uint8_t*>(data); + + while (size > 0) + { + /** + * The built-in implementation of MD5 requires that "size" can + * be casted to "int", so we feed it by chunks of maximum + * 128MB. This fixes an incorrect behavior in Orthanc <= 1.12.7. + **/ + + int chunkSize; + if (size > MAX_SIZE) + { + chunkSize = static_cast<int>(MAX_SIZE); + } + else + { + chunkSize = static_cast<int>(size); + } + + md5_append(&pimpl_->state_, reinterpret_cast<const md5_byte_t*>(p), chunkSize); + + p += chunkSize; + + assert(static_cast<size_t>(chunkSize) <= size); + size -= chunkSize; + } + } + + + void Toolbox::MD5Context::Append(const std::string& source) + { + if (source.size() > 0) + { + Append(source.c_str(), source.size()); + } + } + + + void Toolbox::MD5Context::Export(std::string& target) + { + if (pimpl_->done_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + pimpl_->done_ = true; + + md5_byte_t actualHash[16]; + md5_finish(&pimpl_->state_, actualHash); + + target.resize(32); + for (unsigned int i = 0; i < 16; i++) + { + target[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16)); + target[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16)); + } + } +#endif /* ORTHANC_ENABLE_MD5 */ + + void Toolbox::LinesIterator::FindEndOfLine() { lineEnd_ = lineStart_; @@ -444,21 +551,6 @@ #if ORTHANC_ENABLE_MD5 == 1 - static char GetHexadecimalCharacter(uint8_t value) - { - assert(value < 16); - - if (value < 10) - { - return value + '0'; - } - else - { - return (value - 10) + 'a'; - } - } - - void Toolbox::ComputeMD5(std::string& result, const std::string& data) { @@ -477,25 +569,9 @@ const void* data, size_t size) { - md5_state_s state; - md5_init(&state); - - if (size > 0) - { - md5_append(&state, - reinterpret_cast<const md5_byte_t*>(data), - static_cast<int>(size)); - } - - md5_byte_t actualHash[16]; - md5_finish(&state, actualHash); - - result.resize(32); - for (unsigned int i = 0; i < 16; i++) - { - result[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16)); - result[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16)); - } + MD5Context context; + context.Append(data, size); + context.Export(result); } void Toolbox::ComputeMD5(std::string& result,
--- a/OrthancFramework/Sources/Toolbox.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.h Fri May 30 12:29:41 2025 +0200 @@ -82,6 +82,25 @@ class ORTHANC_PUBLIC Toolbox { public: +#if ORTHANC_ENABLE_MD5 == 1 + class ORTHANC_PUBLIC MD5Context : public boost::noncopyable + { + private: + class PImpl; + boost::shared_ptr<PImpl> pimpl_; + + public: + MD5Context(); + + void Append(const void* data, + size_t size); + + void Append(const std::string& source); + + void Export(std::string& target); + }; +#endif + class ORTHANC_PUBLIC LinesIterator : public boost::noncopyable { private:
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Fri May 30 12:29:41 2025 +0200 @@ -30,10 +30,9 @@ #include <gtest/gtest.h> #include "../Sources/FileStorage/FilesystemStorage.h" +#include "../Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../Sources/FileStorage/StorageAccessor.h" #include "../Sources/FileStorage/StorageCache.h" -#include "../Sources/HttpServer/BufferHttpSender.h" -#include "../Sources/HttpServer/FilesystemHttpSender.h" #include "../Sources/Logging.h" #include "../Sources/OrthancException.h" #include "../Sources/Toolbox.h" @@ -63,12 +62,18 @@ s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); std::string d; { - std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown)); + std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); buffer->MoveToString(d); } ASSERT_EQ(d.size(), data.size()); ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } } TEST(FilesystemStorage, Basic2) @@ -81,12 +86,18 @@ s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); std::string d; { - std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown)); + std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); buffer->MoveToString(d); } ASSERT_EQ(d.size(), data.size()); ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } } TEST(FilesystemStorage, FileWithSameNameAsTopDirectory) @@ -169,13 +180,13 @@ TEST(StorageAccessor, NoCompression) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true); - + const std::string data = "Hello world"; + FileInfo info = accessor.Write(data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, NULL); + std::string r; accessor.Read(r, info); @@ -191,13 +202,13 @@ TEST(StorageAccessor, Compression) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true); - + const std::string data = "Hello world"; + FileInfo info = accessor.Write(data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, NULL); + std::string r; accessor.Read(r, info); @@ -212,17 +223,17 @@ TEST(StorageAccessor, Mix) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string r; - std::string compressedData = "Hello"; - std::string uncompressedData = "HelloWorld"; + const std::string compressedData = "Hello"; + const std::string uncompressedData = "HelloWorld"; - FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom, CompressionType_ZlibWithSize, false); - FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom, CompressionType_None, false); - + FileInfo compressedInfo = accessor.Write(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);
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Fri May 30 12:29:41 2025 +0200 @@ -397,6 +397,25 @@ Toolbox::ComputeMD5(s, set); ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); // set md5 same as string with the values sorted + + { + Toolbox::MD5Context context; + context.Append(""); + context.Append(NULL, 0); + context.Append("Hello"); + context.Export(s); + ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s); + ASSERT_THROW(context.Append("World"), OrthancException); + ASSERT_THROW(context.Export(s), OrthancException); + } + +#if ORTHANC_SANDBOXED != 1 + { + std::istringstream iss(std::string("aaabbbccc")); + SystemToolbox::ComputeStreamMD5(s, iss); + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); + } +#endif } TEST(Toolbox, ComputeSHA1) @@ -1591,6 +1610,47 @@ #endif +#if ORTHANC_SANDBOXED != 1 && ORTHANC_ENABLE_MD5 == 1 +TEST(Toolbox, FileMD5) +{ + { + TemporaryFile tmp1, tmp2; + std::string s = "aaabbbccc"; + + SystemToolbox::WriteFile(s, tmp1.GetPath()); + SystemToolbox::WriteFile(s, tmp2.GetPath()); + + std::string md5; + SystemToolbox::ComputeFileMD5(md5, tmp1.GetPath()); + + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", md5); + ASSERT_TRUE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } + + { // different sizes + TemporaryFile tmp1, tmp2; + std::string s1 = "aaabbbccc"; + std::string s2 = "aaabbbcccd"; + + SystemToolbox::WriteFile(s1, tmp1.GetPath()); + SystemToolbox::WriteFile(s2, tmp2.GetPath()); + + ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } + + { // same sizes, different contents + TemporaryFile tmp1, tmp2; + std::string s1 = "aaabbbccc"; + std::string s2 = "aaabbbccd"; + + SystemToolbox::WriteFile(s1, tmp1.GetPath()); + SystemToolbox::WriteFile(s2, tmp2.GetPath()); + + ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } +} +#endif + #if ORTHANC_SANDBOXED != 1 TEST(Toolbox, GetMacAddressess) {
--- a/OrthancServer/CMakeLists.txt Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/CMakeLists.txt Fri May 30 12:29:41 2025 +0200 @@ -243,15 +243,17 @@ ##################################################################### set(ORTHANC_EMBEDDED_FILES - CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json - DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt - FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json - LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua - PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql - UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql - INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql - INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua + PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql + INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql + INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + INSTALL_REVISION_AND_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql + INSTALL_KEY_VALUE_STORE_AND_QUEUES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallKeyValueStoresAndQueues.sql ) if (STANDALONE_BUILD)
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Fri May 30 12:29:41 2025 +0200 @@ -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); } @@ -1448,6 +1450,67 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void EnqueueValue(const std::string& queueId, + const std::string& value) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetAttachment(FileInfo& attachment, + int64_t& revision, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid, + const std::string& customData) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } }; @@ -1620,7 +1683,7 @@ void OrthancPluginDatabase::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { VoidDatabaseListener listener;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Fri May 30 12:29:41 2025 +0200 @@ -103,7 +103,7 @@ virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Fri May 30 12:29:41 2025 +0200 @@ -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); } @@ -677,7 +679,6 @@ } } - virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) ORTHANC_OVERRIDE @@ -1061,6 +1062,67 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void EnqueueValue(const std::string& queueId, + const std::string& value) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetAttachment(FileInfo& attachment, + int64_t& revision, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid, + const std::string& customData) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } }; @@ -1231,7 +1293,7 @@ void OrthancPluginDatabaseV3::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { VoidDatabaseListener listener;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Fri May 30 12:29:41 2025 +0200 @@ -76,7 +76,7 @@ virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Fri May 30 12:29:41 2025 +0200 @@ -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.8 request.mutable_add_attachment()->set_revision(revision); ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request); @@ -1016,7 +1018,57 @@ } } - + + virtual bool GetAttachment(FileInfo& attachment, + int64_t& revision, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_attachment()->set_uuid(attachmentUuid); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ATTACHMENT, request); + + if (response.get_attachment().found()) + { + revision = response.get_attachment().revision(); + attachment = Convert(response.get_attachment().attachment()); + return true; + } + else + { + return false; + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid, + const std::string& customData) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_update_attachment_custom_data()->set_uuid(attachmentUuid); + request.mutable_update_attachment_custom_data()->set_custom_data(customData); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_UPDATE_ATTACHMENT_CUSTOM_DATA, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) ORTHANC_OVERRIDE @@ -1805,6 +1857,185 @@ find.ExecuteExpand(response, capabilities, request, identifier); } } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_store_key_value()->set_store_id(storeId); + request.mutable_store_key_value()->set_key(key); + request.mutable_store_key_value()->set_value(value); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_STORE_KEY_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_delete_key_value()->set_store_id(storeId); + request.mutable_delete_key_value()->set_key(key); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_DELETE_KEY_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_key_value()->set_store_id(storeId); + request.mutable_get_key_value()->set_key(key); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_KEY_VALUE, request); + + if (response.get_key_value().found()) + { + value = response.get_key_value().value(); + return true; + } + else + { + return false; + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool fromFirst, + const std::string& fromKey, + uint64_t limit) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_list_keys_values()->set_store_id(storeId); + request.mutable_list_keys_values()->set_from_first(fromFirst); + request.mutable_list_keys_values()->set_from_key(fromKey); + request.mutable_list_keys_values()->set_limit(limit); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LIST_KEY_VALUES, request); + + for (int i = 0; i < response.list_keys_values().keys_values_size(); ++i) + { + keys.push_back(response.list_keys_values().keys_values(i).key()); + values.push_back(response.list_keys_values().keys_values(i).value()); + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void EnqueueValue(const std::string& queueId, + const std::string& value) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_enqueue_value()->set_queue_id(queueId); + request.mutable_enqueue_value()->set_value(value); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_ENQUEUE_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_dequeue_value()->set_queue_id(queueId); + + switch (origin) + { + case QueueOrigin_Back: + request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_BACK); + break; + + case QueueOrigin_Front: + request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_FRONT); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DEQUEUE_VALUE, request); + + if (response.dequeue_value().found()) + { + value = response.dequeue_value().value(); + return true; + } + else + { + return false; + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_queue_size()->set_queue_id(queueId); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_QUEUE_SIZE, request); + + return response.get_queue_size().size(); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } }; @@ -1895,6 +2126,9 @@ dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency()); dbCapabilities_.SetHasExtendedChanges(systemInfo.has_extended_changes()); dbCapabilities_.SetHasFindSupport(systemInfo.supports_find()); + dbCapabilities_.SetKeyValueStoresSupport(systemInfo.supports_key_value_stores()); + dbCapabilities_.SetQueuesSupport(systemInfo.supports_queues()); + dbCapabilities_.SetAttachmentCustomDataSupport(systemInfo.has_attachment_custom_data()); } open_ = true; @@ -1961,7 +2195,7 @@ void OrthancPluginDatabaseV4::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { if (!open_) {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Fri May 30 12:29:41 2025 +0200 @@ -88,7 +88,7 @@ virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri May 30 12:29:41 2025 +0200 @@ -39,7 +39,7 @@ #include "../../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h" #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" -#include "../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h" +#include "../../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../../OrthancFramework/Sources/HttpServer/HttpServer.h" #include "../../../OrthancFramework/Sources/HttpServer/HttpToolbox.h" #include "../../../OrthancFramework/Sources/Images/Image.h" @@ -79,6 +79,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: @@ -549,9 +668,48 @@ } } }; - - - class StorageAreaBase : public IStorageArea + + + static IMemoryBuffer* GetRangeFromWhole(std::unique_ptr<MallocMemoryBuffer>& whole, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */) + { + if (start > end) + { + throw OrthancException(ErrorCode_BadRange); + } + else if (start == end) + { + return new StringMemoryBuffer; // Empty + } + else + { + 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); + } + } + } + + + // "legacy" storage plugins don't store customData -> derive from IStorageArea + class StorageAreaWithoutCustomData : public IStorageArea { private: OrthancPluginStorageCreate create_; @@ -564,50 +722,10 @@ return errorDictionary_; } - IMemoryBuffer* RangeFromWhole(const std::string& uuid, - FileContentType type, - uint64_t start /* inclusive */, - uint64_t end /* exclusive */) - { - if (start > end) - { - throw OrthancException(ErrorCode_BadRange); - } - else if (start == end) - { - return new StringMemoryBuffer; // Empty - } - else - { - std::unique_ptr<IMemoryBuffer> whole(Read(uuid, type)); - - if (start == 0 && - end == whole->GetSize()) - { - return whole.release(); - } - else if (end > whole->GetSize()) - { - throw OrthancException(ErrorCode_BadRange); - } - else - { - std::string range; - range.resize(end - start); - assert(!range.empty()); - - memcpy(&range[0], reinterpret_cast<const char*>(whole->GetData()) + start, range.size()); - - whole.reset(NULL); - return StringMemoryBuffer::CreateFromSwap(range); - } - } - } - public: - StorageAreaBase(OrthancPluginStorageCreate create, - OrthancPluginStorageRemove remove, - PluginsErrorDictionary& errorDictionary) : + StorageAreaWithoutCustomData(OrthancPluginStorageCreate create, + OrthancPluginStorageRemove remove, + PluginsErrorDictionary& errorDictionary) : create_(create), remove_(remove), errorDictionary_(errorDictionary) @@ -649,24 +767,16 @@ }; - class PluginStorageArea : public StorageAreaBase + class PluginStorageAreaV1 : public StorageAreaWithoutCustomData { private: OrthancPluginStorageRead read_; OrthancPluginFree free_; - void Free(void* buffer) const - { - if (buffer != NULL) - { - free_(buffer); - } - } - public: - PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks, - PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaV1(const _OrthancPluginRegisterStorageArea& callbacks, + PluginsErrorDictionary& errorDictionary) : + StorageAreaWithoutCustomData(callbacks.create, callbacks.remove, errorDictionary), read_(callbacks.read), free_(callbacks.free) { @@ -676,38 +786,30 @@ } } - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE - { - std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer); - - void* buffer = NULL; - int64_t size = 0; - - OrthancPluginErrorCode error = read_ - (&buffer, &size, uuid.c_str(), Plugins::Convert(type)); - - if (error == OrthancPluginErrorCode_Success) - { - result->Assign(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 */) ORTHANC_OVERRIDE { - return RangeFromWhole(uuid, type, start, end); - } - - virtual bool HasReadRange() const ORTHANC_OVERRIDE + void* buffer = NULL; + int64_t size = 0; + + OrthancPluginErrorCode error = read_(&buffer, &size, uuid.c_str(), Plugins::Convert(type)); + + if (error == OrthancPluginErrorCode_Success) + { + std::unique_ptr<MallocMemoryBuffer> whole(new MallocMemoryBuffer); + whole->Assign(buffer, size, free_); + return GetRangeFromWhole(whole, start, end); + } + else + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE { return false; } @@ -715,16 +817,16 @@ // New in Orthanc 1.9.0 - class PluginStorageArea2 : public StorageAreaBase + class PluginStorageAreaV2 : public StorageAreaWithoutCustomData { private: OrthancPluginStorageReadWhole readWhole_; OrthancPluginStorageReadRange readRange_; public: - PluginStorageArea2(const _OrthancPluginRegisterStorageArea2& callbacks, - PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaV2(const _OrthancPluginRegisterStorageArea2& callbacks, + PluginsErrorDictionary& errorDictionary) : + StorageAreaWithoutCustomData(callbacks.create, callbacks.remove, errorDictionary), readWhole_(callbacks.readWhole), readRange_(callbacks.readRange) { @@ -734,29 +836,6 @@ } } - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE - { - std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer); - - OrthancPluginMemoryBuffer64 buffer; - buffer.size = 0; - buffer.data = NULL; - - OrthancPluginErrorCode error = readWhole_(&buffer, uuid.c_str(), Plugins::Convert(type)); - - if (error == OrthancPluginErrorCode_Success) - { - result->Assign(buffer.data, buffer.size, ::free); - return result.release(); - } - else - { - GetErrorDictionary().LogError(error, true); - throw OrthancException(static_cast<ErrorCode>(error)); - } - } - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, @@ -764,7 +843,23 @@ { if (readRange_ == NULL) { - return RangeFromWhole(uuid, type, start, end); + OrthancPluginMemoryBuffer64 buffer; + buffer.size = 0; + buffer.data = NULL; + + OrthancPluginErrorCode error = readWhole_(&buffer, uuid.c_str(), Plugins::Convert(type)); + + if (error == OrthancPluginErrorCode_Success) + { + std::unique_ptr<MallocMemoryBuffer> whole(new MallocMemoryBuffer); + whole->Assign(buffer.data, buffer.size, ::free); + return GetRangeFromWhole(whole, start, end); + } + else + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } } else { @@ -802,26 +897,153 @@ } } - virtual bool HasReadRange() const ORTHANC_OVERRIDE + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE { return (readRange_ != NULL); } }; + // New in Orthanc 1.12.8 + class PluginStorageAreaV3 : public IPluginStorageArea + { + private: + OrthancPluginStorageCreate2 create_; + OrthancPluginStorageReadRange2 readRange_; + OrthancPluginStorageRemove2 remove_; + + PluginsErrorDictionary& errorDictionary_; + + protected: + PluginsErrorDictionary& GetErrorDictionary() const + { + return errorDictionary_; + } + + public: + PluginStorageAreaV3(const _OrthancPluginRegisterStorageArea3& callbacks, + PluginsErrorDictionary& errorDictionary) : + create_(callbacks.create), + readRange_(callbacks.readRange), + remove_(callbacks.remove), + errorDictionary_(errorDictionary) + { + if (create_ == NULL || + readRange_ == NULL || + remove_ == NULL) + { + throw OrthancException(ErrorCode_Plugin, "Storage area plugin does not implement all the required primitives (create, remove, and readRange)"); + } + } + + virtual void Create(std::string& customData /* out */, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance /* can be NULL if not a DICOM instance */) ORTHANC_OVERRIDE + { + 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 = remove_(uuid.c_str(), Plugins::Convert(type), + customData.empty() ? NULL : customData.c_str(), customData.size()); + + if (error != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) ORTHANC_OVERRIDE + { + if (start > end) + { + throw OrthancException(ErrorCode_BadRange); + } + else if (start == end) + { + return new 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 = + readRange_(&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 HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return true; + } + }; + + class StorageAreaFactory : public boost::noncopyable { private: enum Version { Version1, - Version2 + Version2, + Version3 }; SharedLibrary& sharedLibrary_; Version version_; - _OrthancPluginRegisterStorageArea callbacks_; + _OrthancPluginRegisterStorageArea callbacks1_; _OrthancPluginRegisterStorageArea2 callbacks2_; + _OrthancPluginRegisterStorageArea3 callbacks3_; PluginsErrorDictionary& errorDictionary_; static void WarnNoReadRange() @@ -835,7 +1057,7 @@ PluginsErrorDictionary& errorDictionary) : sharedLibrary_(sharedLibrary), version_(Version1), - callbacks_(callbacks), + callbacks1_(callbacks), errorDictionary_(errorDictionary) { WarnNoReadRange(); @@ -855,20 +1077,37 @@ } } + StorageAreaFactory(SharedLibrary& sharedLibrary, + const _OrthancPluginRegisterStorageArea3& callbacks, + PluginsErrorDictionary& errorDictionary) : + sharedLibrary_(sharedLibrary), + version_(Version3), + callbacks3_(callbacks), + errorDictionary_(errorDictionary) + { + if (callbacks.readRange == NULL) + { + WarnNoReadRange(); + } + } + SharedLibrary& GetSharedLibrary() { return sharedLibrary_; } - IStorageArea* Create() const + IPluginStorageArea* Create() const { switch (version_) { case Version1: - return new PluginStorageArea(callbacks_, errorDictionary_); + return new PluginStorageAreaAdapter(new PluginStorageAreaV1(callbacks1_, errorDictionary_)); case Version2: - return new PluginStorageArea2(callbacks2_, errorDictionary_); + return new PluginStorageAreaAdapter(new PluginStorageAreaV2(callbacks2_, errorDictionary_)); + + case Version3: + return new PluginStorageAreaV3(callbacks3_, errorDictionary_); default: throw OrthancException(ErrorCode_InternalError); @@ -2527,125 +2766,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 +3731,12 @@ break; } + case OrthancPluginCompressionType_None: + { + CopyToMemoryBuffer(*p.target, p.source, p.size); + return; + } + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -4499,6 +4625,164 @@ reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendMultipartItem(p.answer, p.answerSize, headers); } + // static FileInfo CreateFileInfoFromPluginAttachment(OrthancPluginAttachment* attachment) + // { + // return FileInfo(attachment->uuid, + // Orthanc::Plugins::Convert(attachment->contentType), + // attachment->uncompressedSize, + // attachment->uncompressedHash + // ) fileInfo() + // } + + static FileInfo Convert(const OrthancPluginAttachment2& attachment) + { + std::string uuid, customData; + if (attachment.uuid != NULL) + { + uuid = attachment.uuid; + } + else + { + uuid = Toolbox::GenerateUuid(); + } + + if (attachment.customData != NULL) + { + customData = std::string(reinterpret_cast<const char*>(attachment.customData), attachment.customDataSize); + } + + return FileInfo(uuid, + Orthanc::Plugins::Convert(static_cast<OrthancPluginContentType>(attachment.contentType)), + attachment.uncompressedSize, + attachment.uncompressedHash, + Orthanc::Plugins::Convert(static_cast<OrthancPluginCompressionType>(attachment.compressionType)), + attachment.compressedSize, + attachment.compressedHash, + customData); + } + + void OrthancPlugins::ApplyAdoptAttachment(const _OrthancPluginAdoptAttachment& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + FileInfo adoptedFile = Convert(*(parameters.attachmentInfo)); + + if (adoptedFile.GetContentType() == FileContentType_Dicom) + { + std::unique_ptr<DicomInstanceToStore> dicom(DicomInstanceToStore::CreateFromBuffer(parameters.buffer, parameters.bufferSize)); + dicom->SetOrigin(DicomInstanceOrigin::FromPlugins()); + + std::string resultPublicId; + + ServerContext::StoreResult result = lock.GetContext().AdoptAttachment(resultPublicId, *dicom, StoreInstanceMode_Default, adoptedFile); + + CopyToMemoryBuffer(*parameters.attachmentUuid, adoptedFile.GetUuid().size() > 0 ? adoptedFile.GetUuid().c_str() : NULL, adoptedFile.GetUuid().size()); + CopyToMemoryBuffer(*parameters.createdResourceId, resultPublicId.size() > 0 ? resultPublicId.c_str() : NULL, resultPublicId.size()); + *(parameters.storeStatus) = Plugins::Convert(result.GetStatus()); + } + } + + void OrthancPlugins::ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + FileInfo fileInfo; + int64_t revision; + + if (lock.GetContext().GetIndex().GetAttachment(fileInfo, revision, parameters.attachmentUuid)) + { + CopyToMemoryBuffer(*parameters.customData, fileInfo.GetCustomData().size() > 0 ? fileInfo.GetCustomData().c_str() : NULL, fileInfo.GetCustomData().size()); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + void OrthancPlugins::ApplyUpdateAttachmentCustomData(const _OrthancPluginUpdateAttachmentCustomData& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + FileInfo fileInfo; + std::string customData(parameters.customData, parameters.customDataSize); + + lock.GetContext().GetIndex().UpdateAttachmentCustomData(parameters.attachmentUuid, customData); + } + + bool OrthancPlugins::HasKeyValueStoresSupport() + { + PImpl::ServerContextReference lock(*pimpl_); + + return lock.GetContext().GetIndex().HasKeyValueStoresSupport(); + } + + void OrthancPlugins::ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + std::string value(reinterpret_cast<const char*>(parameters.value), parameters.valueSize); + + lock.GetContext().GetIndex().StoreKeyValue(parameters.storeId, parameters.key, value); + } + + void OrthancPlugins::ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + lock.GetContext().GetIndex().DeleteKeyValue(parameters.storeId, parameters.key); + } + + void OrthancPlugins::ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + std::string value; + + if (lock.GetContext().GetIndex().GetKeyValue(value, parameters.storeId, parameters.key)) + { + CopyToMemoryBuffer(*parameters.target, value.size() > 0 ? value.c_str() : NULL, value.size()); + *parameters.found = true; + } + else + { + *parameters.found = false; + } + } + + bool OrthancPlugins::HasQueuesSupport() + { + PImpl::ServerContextReference lock(*pimpl_); + + return lock.GetContext().GetIndex().HasQueuesSupport(); + } + + void OrthancPlugins::ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + std::string value(reinterpret_cast<const char*>(parameters.value), parameters.valueSize); + + lock.GetContext().GetIndex().EnqueueValue(parameters.queueId, value); + } + + void OrthancPlugins::ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + std::string value; + + if (lock.GetContext().GetIndex().DequeueValue(value, parameters.queueId, Plugins::Convert(parameters.origin))) + { + CopyToMemoryBuffer(*parameters.target, value.size() > 0 ? value.c_str() : NULL, value.size()); + *parameters.found = true; + } + else + { + *parameters.found = false; + } + } + + void OrthancPlugins::ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + *parameters.size = lock.GetContext().GetIndex().GetQueueSize(parameters.queueId); + } void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params) { @@ -5066,32 +5350,13 @@ return true; case _OrthancPluginService_StorageAreaCreate: - { - const _OrthancPluginStorageAreaCreate& p = - *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - storage.Create(p.uuid, p.content, static_cast<size_t>(p.size), Plugins::Convert(p.type)); - return true; - } + throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaCreate() is only available in Orthanc <= 1.12.6"); case _OrthancPluginService_StorageAreaRead: - { - const _OrthancPluginStorageAreaRead& p = - *reinterpret_cast<const _OrthancPluginStorageAreaRead*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - std::unique_ptr<IMemoryBuffer> content(storage.Read(p.uuid, Plugins::Convert(p.type))); - CopyToMemoryBuffer(*p.target, content->GetData(), content->GetSize()); - return true; - } + throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaRead() is only available in Orthanc <= 1.12.6"); case _OrthancPluginService_StorageAreaRemove: - { - const _OrthancPluginStorageAreaRemove& p = - *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - storage.Remove(p.uuid, Plugins::Convert(p.type)); - return true; - } + throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaRemove() is only available in Orthanc <= 1.12.6"); case _OrthancPluginService_DicomBufferToJson: case _OrthancPluginService_DicomInstanceToJson: @@ -5587,6 +5852,183 @@ return true; } + case _OrthancPluginService_AdoptAttachment: + { + const _OrthancPluginAdoptAttachment& p = + *reinterpret_cast<const _OrthancPluginAdoptAttachment*>(parameters); + ApplyAdoptAttachment(p); + return true; + } + + case _OrthancPluginService_GetAttachmentCustomData: + { + const _OrthancPluginGetAttachmentCustomData& p = + *reinterpret_cast<const _OrthancPluginGetAttachmentCustomData*>(parameters); + ApplyGetAttachmentCustomData(p); + return true; + } + + case _OrthancPluginService_UpdateAttachmentCustomData: + { + const _OrthancPluginUpdateAttachmentCustomData& p = + *reinterpret_cast<const _OrthancPluginUpdateAttachmentCustomData*>(parameters); + ApplyUpdateAttachmentCustomData(p); + return true; + } + + case _OrthancPluginService_StoreKeyValue: + if (!HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support key-value stores"); + } + else + { + const _OrthancPluginStoreKeyValue& p = + *reinterpret_cast<const _OrthancPluginStoreKeyValue*>(parameters); + ApplyStoreKeyValue(p); + return true; + } + + case _OrthancPluginService_DeleteKeyValue: + if (!HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support key-value stores"); + } + else + { + const _OrthancPluginDeleteKeyValue& p = + *reinterpret_cast<const _OrthancPluginDeleteKeyValue*>(parameters); + ApplyDeleteKeyValue(p); + return true; + } + + case _OrthancPluginService_GetKeyValue: + if (!HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support key-value stores"); + } + else + { + const _OrthancPluginGetKeyValue& p = + *reinterpret_cast<const _OrthancPluginGetKeyValue*>(parameters); + ApplyGetKeyValue(p); + return true; + } + + case _OrthancPluginService_CreateKeysValuesIterator: + if (!HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support key-value stores"); + } + else + { + const _OrthancPluginCreateKeysValuesIterator& p = + *reinterpret_cast<const _OrthancPluginCreateKeysValuesIterator*>(parameters); + + { + PImpl::ServerContextReference lock(*pimpl_); + *p.target = reinterpret_cast<OrthancPluginKeysValuesIterator*>(new StatelessDatabaseOperations::KeysValuesIterator(lock.GetContext().GetIndex(), p.storeId)); + } + + return true; + } + + case _OrthancPluginService_FreeKeysValuesIterator: + if (!HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support key-value stores"); + } + else + { + const _OrthancPluginFreeKeysValuesIterator& p = + *reinterpret_cast<const _OrthancPluginFreeKeysValuesIterator*>(parameters); + delete reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + return true; + } + + case _OrthancPluginService_KeysValuesIteratorNext: + if (!HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support key-value stores"); + } + else + { + const _OrthancPluginKeysValuesIteratorNext& p = + *reinterpret_cast<const _OrthancPluginKeysValuesIteratorNext*>(parameters); + + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + *p.done = iterator.Next() ? 1 : 0; + return true; + } + + case _OrthancPluginService_KeysValuesIteratorGetKey: + case _OrthancPluginService_KeysValuesIteratorGetValue: + if (!HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support key-value stores"); + } + else + { + const _OrthancPluginKeysValuesIteratorGetString& p = + *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetString*>(parameters); + + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + + if (service == _OrthancPluginService_KeysValuesIteratorGetKey) + { + *p.target = iterator.GetKey().c_str(); + return true; + } + else if (service == _OrthancPluginService_KeysValuesIteratorGetValue) + { + *p.target = iterator.GetValue().c_str(); + return true; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + case _OrthancPluginService_EnqueueValue: + if (!HasQueuesSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support queues"); + } + else + { + const _OrthancPluginEnqueueValue& p = + *reinterpret_cast<const _OrthancPluginEnqueueValue*>(parameters); + ApplyEnqueueValue(p); + return true; + } + + case _OrthancPluginService_DequeueValue: + if (!HasQueuesSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support queues"); + } + else + { + const _OrthancPluginDequeueValue& p = + *reinterpret_cast<const _OrthancPluginDequeueValue*>(parameters); + ApplyDequeueValue(p); + return true; + } + + case _OrthancPluginService_GetQueueSize: + if (!HasQueuesSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The DB engine does not support queues"); + } + else + { + const _OrthancPluginGetQueueSize& p = + *reinterpret_cast<const _OrthancPluginGetQueueSize*>(parameters); + ApplyGetQueueSize(p); + return true; + } + default: return false; } @@ -5670,23 +6112,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 +6262,7 @@ case _OrthancPluginService_RegisterDatabaseBackendV4: { - CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end"; + CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end (v4)"; const _OrthancPluginRegisterDatabaseBackendV4& p = *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV4*>(parameters); @@ -5874,7 +6327,7 @@ VoidDatabaseListener listener; { - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); + IPluginStorageArea& storage = *reinterpret_cast<IPluginStorageArea*>(p.storageArea); std::unique_ptr<IDatabaseWrapper::ITransaction> transaction( pimpl_->database_->StartTransaction(TransactionType_ReadWrite, listener)); @@ -5973,7 +6426,7 @@ } - IStorageArea* OrthancPlugins::CreateStorageArea() + IPluginStorageArea* OrthancPlugins::CreateStorageArea() { if (!HasStorageArea()) {
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Fri May 30 12:29:41 2025 +0200 @@ -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); @@ -220,6 +223,28 @@ void ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& parameters); + void ApplyAdoptAttachment(const _OrthancPluginAdoptAttachment& parameters); + + void ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters); + + void ApplyUpdateAttachmentCustomData(const _OrthancPluginUpdateAttachmentCustomData& parameters); + + bool HasKeyValueStoresSupport(); + + void ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters); + + void ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters); + + void ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters); + + bool HasQueuesSupport(); + + void ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters); + + void ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters); + + void ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters); + void ComputeHash(_OrthancPluginService service, const void* parameters); @@ -291,7 +316,7 @@ bool HasStorageArea() const; - IStorageArea* CreateStorageArea(); // To be freed after use + IPluginStorageArea* CreateStorageArea(); // To be freed after use const SharedLibrary& GetStorageAreaLibrary() const;
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Fri May 30 12:29:41 2025 +0200 @@ -664,5 +664,116 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + OrthancPluginCompressionType Convert(CompressionType type) + { + switch (type) + { + case CompressionType_None: + return OrthancPluginCompressionType_None; + + case CompressionType_ZlibWithSize: + return OrthancPluginCompressionType_ZlibWithSize; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + CompressionType Convert(OrthancPluginCompressionType type) + { + switch (type) + { + case OrthancPluginCompressionType_None: + return CompressionType_None; + + case OrthancPluginCompressionType_ZlibWithSize: + return CompressionType_ZlibWithSize; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + OrthancPluginStoreStatus Convert(StoreStatus status) + { + switch (status) + { + case StoreStatus_Success: + return OrthancPluginStoreStatus_Success; + + case StoreStatus_AlreadyStored: + return OrthancPluginStoreStatus_AlreadyStored; + + case StoreStatus_Failure: + return OrthancPluginStoreStatus_Failure; + + case StoreStatus_FilteredOut: + return OrthancPluginStoreStatus_FilteredOut; + + case StoreStatus_StorageFull: + return OrthancPluginStoreStatus_StorageFull; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + StoreStatus Convert(OrthancPluginStoreStatus status) + { + switch (status) + { + case OrthancPluginStoreStatus_Success: + return StoreStatus_Success; + + case OrthancPluginStoreStatus_AlreadyStored: + return StoreStatus_AlreadyStored; + + case OrthancPluginStoreStatus_Failure: + return StoreStatus_Failure; + + case OrthancPluginStoreStatus_FilteredOut: + return StoreStatus_FilteredOut; + + case OrthancPluginStoreStatus_StorageFull: + return StoreStatus_StorageFull; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + OrthancPluginQueueOrigin Convert(QueueOrigin origin) + { + switch (origin) + { + case QueueOrigin_Front: + return OrthancPluginQueueOrigin_Front; + + case QueueOrigin_Back: + return OrthancPluginQueueOrigin_Back; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + QueueOrigin Convert(OrthancPluginQueueOrigin origin) + { + switch (origin) + { + case OrthancPluginQueueOrigin_Front: + return QueueOrigin_Front; + + case OrthancPluginQueueOrigin_Back: + return QueueOrigin_Back; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h Fri May 30 12:29:41 2025 +0200 @@ -73,6 +73,18 @@ ResourceType Convert(OrthancPluginResourceType type); OrthancPluginConstraintType Convert(ConstraintType constraint); + + OrthancPluginCompressionType Convert(CompressionType type); + + CompressionType Convert(OrthancPluginCompressionType type); + + OrthancPluginStoreStatus Convert(StoreStatus type); + + StoreStatus Convert(OrthancPluginStoreStatus type); + + OrthancPluginQueueOrigin Convert(QueueOrigin type); + + QueueOrigin Convert(OrthancPluginQueueOrigin type); } }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Fri May 30 12:29:41 2025 +0200 @@ -1327,8 +1327,6 @@ } OrthancPluginDatabaseBackendV3; -/*<! @endcond */ - typedef struct { @@ -1361,7 +1359,10 @@ return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, ¶ms); } - + +/*<! @endcond */ + + #ifdef __cplusplus } #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri May 30 12:29:41 2025 +0200 @@ -16,7 +16,7 @@ * - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback(). * - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback(). * - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback(). - * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea2(). + * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea3(). * - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV4(). * - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback(). * - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback(). @@ -121,7 +121,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 12 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 6 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 8 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -469,6 +469,20 @@ _OrthancPluginService_SetMetricsIntegerValue = 43, /* New in Orthanc 1.12.1 */ _OrthancPluginService_SetCurrentThreadName = 44, /* New in Orthanc 1.12.2 */ _OrthancPluginService_LogMessage = 45, /* New in Orthanc 1.12.4 */ + _OrthancPluginService_AdoptAttachment = 46, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetAttachmentCustomData = 47, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_UpdateAttachmentCustomData = 48, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_StoreKeyValue = 49, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DeleteKeyValue = 50, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetKeyValue = 51, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_CreateKeysValuesIterator = 52, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_FreeKeysValuesIterator = 53, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorNext = 54, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetKey = 55, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetValue = 56, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_EnqueueValue = 57, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DequeueValue = 58, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetQueueSize = 59, /* New in Orthanc 1.12.8 */ /* Registration of callbacks */ @@ -492,6 +506,7 @@ _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017, /* New in Orthanc 1.10.0 */ _OrthancPluginService_RegisterReceivedInstanceCallback = 1018, /* New in Orthanc 1.10.0 */ _OrthancPluginService_RegisterWebDavCollection = 1019, /* New in Orthanc 1.10.1 */ + _OrthancPluginService_RegisterStorageArea3 = 1020, /* New in Orthanc 1.12.8 */ /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -562,7 +577,7 @@ _OrthancPluginService_StorageAreaRemove = 5005, _OrthancPluginService_RegisterDatabaseBackendV3 = 5006, /* New in Orthanc 1.9.2 */ _OrthancPluginService_RegisterDatabaseBackendV4 = 5007, /* New in Orthanc 1.12.0 */ - + /* Primitives for handling images */ _OrthancPluginService_GetImagePixelFormat = 6000, _OrthancPluginService_GetImageWidth = 6001, @@ -791,6 +806,7 @@ OrthancPluginCompressionType_ZlibWithSize = 1, /*!< zlib, prefixed with uncompressed size (uint64_t) */ OrthancPluginCompressionType_Gzip = 2, /*!< Standard gzip compression */ OrthancPluginCompressionType_GzipWithSize = 3, /*!< gzip, prefixed with uncompressed size (uint64_t) */ + OrthancPluginCompressionType_None = 4, /*!< No compression (new in Orthanc 1.12.8) */ _OrthancPluginCompressionType_INTERNAL = 0x7fffffff } OrthancPluginCompressionType; @@ -1130,6 +1146,33 @@ /** + * The store status response to AdoptAttachment. + **/ + typedef enum + { + OrthancPluginStoreStatus_Success = 0, /*!< The file has been stored/adopted */ + OrthancPluginStoreStatus_AlreadyStored = 1, /*!< The file has already been stored/adopted (only if OverwriteInstances is set to false)*/ + OrthancPluginStoreStatus_Failure = 2, /*!< The file could not be stored/adopted */ + OrthancPluginStoreStatus_FilteredOut = 3, /*!< The file has been filtered out by a lua script or a plugin */ + OrthancPluginStoreStatus_StorageFull = 4, /*!< The storage is full (only if MaximumStorageSize/MaximumPatientCount is set and MaximumStorageMode is Reject)*/ + + _OrthancPluginStoreStatus_INTERNAL = 0x7fffffff + } OrthancPluginStoreStatus; + + /** + * The supported types of enqueuing + **/ + typedef enum + { + OrthancPluginQueueOrigin_Front = 0, /*!< Pop from the front of the queue */ + OrthancPluginQueueOrigin_Back = 1, /*!< Pop from the back of the queue */ + + _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff + } OrthancPluginQueueOrigin; + + + + /** * @brief A 32-bit memory buffer allocated by the core system of Orthanc. * * A memory buffer allocated by the core system of Orthanc. When the @@ -1367,8 +1410,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 +1485,81 @@ /** + * @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 range of a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * reads a portion of a file from the storage area. Orthanc + * indicates the start position and the length of the range. + * + * @param target Memory buffer where to store the content of the range. + * The memory buffer is allocated and freed by Orthanc. The length of the range + * of interest corresponds to the size of this buffer. + * @param uuid The UUID of the file of interest. + * @param type The content type corresponding to this file. + * @param rangeStart Start position of the requested range in the file. + * @param customData The custom data of the file of interest. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + uint64_t rangeStart, + const void* customData, + 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 type The content type corresponding to this file. + * @param customData The custom data of the file to be removed. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) ( + const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint64_t customDataSize); + + + /** * @brief Callback to handle the C-Find SCP requests for worklists. * * Signature of a callback function that is triggered when Orthanc @@ -3327,7 +3444,7 @@ * @param read The callback function to read a file from the custom storage area. * @param remove The callback function to remove a file from the custom storage area. * @ingroup Callbacks - * @deprecated Please use OrthancPluginRegisterStorageArea2() + * @deprecated New plugins should use OrthancPluginRegisterStorageArea3() **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea( OrthancPluginContext* context, @@ -4915,6 +5032,8 @@ * @ingroup Callbacks * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiPut()" on * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaCreate( OrthancPluginContext* context, @@ -4959,6 +5078,8 @@ * @ingroup Callbacks * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiGet()" on * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRead( OrthancPluginContext* context, @@ -4998,6 +5119,8 @@ * @ingroup Callbacks * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiDelete()" on * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRemove( OrthancPluginContext* context, @@ -8917,6 +9040,7 @@ * If this feature is not supported by the plugin, this value can be set to NULL. * @param remove The callback function to remove a file from the custom storage area. * @ingroup Callbacks + * @deprecated New plugins should use OrthancPluginRegisterStorageArea3() **/ ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea2( OrthancPluginContext* context, @@ -9368,6 +9492,41 @@ } + typedef struct + { + OrthancPluginStorageCreate2 create; + OrthancPluginStorageReadRange2 readRange; + OrthancPluginStorageRemove2 remove; + } _OrthancPluginRegisterStorageArea3; + + /** + * @brief Register a custom storage area, with support for custom data. + * + * This function registers a custom storage area, to replace the + * built-in way Orthanc stores its files on the filesystem. This + * function must be called during the initialization of the plugin, + * i.e. inside the OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param create The callback function to store a file on the custom storage area. + * @param readRange The callback function to read some range of a file from the custom storage area. + * 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, + OrthancPluginStorageReadRange2 readRange, + OrthancPluginStorageRemove2 remove) + { + _OrthancPluginRegisterStorageArea3 params; + params.create = create; + params.readRange = readRange; + params.remove = remove; + context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, ¶ms); + } + /** * @brief Signature of a callback function that is triggered when * the Orthanc core requests an operation from the database plugin. @@ -9634,6 +9793,438 @@ return context->InvokeService(context, _OrthancPluginService_SendStreamChunk, ¶ms); } + typedef struct + { + const char* uuid; + int32_t contentType; + uint64_t uncompressedSize; + const char* uncompressedHash; + int32_t compressionType; + uint64_t compressedSize; + const char* compressedHash; + const void* customData; + uint64_t customDataSize; + } OrthancPluginAttachment2; + + + typedef struct + { + const void* buffer; /* in */ + uint64_t bufferSize; /* in */ + OrthancPluginAttachment2* attachmentInfo; /* in, note: uuid may not be defined */ + OrthancPluginResourceType attachToResourceType; /* in */ + const char* attachToResourceId; /* in, can be null in case the attachment is a new instance */ + OrthancPluginMemoryBuffer* createdResourceId; /* out, in case the attachment is actually a new instance */ + OrthancPluginMemoryBuffer* attachmentUuid; /* out */ + OrthancPluginStoreStatus* storeStatus; /* out */ + } _OrthancPluginAdoptAttachment; + + /** + * @brief Tell Orthanc to adopt an existing attachment. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). +TODO_ATTACH_CUSTOM_DATA TODO TODO + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginAdoptAttachment( + OrthancPluginContext* context, + const void* buffer, + uint64_t bufferSize, + OrthancPluginAttachment2* attachmentInfo, + OrthancPluginResourceType attachToResourceType, + const char* attachToResourceId, + OrthancPluginMemoryBuffer* createdResourceId, /* out */ + OrthancPluginMemoryBuffer* attachmentUuid, /* out */ + OrthancPluginStoreStatus* storeStatus /* out */ + ) + { + _OrthancPluginAdoptAttachment params; + params.buffer = buffer; + params.bufferSize = bufferSize; + params.attachmentInfo = attachmentInfo; + params.attachToResourceType = attachToResourceType; + params.attachToResourceId = attachToResourceId; + params.createdResourceId = createdResourceId; + params.attachmentUuid = attachmentUuid; + params.storeStatus = storeStatus; + + return context->InvokeService(context, _OrthancPluginService_AdoptAttachment, ¶ms); + } + + typedef struct + { + const char* attachmentUuid; /* in */ + /* OrthancPluginContentType contentType; */ /* in */ + OrthancPluginMemoryBuffer* customData; /* out */ + } _OrthancPluginGetAttachmentCustomData; + + /** + * @brief Retrieve attachment customData from the Orthanc DB. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). +TODO_ATTACH_CUSTOM_DATA TODO TODO + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetAttachmentCustomData( + OrthancPluginContext* context, + const char* attachmentUuid, /* in */ + /* OrthancPluginContentType contentType, */ /* in */ + OrthancPluginMemoryBuffer* customData /* out */ + ) + { + _OrthancPluginGetAttachmentCustomData params; + params.attachmentUuid = attachmentUuid; + /* params.contentType = contentType; */ + params.customData = customData; + + return context->InvokeService(context, _OrthancPluginService_GetAttachmentCustomData, ¶ms); + } + + typedef struct + { + const char* attachmentUuid; /* in */ + const char* customData; /* in */ + int64_t customDataSize; /* in */ + } _OrthancPluginUpdateAttachmentCustomData; + + + /** + * @brief Update attachment custom data in the Orthanc DB. E.g if a plugin has moved an attachment. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). +TODO_ATTACH_CUSTOM_DATA TODO TODO + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginUpdateAttachmentCustomData( + OrthancPluginContext* context, + const char* attachmentUuid, /* in */ + const char* customData, /* in */ + int64_t customDataSize /* in */ + ) + { + _OrthancPluginUpdateAttachmentCustomData params; + params.attachmentUuid = attachmentUuid; + params.customData = customData; + params.customDataSize = customDataSize; + + return context->InvokeService(context, _OrthancPluginService_UpdateAttachmentCustomData, ¶ms); + } + + + typedef struct + { + const char* storeId; + const char* key; + const void* value; + uint64_t valueSize; + } _OrthancPluginStoreKeyValue; + + /** + * @brief Tell Orthanc to store a key-value in its store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the store + * @param key The key of the value to store (note: storeId + key must be unique) + * @param value The value to store + * @param valueSize The lenght of the value to store + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStoreKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key, /* in */ + const void* value, /* in */ + uint64_t valueSize /* in */) + { + _OrthancPluginStoreKeyValue params; + params.storeId = storeId; + params.key = key; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_StoreKeyValue, ¶ms); + } + + typedef struct + { + const char* storeId; + const char* key; + } _OrthancPluginDeleteKeyValue; + + /** + * @brief Tell Orthanc to delete a key-value from its store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the store + * @param key The key of the value to store (note: storeId + key must be unique) + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDeleteKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginDeleteKeyValue params; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_DeleteKeyValue, ¶ms); + } + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* storeId; + const char* key; + } _OrthancPluginGetKeyValue; + + /** + * @brief Get the value associated to this key in the key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. the key exists in the store + * @param target Memory buffer where to store the retrieved value + * @param storeId A unique identifier identifying both the plugin and the store + * @param key The key of the value to retrieve from the store (note: storeId + key must be unique) + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetKeyValue( + OrthancPluginContext* context, + uint8_t* found, + OrthancPluginMemoryBuffer* target, /* out */ + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginGetKeyValue params; + params.found = found; + params.target = target; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_GetKeyValue, ¶ms); + } + + + /** + * @brief Opaque structure that represents an iterator to the keys and values of + * a key-value store. + * @ingroup Callbacks + **/ + typedef struct _OrthancPluginKeysValuesIterator_t OrthancPluginKeysValuesIterator; + + + + typedef struct + { + OrthancPluginKeysValuesIterator** target; + const char* storeId; + } _OrthancPluginCreateKeysValuesIterator; + + + /* TODO_ATTACH_CUSTOM_DATA TODO DOCUMENT */ + + ORTHANC_PLUGIN_INLINE OrthancPluginKeysValuesIterator* OrthancPluginCreateKeysValuesIterator( + OrthancPluginContext* context, + const char* storeId) + { + OrthancPluginKeysValuesIterator* target = NULL; + + _OrthancPluginCreateKeysValuesIterator params; + params.target = ⌖ + params.storeId = storeId; + + if (context->InvokeService(context, _OrthancPluginService_CreateKeysValuesIterator, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginFreeKeysValuesIterator; + + /* TODO_ATTACH_CUSTOM_DATA TODO DOCUMENT */ + + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeKeysValuesIterator( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + _OrthancPluginFreeKeysValuesIterator params; + params.iterator = iterator; + + context->InvokeService(context, _OrthancPluginService_FreeKeysValuesIterator, ¶ms); + } + + + typedef struct + { + uint8_t* done; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorNext; + + + /* TODO_ATTACH_CUSTOM_DATA TODO DOCUMENT */ + + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorNext( + OrthancPluginContext* context, + uint8_t* done, + OrthancPluginKeysValuesIterator* iterator) + { + _OrthancPluginKeysValuesIteratorNext params; + params.done = done; + params.iterator = iterator; + + return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorNext, ¶ms); + } + + + typedef struct + { + const char** target; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorGetString; + + + /* TODO_ATTACH_CUSTOM_DATA TODO DOCUMENT */ + + ORTHANC_PLUGIN_INLINE const char* OrthancPluginKeysValuesIteratorGetKey( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + const char* target = NULL; + + _OrthancPluginKeysValuesIteratorGetString params; + params.target = ⌖ + params.iterator = iterator; + + if (context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetKey, ¶ms) == OrthancPluginErrorCode_Success) + { + return target; + } + else + { + return NULL; + } + } + + + /* TODO_ATTACH_CUSTOM_DATA TODO DOCUMENT */ + + ORTHANC_PLUGIN_INLINE const char* OrthancPluginKeysValuesIteratorGetValue( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + const char* target = NULL; + + _OrthancPluginKeysValuesIteratorGetString params; + params.target = ⌖ + params.iterator = iterator; + + if (context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetValue, ¶ms) == OrthancPluginErrorCode_Success) + { + return target; + } + else + { + return NULL; + } + } + + + + typedef struct + { + const char* queueId; + const void* value; + uint32_t valueSize; + } _OrthancPluginEnqueueValue; + + /** + * @brief Append a value to the back of a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue + * @param value The value to store + * @param valueSize The size of the value to store + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginEnqueueValue( + OrthancPluginContext* context, + const char* queueId, /* in */ + const void* value, /* in */ + uint32_t valueSize /* in */) + { + _OrthancPluginEnqueueValue params; + params.queueId = queueId; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_EnqueueValue, ¶ms); + } + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* queueId; + OrthancPluginQueueOrigin origin; + } _OrthancPluginDequeueValue; + + /** + * @brief Dequeue a value from a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. a value has been dequeued + * @param target Memory buffer where to store the value that has been retrieved from the queue + * @param queueId A unique identifier identifying both the plugin and the queue + * @param origin The queue position where the value is removed (back for LIFO, front for FIFO) + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDequeueValue( + OrthancPluginContext* context, + uint8_t* found, /* out */ + OrthancPluginMemoryBuffer* target, /* out */ + const char* queueId, /* in */ + OrthancPluginQueueOrigin origin /* in */) + { + _OrthancPluginDequeueValue params; + params.found = found; + params.target = target; + params.queueId = queueId; + params.origin = origin; + + return context->InvokeService(context, _OrthancPluginService_DequeueValue, ¶ms); + } + + typedef struct + { + const char* queueId; + uint64_t* size; + } _OrthancPluginGetQueueSize; + + /** + * @brief Get the number of elements in a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param size The number of elements in the queue. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetQueueSize( + OrthancPluginContext* context, + const char* queueId, /* in */ + uint64_t* size /* out */) + { + _OrthancPluginGetQueueSize params; + params.queueId = queueId; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_GetQueueSize, ¶ms); + } #ifdef __cplusplus }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Fri May 30 12:29:41 2025 +0200 @@ -55,6 +55,7 @@ int32 compression_type = 5; // opaque "CompressionType" in Orthanc uint64 compressed_size = 6; string compressed_hash = 7; + string custom_data = 8; // New in v 1.12.8 } enum ResourceType { @@ -94,6 +95,11 @@ ORDERING_CAST_FLOAT = 2; } +enum QueueOrigin { + QUEUE_ORIGIN_FRONT = 0; + QUEUE_ORIGIN_BACK = 1; +} + message ServerIndexChange { int64 seq = 1; int32 change_type = 2; // opaque "ChangeType" in Orthanc @@ -166,6 +172,9 @@ bool has_measure_latency = 7; bool supports_find = 8; // New in Orthanc 1.12.5 bool has_extended_changes = 9; // New in Orthanc 1.12.5 + bool supports_key_value_stores = 10; // New in Orthanc 1.12.8 + bool supports_queues = 11; // New in Orthanc 1.12.8 + bool has_attachment_custom_data = 12; // New in Orthanc 1.12.8 } } @@ -321,6 +330,16 @@ OPERATION_FIND = 50; // New in Orthanc 1.12.5 OPERATION_GET_CHANGES_EXTENDED = 51; // New in Orthanc 1.12.5 OPERATION_COUNT_RESOURCES = 52; // New in Orthanc 1.12.5 + OPERATION_STORE_KEY_VALUE = 53; // New in Orthanc 1.12.8 + OPERATION_DELETE_KEY_VALUE = 54; // New in Orthanc 1.12.8 + OPERATION_GET_KEY_VALUE = 55; // New in Orthanc 1.12.8 + OPERATION_LIST_KEY_VALUES = 56; // New in Orthanc 1.12.8 + OPERATION_ENQUEUE_VALUE = 57; // New in Orthanc 1.12.8 + OPERATION_DEQUEUE_VALUE = 58; // New in Orthanc 1.12.8 + OPERATION_GET_QUEUE_SIZE = 59; // New in Orthanc 1.12.8 + OPERATION_GET_ATTACHMENT = 60; // New in Orthanc 1.12.8 + OPERATION_UPDATE_ATTACHMENT_CUSTOM_DATA = 61; // New in Orthanc 1.12.8 + } message Rollback { @@ -974,6 +993,110 @@ } } +message StoreKeyValue { + message Request { + string store_id = 1; + string key = 2; + string value = 3; + } + + message Response { + } +} + +message DeleteKeyValue { + message Request { + string store_id = 1; + string key = 2; + } + + message Response { + } +} + +message GetKeyValue { + message Request { + string store_id = 1; + string key = 2; + } + + message Response { + bool found = 1; + string value = 2; + } +} + +message ListKeysValues { + message Request { + string store_id = 1; + bool from_first = 2; + string from_key = 3; // Only meaningful if "from_first == false" + uint64 limit = 4; + } + + message Response { + message KeyValue { + string key = 1; + string value = 2; + } + repeated KeyValue keys_values = 1; + } +} + +message EnqueueValue { + message Request { + string queue_id = 1; + string value = 2; + } + + message Response { + } +} + +message DequeueValue { + message Request { + string queue_id = 1; + QueueOrigin origin = 2; + } + + message Response { + bool found = 1; + string value = 2; + } +} + +message GetQueueSize { + message Request { + string queue_id = 1; + } + + message Response { + uint64 size = 1; + } +} + +message GetAttachment { + message Request { + string uuid = 1; + } + + message Response { + bool found = 1; + FileInfo attachment = 2; + int64 revision = 3; + } +} + +message UpdateAttachmentCustomData { + message Request { + string uuid = 1; + string custom_data = 2; + } + + message Response { + } +} + message TransactionRequest { sfixed64 transaction = 1; TransactionOperation operation = 2; @@ -1031,6 +1154,15 @@ Find.Request find = 150; GetChangesExtended.Request get_changes_extended = 151; Find.Request count_resources = 152; + StoreKeyValue.Request store_key_value = 153; + DeleteKeyValue.Request delete_key_value = 154; + GetKeyValue.Request get_key_value = 155; + ListKeysValues.Request list_keys_values = 156; + EnqueueValue.Request enqueue_value = 157; + DequeueValue.Request dequeue_value = 158; + GetQueueSize.Request get_queue_size = 159; + GetAttachment.Request get_attachment = 160; + UpdateAttachmentCustomData.Request update_attachment_custom_data = 161; } message TransactionResponse { @@ -1087,6 +1219,15 @@ repeated Find.Response find = 150; // One message per found resource GetChangesExtended.Response get_changes_extended = 151; CountResources.Response count_resources = 152; + StoreKeyValue.Response store_key_value = 153; + DeleteKeyValue.Response delete_key_value = 154; + GetKeyValue.Response get_key_value = 155; + ListKeysValues.Response list_keys_values = 156; + EnqueueValue.Response enqueue_value = 157; + DequeueValue.Response dequeue_value = 158; + GetQueueSize.Response get_queue_size = 159; + GetAttachment.Response get_attachment = 160; + UpdateAttachmentCustomData.Response update_attachment_custom_data = 161; } enum RequestType {
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Fri May 30 12:29:41 2025 +0200 @@ -4347,4 +4347,195 @@ } } #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator *iterator) : + iterator_(iterator) + { + if (iterator_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::~Iterator() + { + OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::Iterator::Next() + { + uint8_t done; + OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorNext(OrthancPlugins::GetGlobalContext(), &done, iterator_); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return (done != 0); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + std::string KeyValueStore::Iterator::GetKey() const + { + const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + else + { + return s; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + std::string KeyValueStore::Iterator::GetValue() const + { + const char* s = OrthancPluginKeysValuesIteratorGetValue(OrthancPlugins::GetGlobalContext(), iterator_); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + else + { + return s; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::Store(const std::string& key, + const std::string& value) + { + OrthancPluginErrorCode code = OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(), storeId_.c_str(), + key.c_str(), value.c_str(), value.size()); + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::GetValue(std::string& value, + const std::string& key) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + OrthancPluginErrorCode code = OrthancPluginGetKeyValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::DeleteKey(const std::string& key) + { + OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(), + storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator* KeyValueStore::CreateIterator() + { + return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str())); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + void Queue::PushBack(const std::string& value) + { + OrthancPluginErrorCode code = OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(), + queueId_.c_str(), value.c_str(), value.size()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + bool Queue::PopInternal(std::string& value, + OrthancPluginQueueOrigin origin) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + + OrthancPluginErrorCode code = OrthancPluginDequeueValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, queueId_.c_str(), origin); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + uint64_t Queue::GetSize() + { + uint64_t size = 0; + OrthancPluginErrorCode code = OrthancPluginGetQueueSize(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), &size); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return size; + } + } +#endif }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Fri May 30 12:29:41 2025 +0200 @@ -134,6 +134,14 @@ # define HAS_ORTHANC_PLUGIN_LOG_MESSAGE 0 #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 99) +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 1 +# define HAS_ORTHANC_PLUGIN_QUEUES 1 +#else +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 0 +# define HAS_ORTHANC_PLUGIN_QUEUES 0 +#endif + // Macro to tag a function as having been deprecated #if (__cplusplus >= 201402L) // C++14 @@ -1618,4 +1626,78 @@ bool GetAnswerJson(Json::Value& output) const; }; #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + class KeyValueStore : public boost::noncopyable + { + public: + class Iterator : public boost::noncopyable + { + private: + OrthancPluginKeysValuesIterator *iterator_; + + public: + Iterator(OrthancPluginKeysValuesIterator *iterator); + + ~Iterator(); + + bool Next(); + + std::string GetKey() const; + + std::string GetValue() const; + }; + + private: + std::string storeId_; + + public: + explicit KeyValueStore(const std::string& storeId) : + storeId_(storeId) + { + } + + void Store(const std::string& key, + const std::string& value); + + bool GetValue(std::string& value, + const std::string& key); + + void DeleteKey(const std::string& key); + + Iterator* CreateIterator(); + }; +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + class Queue : public boost::noncopyable + { + private: + std::string queueId_; + + bool PopInternal(std::string& value, OrthancPluginQueueOrigin origin); + + public: + explicit Queue(const std::string& queueId) : + queueId_(queueId) + { + } + + void PushBack(const std::string& value); + + bool PopBack(std::string& value) + { + return PopInternal(value, OrthancPluginQueueOrigin_Back); + } + + bool PopFront(std::string& value) + { + return PopInternal(value, OrthancPluginQueueOrigin_Front); + } + + uint64_t GetSize(); + }; +#endif }
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Fri May 30 12:29:41 2025 +0200 @@ -113,7 +113,7 @@ { try { - std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->Read(uuid, Convert(type))); + std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->ReadWhole(uuid, Convert(type))); // copy from a buffer allocated on plugin's heap into a buffer allocated on core's heap if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, buffer->GetSize()) != OrthancPluginErrorCode_Success)
--- a/OrthancServer/Resources/RunCppCheck.sh Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Fri May 30 12:29:41 2025 +0200 @@ -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
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Fri May 30 12:29:41 2025 +0200 @@ -56,6 +56,9 @@ bool hasMeasureLatency_; bool hasFindSupport_; bool hasExtendedChanges_; + bool hasAttachmentCustomDataSupport_; + bool hasKeyValueStoresSupport_; + bool hasQueuesSupport_; public: Capabilities() : @@ -66,7 +69,10 @@ hasUpdateAndGetStatistics_(false), hasMeasureLatency_(false), hasFindSupport_(false), - hasExtendedChanges_(false) + hasExtendedChanges_(false), + hasAttachmentCustomDataSupport_(false), + hasKeyValueStoresSupport_(false), + hasQueuesSupport_(false) { } @@ -100,6 +106,16 @@ return hasLabelsSupport_; } + void SetAttachmentCustomDataSupport(bool value) + { + hasAttachmentCustomDataSupport_ = value; + } + + bool HasAttachmentCustomDataSupport() const + { + return hasAttachmentCustomDataSupport_; + } + void SetHasExtendedChanges(bool value) { hasExtendedChanges_ = value; @@ -149,6 +165,26 @@ { return hasFindSupport_; } + + void SetKeyValueStoresSupport(bool value) + { + hasKeyValueStoresSupport_ = value; + } + + bool HasKeyValueStoresSupport() const + { + return hasKeyValueStoresSupport_; + } + + void SetQueuesSupport(bool value) + { + hasQueuesSupport_ = value; + } + + bool HasQueuesSupport() const + { + return hasQueuesSupport_; + } }; @@ -250,6 +286,13 @@ int64_t id, FileContentType contentType) = 0; + virtual bool GetAttachment(FileInfo& attachment, + int64_t& revision, + const std::string& attachmentUuid) = 0; + + virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid, + const std::string& customData) = 0; + /** * If "shared" is "true", the property is shared by all the * Orthanc servers that access the same database. If "shared" is @@ -390,6 +433,40 @@ int64_t to, uint32_t limit, const std::set<ChangeType>& filterType) = 0; + + // New in Orthanc 1.12.8 + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) = 0; + + // New in Orthanc 1.12.8 + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) = 0; + + // New in Orthanc 1.12.8 + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) = 0; + + // New in Orthanc 1.12.8 + virtual void ListKeysValues(std::list<std::string>& keys /* out */, + std::list<std::string>& values /* out */, + const std::string& storeId, + bool first, + const std::string& from /* only used if "first == false" */, + uint64_t limit /* maximum number of elements */) = 0; + + // New in Orthanc 1.12.8 + virtual void EnqueueValue(const std::string& queueId, + const std::string& value) = 0; + + // New in Orthanc 1.12.8 + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) = 0; + + // New in Orthanc 1.12.8, for statistics only + virtual uint64_t GetQueueSize(const std::string& queueId) = 0; }; @@ -456,7 +533,7 @@ virtual unsigned int GetDatabaseVersion() = 0; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) = 0; + IPluginStorageArea& storageArea) = 0; virtual const Capabilities GetDatabaseCapabilities() const = 0;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallKeyValueStoresAndQueues.sql Fri May 30 12:29:41 2025 +0200 @@ -0,0 +1,35 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2023 Osimis S.A., Belgium +-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +-- Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +-- +-- This program is free software: you can redistribute it and/or +-- modify it under the terms of the GNU General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. + + +CREATE TABLE KeyValueStores( + storeId TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY(storeId, key) -- Prevents duplicates + ); + +CREATE TABLE Queues ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + queueId TEXT NOT NULL, + value TEXT +); + +CREATE INDEX QueuesIndex ON Queues (queueId, id);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql Fri May 30 12:29:41 2025 +0200 @@ -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
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Fri May 30 12:29:41 2025 +0200 @@ -55,6 +55,7 @@ id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, type INTEGER, value TEXT, + revision INTEGER, -- New in Orthanc 1.12.8 (added in InstallRevisionAndCustomData.sql) PRIMARY KEY(id, type) ); @@ -67,6 +68,8 @@ compressionType INTEGER, uncompressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) compressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) + revision INTEGER, -- New in Orthanc 1.12.8 (added in InstallRevisionAndCustomData.sql) + customData TEXT, -- New in Orthanc 1.12.8 (added in InstallRevisionAndCustomData.sql) PRIMARY KEY(id, fileType) ); @@ -95,22 +98,12 @@ patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE ); --- New in Orthanc 1.12.0 -CREATE TABLE Labels( - id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, - label TEXT NOT NULL, - PRIMARY KEY(id, label) -- Prevents duplicates - ); - CREATE INDEX ChildrenIndex ON Resources(parentId); CREATE INDEX PublicIndex ON Resources(publicId); CREATE INDEX ResourceTypeIndex ON Resources(resourceType); CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); --- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up --- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); --- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); -- The 3 following indexes were added in Orthanc 0.8.5 (database v5) CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); @@ -119,18 +112,6 @@ CREATE INDEX ChangesIndex ON Changes(internalId); --- New in Orthanc 1.12.0 -CREATE INDEX LabelsIndex1 ON Labels(id); -CREATE INDEX LabelsIndex2 ON Labels(label); -- This index allows efficient lookups - -CREATE TRIGGER AttachedFileDeleted -AFTER DELETE ON AttachedFiles -BEGIN - SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, - old.compressionType, old.compressedSize, - -- These 2 arguments are new in Orthanc 0.7.3 (database v4) - old.uncompressedMD5, old.compressedMD5); -END; CREATE TRIGGER ResourceDeleted AFTER DELETE ON Resources @@ -156,6 +137,90 @@ END; +-- new in Orthanc 1.5.1 -------------------------- equivalent to InstallTrackAttachmentsSize.sql + +CREATE TABLE GlobalIntegers( + key INTEGER PRIMARY KEY, + value INTEGER); + +INSERT INTO GlobalProperties VALUES (6, 1); -- GlobalProperty_GetTotalSizeIsFast + +INSERT INTO GlobalIntegers SELECT 0, IFNULL(SUM(compressedSize), 0) FROM AttachedFiles; +INSERT INTO GlobalIntegers SELECT 1, IFNULL(SUM(uncompressedSize), 0) FROM AttachedFiles; + +CREATE TRIGGER AttachedFileIncrementSize +AFTER INSERT ON AttachedFiles +BEGIN + UPDATE GlobalIntegers SET value = value + new.compressedSize WHERE key = 0; + UPDATE GlobalIntegers SET value = value + new.uncompressedSize WHERE key = 1; +END; + +CREATE TRIGGER AttachedFileDecrementSize +AFTER DELETE ON AttachedFiles +BEGIN + UPDATE GlobalIntegers SET value = value - old.compressedSize WHERE key = 0; + UPDATE GlobalIntegers SET value = value - old.uncompressedSize WHERE key = 1; +END; + +-------------------------------------------------- + + +-- new in Orthanc 1.12.0 ------------------------- equivalent to InstallLabelsTable.sql + +CREATE TABLE Labels( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + label TEXT NOT NULL, + PRIMARY KEY(id, label) -- Prevents duplicates + ); + +CREATE INDEX LabelsIndex1 ON Labels(id); +CREATE INDEX LabelsIndex2 ON Labels(label); -- This index allows efficient lookups + +-------------------------------------------------- + + +-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallRevisionAndCustomData.sql + +CREATE TABLE DeletedFiles( + uuid TEXT NOT NULL, -- 0 + customData TEXT -- 1 +); + +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 + +--------------------------------------------------- + + +-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallKeyValueStoresAndQueues.sql +CREATE TABLE KeyValueStores( + storeId TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY(storeId, key) -- Prevents duplicates + ); + +CREATE TABLE Queues ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + queueId TEXT NOT NULL, + value TEXT +); + +CREATE INDEX QueuesIndex ON Queues (queueId, id); + +--------------------------------------------------- + -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration INSERT INTO GlobalProperties VALUES (1, "6");
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Fri May 30 12:29:41 2025 +0200 @@ -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) @@ -384,12 +386,12 @@ } } - boost::mutex::scoped_lock lock_; + boost::recursive_mutex::scoped_lock lock_; IDatabaseListener& listener_; SignalRemainingAncestor& signalRemainingAncestor_; public: - TransactionBase(boost::mutex& mutex, + TransactionBase(boost::recursive_mutex& mutex, SQLite::Connection& db, IDatabaseListener& listener, SignalRemainingAncestor& signalRemainingAncestor) : @@ -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,12 +1777,50 @@ 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; } } + virtual bool GetAttachment(FileInfo& attachment, + int64_t& revision, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT uuid, uncompressedSize, compressionType, compressedSize, " + "uncompressedMD5, compressedMD5, revision, customData, fileType FROM AttachedFiles WHERE uuid=?"); + s.BindString(0, attachmentUuid); + + if (!s.Step()) + { + return false; + } + else + { + attachment = FileInfo(s.ColumnString(0), + static_cast<FileContentType>(s.ColumnInt(8)), + s.ColumnInt64(1), + s.ColumnString(4), + static_cast<CompressionType>(s.ColumnInt(2)), + s.ColumnInt64(3), + s.ColumnString(5), + s.ColumnString(7)); + revision = s.ColumnInt(6); + return true; + } + } + + virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid, + const std::string& customData) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "UPDATE AttachedFiles SET customData=? WHERE uuid=?"); + s.BindString(0, customData); + s.BindString(1, attachmentUuid); + s.Run(); + } virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, @@ -1739,7 +1851,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 +1862,7 @@ else { target = s.ColumnString(0); - revision = 0; // TODO - REVISIONS + revision = s.ColumnInt(1); return true; } } @@ -1922,11 +2034,11 @@ const std::string& value, int64_t revision) ORTHANC_OVERRIDE { - // TODO - REVISIONS - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata (id, type, value) VALUES(?, ?, ?)"); + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata (id, type, value, revision) VALUES(?, ?, ?, ?)"); s.BindInt64(0, id); s.BindInt(1, type); s.BindString(2, value); + s.BindInt(3, revision); s.Run(); } @@ -2027,6 +2139,147 @@ target.insert(s.ColumnString(0)); } } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO KeyValueStores (storeId, key, value) VALUES(?, ?, ?)"); + s.BindString(0, storeId); + s.BindString(1, key); + s.BindString(2, value); + s.Run(); + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM KeyValueStores WHERE storeId = ? AND key = ?"); + s.BindString(0, storeId); + s.BindString(1, key); + s.Run(); + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM KeyValueStores WHERE storeId=? AND key=?"); + s.BindString(0, storeId); + s.BindString(1, key); + + if (!s.Step()) + { + // No value found + return false; + } + else + { + value = s.ColumnString(0); + return true; + } + } + + // New in Orthanc 1.12.8 + virtual void ListKeysValues(std::list<std::string>& keys /* out */, + std::list<std::string>& values /* out */, + const std::string& storeId, + bool first, + const std::string& from /* only used if "first == false" */, + uint64_t limit) ORTHANC_OVERRIDE + { + int64_t actualLimit = limit; + if (limit == 0) + { + actualLimit = -1; // In SQLite, "if negative, there is no upper bound on the number of rows returned" + } + + std::unique_ptr<SQLite::Statement> statement; + + if (first) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? ORDER BY key ASC LIMIT ?")); + statement->BindString(0, storeId); + statement->BindInt64(1, actualLimit); + } + else + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? AND key>? ORDER BY key ASC LIMIT ?")); + statement->BindString(0, storeId); + statement->BindString(1, from); + statement->BindInt64(2, actualLimit); + } + + while (statement->Step()) + { + keys.push_back(statement->ColumnString(0)); + values.push_back(statement->ColumnString(1)); + } + } + + + // New in Orthanc 1.12.8 + virtual void EnqueueValue(const std::string& queueId, + const std::string& value) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO Queues (queueId, value) VALUES (?, ?)"); + s.BindString(0, queueId); + s.BindString(1, value); + s.Run(); + } + + // New in Orthanc 1.12.8 + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + int64_t rowId; + std::unique_ptr<SQLite::Statement> s; + + switch (origin) + { + case QueueOrigin_Front: + s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id ASC LIMIT 1")); + break; + + case QueueOrigin_Back: + s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id DESC LIMIT 1")); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + s->BindString(0, queueId); + if (!s->Step()) + { + // No value found + return false; + } + else + { + rowId = s->ColumnInt64(0); + value = s->ColumnString(1); + + SQLite::Statement s2(db_, SQLITE_FROM_HERE, + "DELETE FROM Queues WHERE id = ?"); + s2.BindInt64(0, rowId); + s2.Run(); + + return true; + } + } + + // New in Orthanc 1.12.8 + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT COUNT(*) FROM Queues WHERE queueId=?"); + s.BindString(0, queueId); + s.Step(); + return s.ColumnInt64(0); + } }; @@ -2055,6 +2308,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 +2331,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); } } }; @@ -2120,20 +2380,39 @@ SQLiteDatabaseWrapper& that_; std::unique_ptr<SQLite::Transaction> transaction_; int64_t initialDiskSize_; + bool isNested_; + + // Rationale for the isNested_ field: + // This was added while implementing the DelayedDeletion part of the advanced-storage plugin. + // When Orthanc deletes an attachment, a SQLite transaction is created to delete the attachment from + // the SQLite DB and, while the transaction is still active, the StorageRemove callback is called. + // The DelayedDeleter does not delete the file directly but, instead, it queues it for deletion. + // Queuing is done through the Orthanc SDK that creates a RW transaction (because it is a generic function). + // Since there is already an active RW transaction, this "nested" transaction does not need to perform anything + // in its Begin/Commit since this will be performed at higher level by the current activeTransaction_. + // However, in case of Rollback, this nested transaction must call the top level transaction Rollback. public: ReadWriteTransaction(SQLiteDatabaseWrapper& that, IDatabaseListener& listener) : TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), that_(that), - transaction_(new SQLite::Transaction(that_.db_)) + transaction_(new SQLite::Transaction(that_.db_)), + isNested_(false) { if (that_.activeTransaction_ != NULL) { - throw OrthancException(ErrorCode_InternalError); + if (dynamic_cast<SQLiteDatabaseWrapper::ReadWriteTransaction*>(that_.activeTransaction_) == NULL) + { + throw OrthancException(ErrorCode_InternalError, "Unable to create a nested RW transaction, the current transaction is not a RW transaction"); + } + + isNested_ = true; } - - that_.activeTransaction_ = this; + else + { + that_.activeTransaction_ = this; + } #if defined(NDEBUG) // Release mode @@ -2146,26 +2425,42 @@ virtual ~ReadWriteTransaction() { - assert(that_.activeTransaction_ != NULL); - that_.activeTransaction_ = NULL; + if (!isNested_) + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } } - void Begin() + virtual void Begin() { - transaction_->Begin(); + if (!isNested_) + { + transaction_->Begin(); + } } virtual void Rollback() ORTHANC_OVERRIDE { - transaction_->Rollback(); + if (isNested_) + { + that_.activeTransaction_->Rollback(); + } + else + { + transaction_->Rollback(); + } } virtual void Commit(int64_t fileSizeDelta /* only used in debug */) ORTHANC_OVERRIDE { - transaction_->Commit(); - - assert(initialDiskSize_ + fileSizeDelta >= 0 && - initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(GetTotalCompressedSize())); + if (!isNested_) + { + transaction_->Commit(); + + assert(initialDiskSize_ + fileSizeDelta >= 0 && + initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(GetTotalCompressedSize())); + } } }; @@ -2174,25 +2469,33 @@ { private: SQLiteDatabaseWrapper& that_; + bool isNested_; // see explanation on the ReadWriteTransaction public: ReadOnlyTransaction(SQLiteDatabaseWrapper& that, IDatabaseListener& listener) : TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), - that_(that) + that_(that), + isNested_(false) { if (that_.activeTransaction_ != NULL) { - throw OrthancException(ErrorCode_InternalError); + isNested_ = true; + // throw OrthancException(ErrorCode_InternalError); } - - that_.activeTransaction_ = this; + else + { + that_.activeTransaction_ = this; + } } virtual ~ReadOnlyTransaction() { - assert(that_.activeTransaction_ != NULL); - that_.activeTransaction_ = NULL; + if (!isNested_) + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } } virtual void Rollback() ORTHANC_OVERRIDE @@ -2214,11 +2517,14 @@ signalRemainingAncestor_(NULL), version_(0) { - // TODO: implement revisions in SQLite + dbCapabilities_.SetRevisionsSupport(true); dbCapabilities_.SetFlushToDisk(true); dbCapabilities_.SetLabelsSupport(true); dbCapabilities_.SetHasExtendedChanges(true); dbCapabilities_.SetHasFindSupport(HasIntegratedFind()); + dbCapabilities_.SetKeyValueStoresSupport(true); + dbCapabilities_.SetQueuesSupport(true); + dbCapabilities_.SetAttachmentCustomDataSupport(true); db_.Open(path); } @@ -2228,11 +2534,14 @@ signalRemainingAncestor_(NULL), version_(0) { - // TODO: implement revisions in SQLite + dbCapabilities_.SetRevisionsSupport(true); dbCapabilities_.SetFlushToDisk(true); dbCapabilities_.SetLabelsSupport(true); dbCapabilities_.SetHasExtendedChanges(true); dbCapabilities_.SetHasFindSupport(HasIntegratedFind()); + dbCapabilities_.SetKeyValueStoresSupport(true); + dbCapabilities_.SetQueuesSupport(true); + dbCapabilities_.SetAttachmentCustomDataSupport(true); db_.OpenInMemory(); } @@ -2248,7 +2557,7 @@ void SQLiteDatabaseWrapper::Open() { { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); if (signalRemainingAncestor_ != NULL) { @@ -2330,6 +2639,25 @@ ServerResources::GetFileResource(query, ServerResources::INSTALL_LABELS_TABLE); db_.Execute(query); } + + // New in Orthanc 1.12.8 + 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); + } + + // New in Orthanc 1.12.8 + if (!db_.DoesTableExist("KeyValueStores")) + { + LOG(INFO) << "Installing the \"KeyValueStores\" and \"Queues\" tables"; + std::string query; + ServerResources::GetFileResource(query, ServerResources::INSTALL_KEY_VALUE_STORE_AND_QUEUES); + db_.Execute(query); + } } transaction->Commit(0); @@ -2339,7 +2667,7 @@ void SQLiteDatabaseWrapper::Close() { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); // close and delete the WAL when exiting properly -> the DB is stored in a single file (no more -wal and -shm files) db_.Execute("PRAGMA JOURNAL_MODE=DELETE;"); db_.Close(); @@ -2358,9 +2686,9 @@ void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); if (targetVersion != 6) { @@ -2402,6 +2730,14 @@ { std::unique_ptr<ITransaction> transaction(StartTransaction(TransactionType_ReadWrite, listener)); + + // ReconstructMaindDicomTags uses LookupAttachment that needs revision and customData. Since we don't want to maintain a legacy version + // of LookupAttachment, we modify the table now) + LOG(INFO) << "First Upgrading SQLite schema to support revision and customData in order to be able to reconstruct main dicom tags"; + std::string query; + ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA); + db_.Execute(query); + ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Patient); ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Study); ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Series); @@ -2413,12 +2749,29 @@ version_ = 6; } + } + // class RaiiTransactionLogger + // { + // TransactionType type_; + // public: + // RaiiTransactionLogger(TransactionType type) + // : type_(type) + // { + // LOG(INFO) << "IN " << (type_ == TransactionType_ReadOnly ? "RO" : "RW"); + // } + // ~RaiiTransactionLogger() + // { + // LOG(INFO) << "OUT " << (type_ == TransactionType_ReadOnly ? "RO" : "RW"); + // } + // }; IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction(TransactionType type, IDatabaseListener& listener) { + // RaiiTransactionLogger logger(type); + switch (type) { case TransactionType_ReadOnly: @@ -2440,7 +2793,7 @@ void SQLiteDatabaseWrapper::FlushToDisk() { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); db_.FlushToDisk(); }
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Fri May 30 12:29:41 2025 +0200 @@ -27,7 +27,7 @@ #include "../../../OrthancFramework/Sources/SQLite/Connection.h" -#include <boost/thread/mutex.hpp> +#include <boost/thread/recursive_mutex.hpp> namespace Orthanc { @@ -47,7 +47,7 @@ class ReadWriteTransaction; class LookupFormatter; - boost::mutex mutex_; + boost::recursive_mutex mutex_; SQLite::Connection db_; TransactionBase* activeTransaction_; SignalRemainingAncestor* signalRemainingAncestor_; @@ -88,7 +88,7 @@ } virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE {
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Fri May 30 12:29:41 2025 +0200 @@ -3200,6 +3200,18 @@ return db_.GetDatabaseCapabilities().HasFindSupport(); } + bool StatelessDatabaseOperations::HasKeyValueStoresSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasKeyValueStoresSupport(); + } + + bool StatelessDatabaseOperations::HasQueuesSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasQueuesSupport(); + } + void StatelessDatabaseOperations::ExecuteCount(uint64_t& count, const FindRequest& request) { @@ -3320,4 +3332,366 @@ } } } + + void StatelessDatabaseOperations::StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& storeId_; + const std::string& key_; + const std::string& value_; + + public: + Operations(const std::string& storeId, + const std::string& key, + const std::string& value) : + storeId_(storeId), + key_(key), + value_(value) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.StoreKeyValue(storeId_, key_, value_); + } + }; + + Operations operations(storeId, key, value); + Apply(operations); + } + + void StatelessDatabaseOperations::DeleteKeyValue(const std::string& storeId, + const std::string& key) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& storeId_; + const std::string& key_; + + public: + Operations(const std::string& storeId, + const std::string& key) : + storeId_(storeId), + key_(key) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.DeleteKeyValue(storeId_, key_); + } + }; + + Operations operations(storeId, key); + Apply(operations); + } + + bool StatelessDatabaseOperations::GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) + { + class Operations : public ReadOnlyOperationsT3<std::string&, const std::string&, const std::string& > + { + bool found_; + public: + Operations(): + found_(false) + {} + + bool HasFound() + { + return found_; + } + + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + found_ = transaction.GetKeyValue(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + Operations operations; + operations.Apply(*this, value, storeId, key); + + return operations.HasFound(); + } + + void StatelessDatabaseOperations::EnqueueValue(const std::string& queueId, + const std::string& value) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& queueId_; + const std::string& value_; + + public: + Operations(const std::string& queueId, + const std::string& value) : + queueId_(queueId), + value_(value) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.EnqueueValue(queueId_, value_); + } + }; + + Operations operations(queueId, value); + Apply(operations); + } + + bool StatelessDatabaseOperations::DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& queueId_; + std::string& value_; + QueueOrigin origin_; + bool found_; + + public: + Operations(std::string& value, + const std::string& queueId, + QueueOrigin origin) : + queueId_(queueId), + value_(value), + origin_(origin), + found_(false) + { + } + + bool HasFound() + { + return found_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + found_ = transaction.DequeueValue(value_, queueId_, origin_); + } + }; + + Operations operations(value, queueId, origin); + Apply(operations); + + return operations.HasFound(); + } + + uint64_t StatelessDatabaseOperations::GetQueueSize(const std::string& queueId) + { + class Operations : public ReadOnlyOperationsT2<uint64_t&, const std::string& > + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + tuple.get<0>() = transaction.GetQueueSize(tuple.get<1>()); + } + }; + + uint64_t size; + + Operations operations; + operations.Apply(*this, size, queueId); + + return size; + } + + + bool StatelessDatabaseOperations::GetAttachment(FileInfo& attachment, + int64_t& revision, + const std::string& attachmentUuid) + { + class Operations : public ReadOnlyOperationsT3<FileInfo&, int64_t&, const std::string& > + { + bool found_; + public: + Operations(): + found_(false) + {} + + bool HasFound() + { + return found_; + } + + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + found_ = transaction.GetAttachment(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + Operations operations; + operations.Apply(*this, attachment, revision, attachmentUuid); + + return operations.HasFound(); + } + + void StatelessDatabaseOperations::UpdateAttachmentCustomData(const std::string& attachmentUuid, + const std::string& customData) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& attachmentUuid_; + const std::string& customData_; + + public: + Operations(const std::string& attachmentUuid, + const std::string& customData) : + attachmentUuid_(attachmentUuid), + customData_(customData) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.UpdateAttachmentCustomData(attachmentUuid_, customData_); + } + }; + + Operations operations(attachmentUuid, customData); + Apply(operations); + } + + StatelessDatabaseOperations::KeysValuesIterator::KeysValuesIterator(StatelessDatabaseOperations& db, + const std::string& storeId) : + db_(db), + state_(State_Waiting), + storeId_(storeId), + limit_(100) + { + } + + bool StatelessDatabaseOperations::KeysValuesIterator::Next() + { + if (state_ == State_Done) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (state_ == State_Available) + { + assert(currentKey_ != keys_.end()); + assert(currentValue_ != values_.end()); + ++currentKey_; + ++currentValue_; + + if (currentKey_ != keys_.end() && + currentValue_ != values_.end()) + { + // A value is still available in the last keys-values block fetched from the database + return true; + } + else if (currentKey_ != keys_.end() || + currentValue_ != values_.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + class Operations : public ReadOnlyOperationsT6<std::list<std::string>&, std::list<std::string>&, const std::string&, bool, const std::string&, uint64_t> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ListKeysValues(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>()); + } + }; + + if (state_ == State_Waiting) + { + keys_.clear(); + values_.clear(); + + Operations operations; + operations.Apply(db_, keys_, values_, storeId_, true, "", limit_); + } + else + { + assert(state_ == State_Available); + if (keys_.empty()) + { + state_ = State_Done; + return false; + } + else + { + const std::string lastKey = keys_.back(); + keys_.clear(); + values_.clear(); + + Operations operations; + operations.Apply(db_, keys_, values_, storeId_, false, lastKey, limit_); + } + } + + if (keys_.size() != values_.size()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (limit_ != 0 && + keys_.size() > limit_) + { + // The database plugin has returned too many key-value pairs + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (keys_.empty() && + values_.empty()) + { + state_ = State_Done; + return false; + } + else if (!keys_.empty() && + !values_.empty()) + { + state_ = State_Available; + currentKey_ = keys_.begin(); + currentValue_ = values_.begin(); + return true; + } + else + { + throw OrthancException(ErrorCode_InternalError); // Should never happen + } + } + + const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetKey() const + { + if (state_ == State_Available) + { + return *currentKey_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetValue() const + { + if (state_ == State_Available) + { + return *currentValue_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Fri May 30 12:29:41 2025 +0200 @@ -226,6 +226,13 @@ return transaction_.LookupAttachment(attachment, revision, id, contentType); } + bool GetAttachment(FileInfo& attachment, + int64_t& revision, + const std::string& attachmentUuid) + { + return transaction_.GetAttachment(attachment, revision, attachmentUuid); + } + bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) @@ -293,6 +300,28 @@ { transaction_.ExecuteExpand(response, capabilities, request, identifier); } + + bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) + { + return transaction_.GetKeyValue(value, storeId, key); + } + + uint64_t GetQueueSize(const std::string& queueId) + { + return transaction_.GetQueueSize(queueId); + } + + void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) + { + return transaction_.ListKeysValues(keys, values, storeId, first, from, limit); + } }; @@ -428,6 +457,39 @@ { transaction_.RemoveLabel(id, label); } + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) + { + transaction_.StoreKeyValue(storeId, key, value); + } + + void DeleteKeyValue(const std::string& storeId, + const std::string& key) + { + transaction_.DeleteKeyValue(storeId, key); + } + + void EnqueueValue(const std::string& queueId, + const std::string& value) + { + transaction_.EnqueueValue(queueId, value); + } + + bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) + { + return transaction_.DequeueValue(value, queueId, origin); + } + + void UpdateAttachmentCustomData(const std::string& attachmentUuid, + const std::string& customData) + { + return transaction_.UpdateAttachmentCustomData(attachmentUuid, customData); + } + }; @@ -523,6 +585,13 @@ /* out */ uint64_t& countSeries, /* out */ uint64_t& countInstances); + bool GetAttachment(FileInfo& attachment, + int64_t& revision, + const std::string& attachmentUuid); + + void UpdateAttachmentCustomData(const std::string& attachmentUuid, + const std::string& customData); + bool LookupAttachment(FileInfo& attachment, int64_t& revision, ResourceType level, @@ -544,6 +613,10 @@ bool HasExtendedChanges(); bool HasFindSupport(); + + bool HasKeyValueStoresSupport(); + + bool HasQueuesSupport(); void GetExportedResources(Json::Value& target, int64_t since, @@ -724,5 +797,65 @@ void ExecuteCount(uint64_t& count, const FindRequest& request); + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value); + + void DeleteKeyValue(const std::string& storeId, + const std::string& key); + + bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key); + + void EnqueueValue(const std::string& queueId, + const std::string& value); + + bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin); + + uint64_t GetQueueSize(const std::string& queueId); + + class KeysValuesIterator : public boost::noncopyable + { + private: + enum State + { + State_Waiting, + State_Available, + State_Done + }; + + StatelessDatabaseOperations& db_; + State state_; + std::string storeId_; + uint64_t limit_; + std::list<std::string> keys_; + std::list<std::string> values_; + std::list<std::string>::const_iterator currentKey_; + std::list<std::string>::const_iterator currentValue_; + + public: + KeysValuesIterator(StatelessDatabaseOperations& db, + const std::string& storeId); + + void SetLimit(uint64_t limit) + { + limit_ = limit; + } + + uint64_t GetLimit() const + { + return limit_; + } + + bool Next(); + + const std::string& GetKey() const; + + const std::string& GetValue() const; + }; }; }
--- a/OrthancServer/Sources/OrthancInitialization.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Fri May 30 12:29:41 2025 +0200 @@ -56,6 +56,7 @@ #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/HttpClient.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/OrthancException.h" @@ -482,19 +483,6 @@ } } - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE - { - if (type != FileContentType_Dicom) - { - return storage_.Read(uuid, type); - } - else - { - throw OrthancException(ErrorCode_UnknownResource); - } - } - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, @@ -510,9 +498,9 @@ } } - virtual bool HasReadRange() const ORTHANC_OVERRIDE + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE { - return storage_.HasReadRange(); + return storage_.HasEfficientReadRange(); } virtual void Remove(const std::string& uuid, @@ -527,7 +515,7 @@ } - static IStorageArea* CreateFilesystemStorage() + static IPluginStorageArea* CreateFilesystemStorage() { static const char* const SYNC_STORAGE_AREA = "SyncStorageArea"; static const char* const STORE_DICOM = "StoreDicom"; @@ -547,12 +535,12 @@ if (lock.GetConfiguration().GetBooleanParameter(STORE_DICOM, true)) { - return new FilesystemStorage(storageDirectory.string(), fsyncOnWrite); + return new PluginStorageAreaAdapter(new FilesystemStorage(storageDirectory.string(), fsyncOnWrite)); } else { LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode"; - return new FilesystemStorageWithoutDicom(storageDirectory.string(), fsyncOnWrite); + return new PluginStorageAreaAdapter(new FilesystemStorageWithoutDicom(storageDirectory.string(), fsyncOnWrite)); } } @@ -563,7 +551,7 @@ } - IStorageArea* CreateStorageArea() + IPluginStorageArea* CreateStorageArea() { return CreateFilesystemStorage(); }
--- a/OrthancServer/Sources/OrthancInitialization.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.h Fri May 30 12:29:41 2025 +0200 @@ -35,7 +35,7 @@ IDatabaseWrapper* CreateDatabaseWrapper(); - IStorageArea* CreateStorageArea(); + IPluginStorageArea* CreateStorageArea(); void SetGlobalVerbosity(Verbosity verbosity);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Fri May 30 12:29:41 2025 +0200 @@ -2666,7 +2666,7 @@ } int64_t newRevision; - context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(), + context.AddAttachment(newRevision, publicId, level, StringToContentType(name), call.GetBodyData(), call.GetBodySize(), hasOldRevision, oldRevision, oldMD5); SetBufferContentETag(call.GetOutput(), newRevision, call.GetBodyData(), call.GetBodySize()); // New in Orthanc 1.9.2
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Fri May 30 12:29:41 2025 +0200 @@ -95,6 +95,8 @@ static const char* const HAS_LABELS = "HasLabels"; static const char* const CAPABILITIES = "Capabilities"; static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges"; + static const char* const HAS_KEY_VALUE_STORES = "HasKeyValueStores"; + static const char* const HAS_QUEUES = "HasQueues"; static const char* const HAS_EXTENDED_FIND = "HasExtendedFind"; static const char* const READ_ONLY = "ReadOnly"; @@ -211,6 +213,8 @@ result[CAPABILITIES] = Json::objectValue; result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges(); result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport(); + result[CAPABILITIES][HAS_KEY_VALUE_STORES] = OrthancRestApi::GetIndex(call).HasKeyValueStoresSupport(); + result[CAPABILITIES][HAS_QUEUES] = OrthancRestApi::GetIndex(call).HasQueuesSupport(); call.GetOutput().AnswerJson(result); }
--- a/OrthancServer/Sources/ServerContext.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Fri May 30 12:29:41 2025 +0200 @@ -356,7 +356,7 @@ ServerContext::ServerContext(IDatabaseWrapper& database, - IStorageArea& area, + IPluginStorageArea& area, bool unitTesting, size_t maxCompletedJobs, bool readOnly, @@ -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); } @@ -605,6 +606,37 @@ StoreInstanceMode mode, bool isReconstruct) { + FileInfo adoptedFileNotUsed; + + return StoreAfterTranscoding(resultPublicId, + dicom, + mode, + isReconstruct, + false, + adoptedFileNotUsed); + } + + ServerContext::StoreResult ServerContext::AdoptAttachment(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + const FileInfo& adoptedFile) + { + return StoreAfterTranscoding(resultPublicId, + dicom, + mode, + false, + true, + adoptedFile); + } + + + ServerContext::StoreResult ServerContext::StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + bool isReconstruct, + bool isAdoption, + const FileInfo& adoptedFile) + { bool overwrite; switch (mode) { @@ -707,19 +739,25 @@ // TODO Should we use "gzip" instead? CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); - FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), - FileContentType_Dicom, compression, storeMD5_); + ServerIndex::Attachments attachments; + FileInfo dicomInfo; - ServerIndex::Attachments attachments; - attachments.push_back(dicomInfo); + if (!isAdoption) + { + dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom, compression, storeMD5_, &dicom); + attachments.push_back(dicomInfo); + } + else + { + attachments.push_back(adoptedFile); + } FileInfo dicomUntilPixelData; if (hasPixelDataOffset && - (!area_.HasReadRange() || + (!area_.HasEfficientReadRange() || compressionEnabled_)) { - dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, - FileContentType_DicomUntilPixelData, compression, storeMD5_); + dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL); attachments.push_back(dicomUntilPixelData); } @@ -764,7 +802,10 @@ if (result.GetStatus() != StoreStatus_Success) { - accessor.Remove(dicomInfo); + if (!isAdoption) + { + accessor.Remove(dicomInfo); + } if (dicomUntilPixelData.IsValid()) { @@ -778,7 +819,14 @@ switch (result.GetStatus()) { case StoreStatus_Success: - LOG(INFO) << "New instance stored (" << resultPublicId << ")"; + if (isAdoption) + { + LOG(INFO) << "New instance adopted (" << resultPublicId << ")"; + } + else + { + LOG(INFO) << "New instance stored (" << resultPublicId << ")"; + } break; case StoreStatus_AlreadyStored: @@ -1018,8 +1066,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 { @@ -1201,7 +1248,7 @@ if (hasPixelDataOffset && - area_.HasReadRange() && + area_.HasEfficientReadRange() && LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments) && attachment.GetCompressionType() == CompressionType_None) { @@ -1279,13 +1326,13 @@ index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset, boost::lexical_cast<std::string>(pixelDataOffset)); - if (!area_.HasReadRange() || + if (!area_.HasEfficientReadRange() || compressionEnabled_) { int64_t newRevision; - AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData, + AddAttachment(newRevision, instancePublicId, ResourceType_Instance, FileContentType_DicomUntilPixelData, dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset, - false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); + false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); } } } @@ -1354,7 +1401,7 @@ return true; } - if (!area_.HasReadRange()) + if (!area_.HasEfficientReadRange()) { return false; } @@ -1513,6 +1560,7 @@ bool ServerContext::AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -1526,7 +1574,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 {
--- a/OrthancServer/Sources/ServerContext.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/ServerContext.h Fri May 30 12:29:41 2025 +0200 @@ -43,7 +43,7 @@ namespace Orthanc { class DicomInstanceToStore; - class IStorageArea; + class IPluginStorageArea; class JobsEngine; class MetricsRegistry; class OrthancPlugins; @@ -193,7 +193,7 @@ virtual void SignalJobFailure(const std::string& jobId) ORTHANC_OVERRIDE; ServerIndex index_; - IStorageArea& area_; + IPluginStorageArea& area_; StorageCache storageCache_; bool compressionEnabled_; @@ -269,9 +269,17 @@ StoreInstanceMode mode, bool isReconstruct); + StoreResult StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + bool isReconstruct, + bool isAdoption, + const FileInfo& adoptedFile); + // This method must only be called from "ServerIndex"! void RemoveFile(const std::string& fileUuid, - FileContentType type); + FileContentType type, + const std::string& customData); // This DicomModification object is intended to be used as a // "rules engine" when de-identifying logs for C-Find, C-Get, and @@ -305,7 +313,7 @@ }; ServerContext(IDatabaseWrapper& database, - IStorageArea& area, + IPluginStorageArea& area, bool unitTesting, size_t maxCompletedJobs, bool readOnly, @@ -344,6 +352,7 @@ bool AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -355,6 +364,11 @@ DicomInstanceToStore& dicom, StoreInstanceMode mode); + StoreResult AdoptAttachment(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + const FileInfo& adoptedFile); + StoreResult TranscodeAndStore(std::string& resultPublicId, DicomInstanceToStore* dicom, StoreInstanceMode mode,
--- a/OrthancServer/Sources/ServerEnumerations.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Fri May 30 12:29:41 2025 +0200 @@ -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.8 GlobalProperty_Modalities = 20, // New in Orthanc 1.5.0 GlobalProperty_Peers = 21, // New in Orthanc 1.5.0 @@ -258,6 +259,11 @@ Warnings_007_MissingRequestedTagsNotReadFromDisk // new in Orthanc 1.12.5 }; + enum QueueOrigin + { + QueueOrigin_Front, + QueueOrigin_Back + }; void InitializeServerEnumerations();
--- a/OrthancServer/Sources/ServerIndex.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Fri May 30 12:29:41 2025 +0200 @@ -45,12 +45,14 @@ struct FileToRemove { private: - std::string uuid_; - FileContentType type_; + std::string uuid_; + std::string customData_; + FileContentType type_; public: explicit FileToRemove(const FileInfo& info) : - uuid_(info.GetUuid()), + uuid_(info.GetUuid()), + customData_(info.GetCustomData()), type_(info.GetContentType()) { } @@ -60,6 +62,11 @@ return uuid_; } + const std::string& GetCustomData() const + { + return customData_; + } + FileContentType GetContentType() const { return type_; @@ -93,7 +100,7 @@ { try { - context_.RemoveFile(it->GetUuid(), it->GetContentType()); + context_.RemoveFile(it->GetUuid(), it->GetContentType(), it->GetCustomData()); } catch (OrthancException& e) {
--- a/OrthancServer/Sources/ServerToolbox.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Fri May 30 12:29:41 2025 +0200 @@ -96,7 +96,7 @@ void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, ResourceType level) { // WARNING: The database should be locked with a transaction!
--- a/OrthancServer/Sources/ServerToolbox.h Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/ServerToolbox.h Fri May 30 12:29:41 2025 +0200 @@ -32,7 +32,7 @@ namespace Orthanc { class ServerContext; - class IStorageArea; + class IPluginStorageArea; namespace ServerToolbox { @@ -42,7 +42,7 @@ ResourceType type); void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, ResourceType level); void LoadIdentifiers(const DicomTag*& tags,
--- a/OrthancServer/Sources/main.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/Sources/main.cpp Fri May 30 12:29:41 2025 +0200 @@ -30,6 +30,7 @@ #include "../../OrthancFramework/Sources/DicomNetworking/DicomServer.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h" #include "../../OrthancFramework/Sources/HttpServer/HttpServer.h" #include "../../OrthancFramework/Sources/Logging.h" @@ -1426,7 +1427,7 @@ static void UpgradeDatabase(IDatabaseWrapper& database, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { // Upgrade the schema of the database, if needed unsigned int currentVersion = database.GetDatabaseVersion(); @@ -1529,7 +1530,7 @@ static bool ConfigureServerContext(IDatabaseWrapper& database, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, OrthancPlugins *plugins, bool loadJobsFromDatabase) { @@ -1667,7 +1668,7 @@ static bool ConfigureDatabase(IDatabaseWrapper& database, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, OrthancPlugins *plugins, bool upgradeDatabase, bool loadJobsFromDatabase) @@ -1746,7 +1747,7 @@ bool loadJobsFromDatabase) { std::unique_ptr<IDatabaseWrapper> databasePtr; - std::unique_ptr<IStorageArea> storage; + std::unique_ptr<IPluginStorageArea> storage; #if ORTHANC_ENABLE_PLUGINS == 1 std::string databaseServerIdentifier; @@ -1997,7 +1998,7 @@ { SQLiteDatabaseWrapper inMemoryDatabase; inMemoryDatabase.Open(); - MemoryStorageArea inMemoryStorage; + PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */); OrthancRestApi restApi(context, false /* no Orthanc Explorer */); restApi.GenerateOpenApiDocumentation(openapi); @@ -2048,7 +2049,7 @@ { SQLiteDatabaseWrapper inMemoryDatabase; inMemoryDatabase.Open(); - MemoryStorageArea inMemoryStorage; + PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */); OrthancRestApi restApi(context, false /* no Orthanc Explorer */); restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html");
--- a/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Fri May 30 12:29:41 2025 +0200 @@ -26,9 +26,8 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" -#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/Logging.h" -#include "../../OrthancFramework/Sources/SerializationToolbox.h" #include "../Sources/Database/SQLiteDatabaseWrapper.h" #include "../Sources/ServerContext.h" @@ -39,7 +38,7 @@ { const std::string path = "UnitTestsStorage"; - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false, 1);
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Fri May 30 12:29:41 2025 +0200 @@ -27,6 +27,7 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/Images/Image.h" #include "../../OrthancFramework/Sources/Logging.h" @@ -196,6 +197,74 @@ transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */); } }; + + class DummyTransactionContextFactory : public StatelessDatabaseOperations::ITransactionContextFactory + { + public: + virtual StatelessDatabaseOperations::ITransactionContext* Create() + { + class DummyTransactionContext : public StatelessDatabaseOperations::ITransactionContext + { + public: + virtual void SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalResourceDeleted(ResourceType type, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void Commit() ORTHANC_OVERRIDE + { + } + + virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE + { + return 0; + } + + virtual bool IsUnstableResource(Orthanc::ResourceType type, + int64_t id) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */, + ResourceType& remainingLevel /* out */) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void MarkAsUnstable(Orthanc::ResourceType type, + int64_t id, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalAttachmentsAdded(uint64_t compressedSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + }; + + return new DummyTransactionContext; + } + }; } @@ -296,11 +365,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 +407,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 +425,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 +434,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 +470,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 +546,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 +607,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])); } @@ -618,7 +686,7 @@ const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); - FilesystemStorage storage(path); + PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -700,7 +768,7 @@ const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); - FilesystemStorage storage(path); + PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -782,7 +850,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); @@ -817,7 +885,7 @@ { bool overwrite = (i == 0); - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -982,7 +1050,7 @@ { const bool compression = (i == 0); - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -1058,3 +1126,131 @@ ASSERT_FALSE(ServerToolbox::IsValidLabel("&")); ASSERT_FALSE(ServerToolbox::IsValidLabel(".")); } + + +TEST(SQLiteDatabaseWrapper, KeyValueStores) +{ + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + + { + StatelessDatabaseOperations op(db, false); + op.SetTransactionContextFactory(new DummyTransactionContextFactory); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.StoreKeyValue("test", "hello", "world"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.StoreKeyValue("test", "hello2", "world2"); + op.StoreKeyValue("test", "hello3", "world3"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello2", it.GetKey()); + ASSERT_EQ("world2", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello3", it.GetKey()); + ASSERT_EQ("world3", it.GetValue()); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.DeleteKeyValue("test", "hello2"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello3", it.GetKey()); + ASSERT_EQ("world3", it.GetValue()); + ASSERT_FALSE(it.Next()); + } + + std::string s; + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("world", s); + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello3")); ASSERT_EQ("world3", s); + ASSERT_FALSE(op.GetKeyValue(s, "test", "hello2")); + + op.DeleteKeyValue("test", "nope"); + + op.DeleteKeyValue("test", "hello"); + op.DeleteKeyValue("test", "hello3"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_FALSE(it.Next()); + } + } + + db.Close(); +} + + +TEST(SQLiteDatabaseWrapper, Queues) +{ + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + + { + StatelessDatabaseOperations op(db, false); + op.SetTransactionContextFactory(new DummyTransactionContextFactory); + + ASSERT_EQ(0u, op.GetQueueSize("test")); + op.EnqueueValue("test", "hello"); + ASSERT_EQ(1u, op.GetQueueSize("test")); + op.EnqueueValue("test", "world"); + ASSERT_EQ(2u, op.GetQueueSize("test")); + + std::string s; + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back)); ASSERT_EQ("world", s); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back)); ASSERT_EQ("hello", s); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Back)); + + op.EnqueueValue("test", "hello"); + op.EnqueueValue("test", "world"); + + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); ASSERT_EQ("hello", s); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); ASSERT_EQ("world", s); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front)); + } + + db.Close(); +}
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Fri May 30 10:30:04 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Fri May 30 12:29:41 2025 +0200 @@ -26,6 +26,7 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/SerializationToolbox.h" @@ -528,12 +529,13 @@ class OrthancJobsSerialization : public testing::Test { private: - MemoryStorageArea storage_; - SQLiteDatabaseWrapper db_; // The SQLite DB is in memory - std::unique_ptr<ServerContext> context_; + PluginStorageAreaAdapter storage_; + SQLiteDatabaseWrapper db_; // The SQLite DB is in memory + std::unique_ptr<ServerContext> context_; public: - OrthancJobsSerialization() + OrthancJobsSerialization() : + storage_(new MemoryStorageArea) { db_.Open(); context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */));
--- a/TODO Fri May 30 10:30:04 2025 +0200 +++ b/TODO Fri May 30 12:29:41 2025 +0200 @@ -1,11 +1,3 @@ -current work on C-Get SCU: -- for the negotiation, limit SOPClassUID to the ones listed in a C-Find response or to a list provided in the Rest API ? -- SetupPresentationContexts -- handle progress -- handle cancellation when the job is cancelled ? - - - ======================= === Orthanc Roadmap === =======================