Mercurial > hg > orthanc
changeset 6174:9fce9208f24f attach-custom-data
integration mainline->attach-custom-data
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 11 Jun 2025 17:52:06 +0200 |
parents | 86a076ceaf3a (diff) 628edb487cec (current diff) |
children | 8bd3a683778d |
files | NEWS OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h |
diffstat | 64 files changed, 4864 insertions(+), 792 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Wed Jun 11 17:51:11 2025 +0200 +++ b/.hgignore Wed Jun 11 17:52:06 2025 +0200 @@ -15,3 +15,4 @@ .project Resources/Testing/Issue32/Java/bin Resources/Testing/Issue32/Java/target +build/
--- a/NEWS Wed Jun 11 17:51:11 2025 +0200 +++ b/NEWS Wed Jun 11 17:52:06 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,12 +22,13 @@ - 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. * Delayed Deletion plugin: - Added an index in the delayed-deletion SQLite external DB to speed up delayed deletions. This new index will only apply to new databases. If you wish to speed up an existing installation, run "CREATE INDEX PendingIndex ON Pending(uuid)" manually in the plugin SQLite DB. Patch provided by Yurii (George) from ivtech.dev. - With this patch, we observed a 100 fold performance improvement when the + With this patch, we observed a 100 fold performance improvement when the "Pending" table contains 1-2 millions files. * Configuration options "RejectSopClasses" and "RejectedSopClasses" are taken as synonyms. In Orthanc 1.12.6 and 1.12.7, "RejectSopClasses" was used instead of the expected @@ -25,7 +39,6 @@ REST handler was actually returning an HTTP error 415. The plugin will now receive an error 3000:OrthancPluginErrorCode_UnsupportedMediaType. - REST API -------- @@ -34,7 +47,6 @@ patient resource. - Version 1.12.7 (2025-04-07) ===========================
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -169,4 +169,56 @@ throw OrthancException(ErrorCode_BadSequenceOfCalls); } } + + void FileInfo::SetCustomData(const void* data, + size_t size) + { + if (valid_) + { + customData_.assign(reinterpret_cast<const char*>(data), size); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FileInfo::SetCustomData(const std::string& data) + { + if (valid_) + { + customData_ = data; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FileInfo::SwapCustomData(std::string& data) + { + if (valid_) + { + customData_.swap(data); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const std::string& FileInfo::GetCustomData() const + { + if (valid_) + { + return customData_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } }
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.h Wed Jun 11 17:52:06 2025 +0200 @@ -42,6 +42,7 @@ CompressionType compressionType_; uint64_t compressedSize_; std::string compressedMD5_; + std::string customData_; public: FileInfo(); @@ -80,5 +81,14 @@ const std::string& GetCompressedMD5() const; const std::string& GetUncompressedMD5() const; + + void SetCustomData(const void* data, + size_t size); + + void SetCustomData(const std::string& data); + + void SwapCustomData(std::string& data); + + const std::string& GetCustomData() const; }; }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:52:06 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 Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -275,7 +275,7 @@ }; - StorageAccessor::StorageAccessor(IStorageArea& area) : + StorageAccessor::StorageAccessor(IPluginStorageArea& area) : area_(area), cache_(NULL), metrics_(NULL) @@ -283,7 +283,7 @@ } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, StorageCache& cache) : area_(area), cache_(&cache), @@ -292,7 +292,7 @@ } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, MetricsRegistry& metrics) : area_(area), cache_(NULL), @@ -300,7 +300,7 @@ { } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, StorageCache& cache, MetricsRegistry& metrics) : area_(area), @@ -310,13 +310,15 @@ } - FileInfo StorageAccessor::Write(const void* data, - size_t size, - FileContentType type, - CompressionType compression, - bool storeMd5) + void StorageAccessor::Write(FileInfo& info, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const DicomInstanceToStore* instance) { - std::string uuid = Toolbox::GenerateUuid(); + const std::string uuid = Toolbox::GenerateUuid(); std::string md5; @@ -325,13 +327,15 @@ Toolbox::ComputeMD5(md5, data, size); } + std::string customData; + switch (compression) { case CompressionType_None: { { MetricsTimer timer(*this, METRICS_CREATE_DURATION); - area_.Create(uuid, data, size, type); + area_.Create(customData, uuid, data, size, type, compression, instance); } if (metrics_ != NULL) @@ -345,7 +349,9 @@ cacheAccessor.Add(uuid, type, data, size); } - return FileInfo(uuid, type, size, md5); + info = FileInfo(uuid, type, size, md5); + info.SetCustomData(customData); + return; } case CompressionType_ZlibWithSize: @@ -367,11 +373,11 @@ if (compressed.size() > 0) { - area_.Create(uuid, &compressed[0], compressed.size(), type); + area_.Create(customData, uuid, &compressed[0], compressed.size(), type, compression, instance); } else { - area_.Create(uuid, NULL, 0, type); + area_.Create(customData, uuid, NULL, 0, type, compression, instance); } } @@ -386,8 +392,10 @@ cacheAccessor.Add(uuid, type, data, size); // always add uncompressed data to cache } - return FileInfo(uuid, type, size, md5, + info = FileInfo(uuid, type, size, md5, CompressionType_ZlibWithSize, compressed.size(), compressedMD5); + info.SetCustomData(customData); + return; } default: @@ -395,16 +403,6 @@ } } - FileInfo StorageAccessor::Write(const std::string &data, - FileContentType type, - CompressionType compression, - bool storeMd5) - { - return Write((data.size() == 0 ? NULL : data.c_str()), - data.size(), type, compression, storeMd5); - } - - void StorageAccessor::Read(std::string& content, const FileInfo& info) { @@ -446,7 +444,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } if (metrics_ != NULL) @@ -467,7 +465,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - compressed.reset(area_.Read(info.GetUuid(), info.GetContentType())); + compressed.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } if (metrics_ != NULL) @@ -526,7 +524,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } if (metrics_ != NULL) @@ -539,7 +537,8 @@ void StorageAccessor::Remove(const std::string& fileUuid, - FileContentType type) + FileContentType type, + const std::string& customData) { if (cache_ != NULL) { @@ -548,14 +547,14 @@ { MetricsTimer timer(*this, METRICS_REMOVE_DURATION); - area_.Remove(fileUuid, type); + area_.Remove(fileUuid, type, customData); } } void StorageAccessor::Remove(const FileInfo &info) { - Remove(info.GetUuid(), info.GetContentType()); + Remove(info.GetUuid(), info.GetContentType(), info.GetCustomData()); } @@ -616,7 +615,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end)); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end, info.GetCustomData())); assert(buffer->GetSize() == end); } @@ -682,19 +681,19 @@ if (range.HasStart() && range.HasEnd()) { - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1)); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1, info.GetCustomData())); } else if (range.HasStart()) { - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize(), info.GetCustomData())); } else if (range.HasEnd()) { - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1)); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1, info.GetCustomData())); } else { - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } buffer->MoveToString(target); @@ -785,4 +784,5 @@ output.AnswerStream(transcoder); } #endif + }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Wed Jun 11 17:52:06 2025 +0200 @@ -110,7 +110,7 @@ private: class MetricsTimer; - IStorageArea& area_; + IPluginStorageArea& area_; StorageCache* cache_; MetricsRegistry* metrics_; @@ -121,28 +121,25 @@ #endif public: - explicit StorageAccessor(IStorageArea& area); + explicit StorageAccessor(IPluginStorageArea& area); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, StorageCache& cache); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, MetricsRegistry& metrics); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, StorageCache& cache, MetricsRegistry& metrics); - FileInfo Write(const void* data, - size_t size, - FileContentType type, - CompressionType compression, - bool storeMd5); - - FileInfo Write(const std::string& data, - FileContentType type, - CompressionType compression, - bool storeMd5); + void Write(FileInfo& info /* out */, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const DicomInstanceToStore* instance); void Read(std::string& content, const FileInfo& info); @@ -155,7 +152,8 @@ uint64_t end /* exclusive */); void Remove(const std::string& fileUuid, - FileContentType type); + FileContentType type, + const std::string& customData); void Remove(const FileInfo& info); @@ -185,6 +183,7 @@ const std::string& mime, const std::string& contentFilename); #endif + private: void ReadStartRangeInternal(std::string& target, const FileInfo& info,
--- a/OrthancFramework/Sources/SQLite/Statement.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/SQLite/Statement.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -236,10 +236,22 @@ BindString(col, UTF16ToUTF8(value)); }*/ - void Statement::BindBlob(int col, const void* val, int val_len) + void Statement::BindBlob(int col, const void* val, size_t val_len) { - CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, val_len, SQLITE_TRANSIENT), - ErrorCode_BadParameterType); + if (static_cast<size_t>(static_cast<int>(val_len)) != val_len) + { + throw OrthancSQLiteException(ErrorCode_SQLiteBindOutOfRange); + } + else + { + CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, static_cast<int>(val_len), SQLITE_TRANSIENT), + ErrorCode_BadParameterType); + } + } + + void Statement::BindBlob(int col, const std::string& value) + { + BindBlob(col, value.empty() ? NULL : value.c_str(), value.size()); }
--- a/OrthancFramework/Sources/SQLite/Statement.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/SQLite/Statement.h Wed Jun 11 17:52:06 2025 +0200 @@ -130,7 +130,8 @@ void BindCString(int col, const char* val); void BindString(int col, const std::string& val); //void BindString16(int col, const string16& value); - void BindBlob(int col, const void* value, int value_len); + void BindBlob(int col, const void* value, size_t value_len); + void BindBlob(int col, const std::string& value); // Retrieving ----------------------------------------------------------------
--- a/OrthancFramework/Sources/SystemToolbox.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -30,10 +30,9 @@ #include <gtest/gtest.h> #include "../Sources/FileStorage/FilesystemStorage.h" +#include "../Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../Sources/FileStorage/StorageAccessor.h" #include "../Sources/FileStorage/StorageCache.h" -#include "../Sources/HttpServer/BufferHttpSender.h" -#include "../Sources/HttpServer/FilesystemHttpSender.h" #include "../Sources/Logging.h" #include "../Sources/OrthancException.h" #include "../Sources/Toolbox.h" @@ -63,12 +62,18 @@ s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); std::string d; { - std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown)); + std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); buffer->MoveToString(d); } ASSERT_EQ(d.size(), data.size()); ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } } TEST(FilesystemStorage, Basic2) @@ -81,12 +86,18 @@ s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); std::string d; { - std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown)); + std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); buffer->MoveToString(d); } ASSERT_EQ(d.size(), data.size()); ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } } TEST(FilesystemStorage, FileWithSameNameAsTopDirectory) @@ -169,13 +180,14 @@ TEST(StorageAccessor, NoCompression) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true); - + const std::string data = "Hello world"; + FileInfo info; + accessor.Write(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, NULL); + std::string r; accessor.Read(r, info); @@ -191,13 +203,14 @@ TEST(StorageAccessor, Compression) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true); - + const std::string data = "Hello world"; + FileInfo info; + accessor.Write(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, NULL); + std::string r; accessor.Read(r, info); @@ -212,20 +225,22 @@ TEST(StorageAccessor, Mix) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string r; - std::string compressedData = "Hello"; - std::string uncompressedData = "HelloWorld"; + const std::string compressedData = "Hello"; + const std::string uncompressedData = "HelloWorld"; - FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom, CompressionType_ZlibWithSize, false); - FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom, CompressionType_None, false); - + FileInfo compressedInfo; + accessor.Write(compressedInfo, compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, NULL); + + std::string r; accessor.Read(r, compressedInfo); ASSERT_EQ(compressedData, r); + FileInfo uncompressedInfo; + accessor.Write(uncompressedInfo, uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, NULL); accessor.Read(r, uncompressedInfo); ASSERT_EQ(uncompressedData, r); ASSERT_NE(compressedData, r);
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/CMakeLists.txt Wed Jun 11 17:52:06 2025 +0200 @@ -243,15 +243,18 @@ ##################################################################### set(ORTHANC_EMBEDDED_FILES - CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json - DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt - FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json - LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua - PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql - UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql - INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql - INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua + PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql + INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql + INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + INSTALL_REVISION_AND_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql + INSTALL_DELETED_FILES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallDeletedFiles.sql + INSTALL_KEY_VALUE_STORES_AND_QUEUES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallKeyValueStoresAndQueues.sql ) if (STANDALONE_BUILD)
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -1448,6 +1448,70 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual 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 void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } }; @@ -1620,7 +1684,7 @@ void OrthancPluginDatabase::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { VoidDatabaseListener listener;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -677,7 +677,6 @@ } } - virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) ORTHANC_OVERRIDE @@ -1061,6 +1060,70 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual 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 void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } }; @@ -1231,7 +1294,7 @@ void OrthancPluginDatabaseV3::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { VoidDatabaseListener listener;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -41,7 +41,7 @@ #include "OrthancDatabasePlugin.pb.h" // Auto-generated file #include <cassert> - +#include <limits> namespace Orthanc { @@ -100,15 +100,17 @@ } - static FileInfo Convert(const DatabasePluginMessages::FileInfo& source) + static void Convert(FileInfo& info, + const DatabasePluginMessages::FileInfo& source) { - return FileInfo(source.uuid(), + info = FileInfo(source.uuid(), static_cast<FileContentType>(source.content_type()), source.uncompressed_size(), source.uncompressed_hash(), static_cast<CompressionType>(source.compression_type()), source.compressed_size(), source.compressed_hash()); + info.SetCustomData(source.custom_data()); } @@ -576,6 +578,7 @@ request.mutable_add_attachment()->mutable_attachment()->set_compression_type(attachment.GetCompressionType()); request.mutable_add_attachment()->mutable_attachment()->set_compressed_size(attachment.GetCompressedSize()); request.mutable_add_attachment()->mutable_attachment()->set_compressed_hash(attachment.GetCompressedMD5()); + request.mutable_add_attachment()->mutable_attachment()->set_custom_data(attachment.GetCustomData()); // New in 1.12.8 request.mutable_add_attachment()->set_revision(revision); ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request); @@ -604,7 +607,9 @@ DatabasePluginMessages::TransactionResponse response; ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DELETE_ATTACHMENT, request); - listener_.SignalAttachmentDeleted(Convert(response.delete_attachment().deleted_attachment())); + FileInfo info; + Convert(info, response.delete_attachment().deleted_attachment()); + listener_.SignalAttachmentDeleted(info); } @@ -629,7 +634,9 @@ for (int i = 0; i < response.delete_resource().deleted_attachments().size(); i++) { - listener_.SignalAttachmentDeleted(Convert(response.delete_resource().deleted_attachments(i))); + FileInfo info; + Convert(info, response.delete_resource().deleted_attachments(i)); + listener_.SignalAttachmentDeleted(info); } for (int i = 0; i < response.delete_resource().deleted_resources().size(); i++) @@ -1006,7 +1013,7 @@ if (response.lookup_attachment().found()) { - attachment = Convert(response.lookup_attachment().attachment()); + Convert(attachment, response.lookup_attachment().attachment()); revision = response.lookup_attachment().revision(); return true; } @@ -1016,7 +1023,58 @@ } } - + + 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(); + Convert(attachment, 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 void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_set_attachment_custom_data()->set_uuid(attachmentUuid); + request.mutable_set_attachment_custom_data()->set_custom_data(customData, customDataSize); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_SET_ATTACHMENT_CUSTOM_DATA, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) ORTHANC_OVERRIDE @@ -1686,7 +1744,9 @@ for (int j = 0; j < source.attachments().size(); j++) { - target->AddAttachment(Convert(source.attachments(j)), source.attachments_revisions(j)); + FileInfo info; + Convert(info, source.attachments(j)); + target->AddAttachment(info, source.attachments_revisions(j)); } Convert(*target, ResourceType_Patient, source.patient_content()); @@ -1748,7 +1808,8 @@ for (int j = 0; j < source.one_instance_attachments().size(); j++) { - FileInfo info(Convert(source.one_instance_attachments(j))); + FileInfo info; + Convert(info, source.one_instance_attachments(j)); if (attachments.find(info.GetContentType()) == attachments.end()) { attachments[info.GetContentType()] = info; @@ -1805,6 +1866,201 @@ find.ExecuteExpand(response, capabilities, request, identifier); } } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32" + // https://protobuf.dev/programming-guides/proto3/ + if (valueSize > std::numeric_limits<uint32_t>::max()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_store_key_value()->set_store_id(storeId); + request.mutable_store_key_value()->set_key(key); + request.mutable_store_key_value()->set_value(value, valueSize); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_STORE_KEY_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_delete_key_value()->set_store_id(storeId); + request.mutable_delete_key_value()->set_key(key); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_DELETE_KEY_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_key_value()->set_store_id(storeId); + request.mutable_get_key_value()->set_key(key); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_KEY_VALUE, request); + + if (response.get_key_value().found()) + { + value = response.get_key_value().value(); + return true; + } + else + { + return false; + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool fromFirst, + const std::string& fromKey, + uint64_t limit) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_list_keys_values()->set_store_id(storeId); + request.mutable_list_keys_values()->set_from_first(fromFirst); + request.mutable_list_keys_values()->set_from_key(fromKey); + request.mutable_list_keys_values()->set_limit(limit); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LIST_KEY_VALUES, request); + + for (int i = 0; i < response.list_keys_values().keys_values_size(); ++i) + { + keys.push_back(response.list_keys_values().keys_values(i).key()); + values.push_back(response.list_keys_values().keys_values(i).value()); + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32" + // https://protobuf.dev/programming-guides/proto3/ + if (valueSize > std::numeric_limits<uint32_t>::max()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_enqueue_value()->set_queue_id(queueId); + request.mutable_enqueue_value()->set_value(value, valueSize); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_ENQUEUE_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_dequeue_value()->set_queue_id(queueId); + + switch (origin) + { + case QueueOrigin_Back: + request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_BACK); + break; + + case QueueOrigin_Front: + request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_FRONT); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DEQUEUE_VALUE, request); + + if (response.dequeue_value().found()) + { + value = response.dequeue_value().value(); + return true; + } + else + { + return false; + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_queue_size()->set_queue_id(queueId); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_QUEUE_SIZE, request); + + return response.get_queue_size().size(); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } }; @@ -1895,6 +2151,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 +2220,7 @@ void OrthancPluginDatabaseV4::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { if (!open_) {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Jun 11 17:52:06 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); @@ -1724,7 +1963,7 @@ std::string dicom; currentQuery_->SaveToMemoryBuffer(dicom); - CopyToMemoryBuffer(target, dicom.c_str(), dicom.size()); + CopyToMemoryBuffer(target, dicom); } bool IsMatch(const void* dicom, @@ -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) @@ -3615,6 +3735,12 @@ break; } + case OrthancPluginCompressionType_None: + { + CopyToMemoryBuffer(*p.target, p.source, p.size); + return; + } + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -3829,7 +3955,7 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } - CopyToMemoryBuffer(*p.target, compressed.size() > 0 ? compressed.c_str() : NULL, compressed.size()); + CopyToMemoryBuffer(*p.target, compressed); } @@ -4503,6 +4629,198 @@ 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 void Convert(FileInfo& info, + const OrthancPluginAttachment2& attachment) + { + std::string uuid; + if (attachment.uuid != NULL) + { + uuid = attachment.uuid; + } + else + { + uuid = Toolbox::GenerateUuid(); + } + + info = 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); + + if (attachment.customData != NULL) + { + info.SetCustomData(attachment.customData, attachment.customDataSize); + } + } + + void OrthancPlugins::ApplyAdoptAttachment(const _OrthancPluginAdoptAttachment& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + FileInfo adoptedFile; + Convert(adoptedFile, *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()); + CopyToMemoryBuffer(*parameters.createdResourceId, resultPublicId); + *(parameters.storeStatus) = Plugins::Convert(result.GetStatus()); + } + } + + static void CheckAttachmentCustomDataSupport(ServerContext& context) + { + if (!context.GetIndex().HasAttachmentCustomDataSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support custom data for attachments"); + } + } + + void OrthancPlugins::ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckAttachmentCustomDataSupport(lock.GetContext()); + + FileInfo fileInfo; + int64_t revision; + + if (lock.GetContext().GetIndex().GetAttachment(fileInfo, revision, parameters.attachmentUuid)) + { + CopyToMemoryBuffer(*parameters.customData, fileInfo.GetCustomData()); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + void OrthancPlugins::ApplySetAttachmentCustomData(const _OrthancPluginSetAttachmentCustomData& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckAttachmentCustomDataSupport(lock.GetContext()); + + lock.GetContext().GetIndex().SetAttachmentCustomData(parameters.attachmentUuid, parameters.customData, parameters.customDataSize); + } + + static void CheckKeyValueStoresSupport(ServerContext& context) + { + if (!context.GetIndex().HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support key-value stores"); + } + } + + void OrthancPlugins::ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + lock.GetContext().GetIndex().StoreKeyValue(parameters.storeId, parameters.key, parameters.value, parameters.valueSize); + } + + void OrthancPlugins::ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + lock.GetContext().GetIndex().DeleteKeyValue(parameters.storeId, parameters.key); + } + + void OrthancPlugins::ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + std::string value; + + if (lock.GetContext().GetIndex().GetKeyValue(value, parameters.storeId, parameters.key)) + { + CopyToMemoryBuffer(*parameters.target, value); + *parameters.found = true; + } + else + { + *parameters.found = false; + } + } + + void OrthancPlugins::ApplyCreateKeysValuesIterator(const _OrthancPluginCreateKeysValuesIterator& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + *parameters.target = reinterpret_cast<OrthancPluginKeysValuesIterator*>( + new StatelessDatabaseOperations::KeysValuesIterator(lock.GetContext().GetIndex(), parameters.storeId)); + } + + static void CheckQueuesSupport(ServerContext& context) + { + if (!context.GetIndex().HasQueuesSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support queues"); + } + } + + void OrthancPlugins::ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckQueuesSupport(lock.GetContext()); + + lock.GetContext().GetIndex().EnqueueValue(parameters.queueId, parameters.value, parameters.valueSize); + } + + void OrthancPlugins::ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckQueuesSupport(lock.GetContext()); + + std::string value; + + if (lock.GetContext().GetIndex().DequeueValue(value, parameters.queueId, Plugins::Convert(parameters.origin))) + { + CopyToMemoryBuffer(*parameters.target, value); + *parameters.found = true; + } + else + { + *parameters.found = false; + } + } + + void OrthancPlugins::ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckQueuesSupport(lock.GetContext()); + + *parameters.size = lock.GetContext().GetIndex().GetQueueSize(parameters.queueId); + } void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params) { @@ -4952,7 +5270,7 @@ std::string content; SystemToolbox::ReadFile(content, p.path); - CopyToMemoryBuffer(*p.target, content.size() > 0 ? content.c_str() : NULL, content.size()); + CopyToMemoryBuffer(*p.target, content); return true; } @@ -5070,32 +5388,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: @@ -5591,6 +5890,107 @@ 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_SetAttachmentCustomData: + { + const _OrthancPluginSetAttachmentCustomData& p = *reinterpret_cast<const _OrthancPluginSetAttachmentCustomData*>(parameters); + ApplySetAttachmentCustomData(p); + return true; + } + + case _OrthancPluginService_StoreKeyValue: + { + const _OrthancPluginStoreKeyValue& p = *reinterpret_cast<const _OrthancPluginStoreKeyValue*>(parameters); + ApplyStoreKeyValue(p); + return true; + } + + case _OrthancPluginService_DeleteKeyValue: + { + const _OrthancPluginDeleteKeyValue& p = *reinterpret_cast<const _OrthancPluginDeleteKeyValue*>(parameters); + ApplyDeleteKeyValue(p); + return true; + } + + case _OrthancPluginService_GetKeyValue: + { + const _OrthancPluginGetKeyValue& p = *reinterpret_cast<const _OrthancPluginGetKeyValue*>(parameters); + ApplyGetKeyValue(p); + return true; + } + + case _OrthancPluginService_CreateKeysValuesIterator: + { + const _OrthancPluginCreateKeysValuesIterator& p = *reinterpret_cast<const _OrthancPluginCreateKeysValuesIterator*>(parameters); + ApplyCreateKeysValuesIterator(p); + return true; + } + + case _OrthancPluginService_FreeKeysValuesIterator: + { + const _OrthancPluginFreeKeysValuesIterator& p = *reinterpret_cast<const _OrthancPluginFreeKeysValuesIterator*>(parameters); + delete reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + return true; + } + + case _OrthancPluginService_KeysValuesIteratorNext: + { + const _OrthancPluginKeysValuesIteratorNext& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorNext*>(parameters); + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + *p.done = iterator.Next() ? 1 : 0; + return true; + } + + case _OrthancPluginService_KeysValuesIteratorGetKey: + { + const _OrthancPluginKeysValuesIteratorGetKey& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetKey*>(parameters); + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + *p.target = iterator.GetKey().c_str(); + return true; + } + + case _OrthancPluginService_KeysValuesIteratorGetValue: + { + const _OrthancPluginKeysValuesIteratorGetValue& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetValue*>(parameters); + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + CopyToMemoryBuffer(*p.target, iterator.GetValue()); + return true; + } + + case _OrthancPluginService_EnqueueValue: + { + const _OrthancPluginEnqueueValue& p = *reinterpret_cast<const _OrthancPluginEnqueueValue*>(parameters); + ApplyEnqueueValue(p); + return true; + } + + case _OrthancPluginService_DequeueValue: + { + const _OrthancPluginDequeueValue& p = *reinterpret_cast<const _OrthancPluginDequeueValue*>(parameters); + ApplyDequeueValue(p); + return true; + } + + case _OrthancPluginService_GetQueueSize: + { + const _OrthancPluginGetQueueSize& p = *reinterpret_cast<const _OrthancPluginGetQueueSize*>(parameters); + ApplyGetQueueSize(p); + return true; + } + default: return false; } @@ -5674,23 +6074,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); @@ -5813,7 +6224,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); @@ -5878,7 +6289,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)); @@ -5977,7 +6388,7 @@ } - IStorageArea* OrthancPlugins::CreateStorageArea() + IPluginStorageArea* OrthancPlugins::CreateStorageArea() { if (!HasStorageArea()) {
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Jun 11 17:52:06 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,26 @@ void ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& parameters); + void ApplyAdoptAttachment(const _OrthancPluginAdoptAttachment& parameters); + + void ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters); + + void ApplySetAttachmentCustomData(const _OrthancPluginSetAttachmentCustomData& parameters); + + void ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters); + + void ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters); + + void ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters); + + void ApplyCreateKeysValuesIterator(const _OrthancPluginCreateKeysValuesIterator& parameters); + + void ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters); + + void ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters); + + void ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters); + void ComputeHash(_OrthancPluginService service, const void* parameters); @@ -291,7 +314,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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Jun 11 17:52:06 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,7 +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_SetAttachmentCustomData = 48, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_StoreKeyValue = 49, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DeleteKeyValue = 50, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetKeyValue = 51, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_CreateKeysValuesIterator = 52, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_FreeKeysValuesIterator = 53, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorNext = 54, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetKey = 55, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetValue = 56, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_EnqueueValue = 57, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DequeueValue = 58, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetQueueSize = 59, /* New in Orthanc 1.12.8 */ /* Registration of callbacks */ _OrthancPluginService_RegisterRestCallback = 1000, @@ -492,6 +505,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 +576,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 +805,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 +1145,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 modes to remove an element from a queue. + **/ + typedef enum + { + OrthancPluginQueueOrigin_Front = 0, /*!< Dequeue from the front of the queue */ + OrthancPluginQueueOrigin_Back = 1, /*!< Dequeue from the back of the queue */ + + _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff + } OrthancPluginQueueOrigin; + + + + /** * @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 +1409,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 +1484,83 @@ /** + * @brief Callback for writing to the storage area. + * + * Signature of a callback function that is triggered when Orthanc writes a file to the storage area. + * + * @param customData Custom, plugin-specific data associated with the attachment (out). + * It must be allocated by the plugin using OrthancPluginCreateMemoryBuffer(). The core of Orthanc will free it. + * If the plugin does not generate custom data, leave `customData` unchanged; it will default to an empty value. + * @param uuid The UUID of the file. + * @param content The content of the file (might be compressed data). + * @param size The size of the file. + * @param type The content type corresponding to this file. + * @param compressionType The compression algorithm that was used to encode `content` + * (the absence of compression is indicated using `OrthancPluginCompressionType_None`). + * @param dicomInstance The DICOM instance being stored. Equals `NULL` if not storing a DICOM instance. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate2) ( + OrthancPluginMemoryBuffer* customData, + const char* uuid, + const void* content, + uint64_t size, + OrthancPluginContentType type, + OrthancPluginCompressionType compressionType, + const OrthancPluginDicomInstance* dicomInstance); + + + + /** + * @brief Callback for reading a range of a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * reads a portion of a file from the storage area. Orthanc + * indicates the start position and the length of the range. + * + * @param target Memory buffer where to store the content of the range. + * The memory buffer is allocated and freed by Orthanc. The length of the range + * of interest corresponds to the size of this buffer. + * @param uuid The UUID of the file of interest. + * @param type The content type corresponding to this file. + * @param rangeStart Start position of the requested range in the file. + * @param customData The custom data of the file of interest. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + uint64_t rangeStart, + const void* customData, + uint32_t customDataSize); + + + + /** + * @brief Callback for removing a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * deletes a file from the storage area. + * + * @param uuid The UUID of the file to be removed. + * @param type The content type corresponding to this file. + * @param customData The custom data of the file to be removed. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) ( + const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint32_t customDataSize); + + + /** * @brief Callback to handle the C-Find SCP requests for worklists. * * Signature of a callback function that is triggered when Orthanc @@ -1509,9 +1627,9 @@ * concurrently by different threads of the Web server of * Orthanc. You must implement proper locking if applicable. * - * Note that if you are using HTTP basic authentication, you can extract - * the username from the "Authorization" HTTP header. The value of that header - * contains username:pwd encoded in base64. + * Note that if you are using HTTP basic authentication, you can + * extract the username from the "Authorization" HTTP header. The + * value of that header contains username:pwd encoded in base64. * * @param method The HTTP method used by the request. * @param uri The URI of interest. @@ -3327,7 +3445,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 +5033,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 +5079,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 +5120,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 +9041,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 +9493,40 @@ } + typedef struct + { + OrthancPluginStorageCreate2 create; + OrthancPluginStorageReadRange2 readRange; + OrthancPluginStorageRemove2 remove; + } _OrthancPluginRegisterStorageArea3; + + /** + * @brief Register a custom storage area, with support for custom data. + * + * This function registers a custom storage area, to replace the + * built-in way Orthanc stores its files on the filesystem. This + * function must be called during the initialization of the plugin, + * i.e. inside the OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param create The callback function to store a file on the custom storage area. + * @param readRange The callback function to read some range of a file from the custom storage area. + * @param remove The callback function to remove a file from the custom storage area. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3( + OrthancPluginContext* context, + OrthancPluginStorageCreate2 create, + OrthancPluginStorageReadRange2 readRange, + OrthancPluginStorageRemove2 remove) + { + _OrthancPluginRegisterStorageArea3 params; + params.create = create; + params.readRange = readRange; + params.remove = remove; + context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, ¶ms); + } + /** * @brief Signature of a callback function that is triggered when * the Orthanc core requests an operation from the database plugin. @@ -9635,6 +9794,468 @@ } + 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; + uint32_t customDataSize; + } OrthancPluginAttachment2; + + + typedef struct + { + OrthancPluginMemoryBuffer* createdResourceId; /* out, in case the attachment is actually a new instance */ + OrthancPluginMemoryBuffer* attachmentUuid; /* out */ + OrthancPluginStoreStatus* storeStatus; /* out */ + 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 */ + } _OrthancPluginAdoptAttachment; + + /** + * @brief Request 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, + OrthancPluginMemoryBuffer* createdResourceId, /* out */ + OrthancPluginMemoryBuffer* attachmentUuid, /* out */ + OrthancPluginStoreStatus* storeStatus /* out */, + const void* buffer, + uint64_t bufferSize, + OrthancPluginAttachment2* attachmentInfo, + OrthancPluginResourceType attachToResourceType, + const char* attachToResourceId) + { + _OrthancPluginAdoptAttachment params; + params.createdResourceId = createdResourceId; + params.attachmentUuid = attachmentUuid; + params.storeStatus = storeStatus; + params.buffer = buffer; + params.bufferSize = bufferSize; + params.attachmentInfo = attachmentInfo; + params.attachToResourceType = attachToResourceType; + params.attachToResourceId = attachToResourceId; + + return context->InvokeService(context, _OrthancPluginService_AdoptAttachment, ¶ms); + } + + typedef struct + { + OrthancPluginMemoryBuffer* customData; /* out */ + const char* attachmentUuid; /* in */ + /* OrthancPluginContentType contentType; */ /* in */ + } _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, + OrthancPluginMemoryBuffer* customData, /* out */ + const char* attachmentUuid /* in */) + /* OrthancPluginContentType contentType, */ /* in */ + { + _OrthancPluginGetAttachmentCustomData params; + params.customData = customData; + params.attachmentUuid = attachmentUuid; + /* params.contentType = contentType; */ + + return context->InvokeService(context, _OrthancPluginService_GetAttachmentCustomData, ¶ms); + } + + typedef struct + { + const char* attachmentUuid; /* in */ + const void* customData; /* in */ + uint32_t customDataSize; /* in */ + } _OrthancPluginSetAttachmentCustomData; + + + /** + * @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 OrthancPluginSetAttachmentCustomData( + OrthancPluginContext* context, + const char* attachmentUuid, /* in */ + const void* customData, /* in */ + uint32_t customDataSize /* in */) + { + _OrthancPluginSetAttachmentCustomData params; + params.attachmentUuid = attachmentUuid; + params.customData = customData; + params.customDataSize = customDataSize; + + return context->InvokeService(context, _OrthancPluginService_SetAttachmentCustomData, ¶ms); + } + + + typedef struct + { + const char* storeId; + const char* key; + const void* value; + uint32_t valueSize; + } _OrthancPluginStoreKeyValue; + + /** + * @brief Store a key-value pair in the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to store (note: storeId + key must be unique). + * @param value The value to store. + * @param valueSize The length of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStoreKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key, /* in */ + const void* value, /* in */ + uint32_t valueSize /* in */) + { + _OrthancPluginStoreKeyValue params; + params.storeId = storeId; + params.key = key; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_StoreKeyValue, ¶ms); + } + + typedef struct + { + const char* storeId; + const char* key; + } _OrthancPluginDeleteKeyValue; + + /** + * @brief Delete a key-value pair from the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to store (note: storeId + key must be unique). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDeleteKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginDeleteKeyValue params; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_DeleteKeyValue, ¶ms); + } + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* storeId; + const char* key; + } _OrthancPluginGetKeyValue; + + /** + * @brief Get the value associated with a key in the Orthanc key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. the key exists in the key-value store. + * @param target Memory buffer where to store the retrieved value. It must be freed + * by the plugin by calling OrthancPluginFreeMemoryBuffer(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to retrieve from the store (note: storeId + key must be unique). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetKeyValue( + OrthancPluginContext* context, + uint8_t* found, /* out */ + OrthancPluginMemoryBuffer* target, /* out */ + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginGetKeyValue params; + params.found = found; + params.target = target; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_GetKeyValue, ¶ms); + } + + + /** + * @brief Opaque structure that represents an iterator over the keys and values of + * a key-value store. + * @ingroup Callbacks + **/ + typedef struct _OrthancPluginKeysValuesIterator_t OrthancPluginKeysValuesIterator; + + + + typedef struct + { + OrthancPluginKeysValuesIterator** target; + const char* storeId; + } _OrthancPluginCreateKeysValuesIterator; + + + /** + * @brief Create an iterator over the key-value pairs of a key-value store in the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @return The newly allocated iterator, or NULL in the case of an error. + * The iterator must be freed by calling OrthancPluginFreeKeysValuesIterator(). + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginKeysValuesIterator* OrthancPluginCreateKeysValuesIterator( + OrthancPluginContext* context, + const char* storeId) + { + OrthancPluginKeysValuesIterator* target = NULL; + + _OrthancPluginCreateKeysValuesIterator params; + params.target = ⌖ + params.storeId = storeId; + + if (context->InvokeService(context, _OrthancPluginService_CreateKeysValuesIterator, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginFreeKeysValuesIterator; + + /** + * @brief Free an iterator over a key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param iterator The iterator of interest. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeKeysValuesIterator( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + _OrthancPluginFreeKeysValuesIterator params; + params.iterator = iterator; + + context->InvokeService(context, _OrthancPluginService_FreeKeysValuesIterator, ¶ms); + } + + + typedef struct + { + uint8_t* done; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorNext; + + /** + * @brief Read the next element of an iterator over a key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param done Pointer to a Boolean that is set to "true" iff. the iterator has reached the end of the store. + * @param iterator The iterator of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorNext( + OrthancPluginContext* context, + uint8_t* done, /* out */ + OrthancPluginKeysValuesIterator* iterator /* in */) + { + _OrthancPluginKeysValuesIteratorNext params; + params.done = done; + params.iterator = iterator; + + return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorNext, ¶ms); + } + + + typedef struct + { + const char** target; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorGetKey; + + /** + * @brief Get the current key of an iterator over a key-value store. + * + * Before using this function, the function OrthancPluginKeysValuesIteratorNext() + * must have been called at least once. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param iterator The iterator of interest. + * @return The current key, or NULL in the case of an error. + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginKeysValuesIteratorGetKey( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + const char* target = NULL; + + _OrthancPluginKeysValuesIteratorGetKey params; + params.target = ⌖ + params.iterator = iterator; + + if (context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetKey, ¶ms) == OrthancPluginErrorCode_Success) + { + return target; + } + else + { + return NULL; + } + } + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorGetValue; + + /** + * @brief Get the current value of an iterator over a key-value store. + * + * Before using this function, the function OrthancPluginKeysValuesIteratorNext() + * must have been called at least once. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target Memory buffer where to store the value that has been retrieved from the key-value store. + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param iterator The iterator of interest. + * @return The current value, or NULL in the case of an error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorGetValue( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target /* out */, + OrthancPluginKeysValuesIterator* iterator /* in */) + { + _OrthancPluginKeysValuesIteratorGetValue params; + params.target = target; + params.iterator = iterator; + + return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetValue, ¶ms); + } + + + + typedef struct + { + const char* queueId; + const void* value; + uint32_t valueSize; + } _OrthancPluginEnqueueValue; + + /** + * @brief Append a value to the back of a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param value The value to store. + * @param valueSize The size of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginEnqueueValue( + OrthancPluginContext* context, + const char* queueId, /* in */ + const void* value, /* in */ + uint32_t valueSize /* in */) + { + _OrthancPluginEnqueueValue params; + params.queueId = queueId; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_EnqueueValue, ¶ms); + } + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* queueId; + OrthancPluginQueueOrigin origin; + } _OrthancPluginDequeueValue; + + /** + * @brief Dequeue a value from a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. a value has been dequeued. + * @param target Memory buffer where to store the value that has been retrieved from the queue. + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param origin The position from where the value is dequeued (back for LIFO, front for FIFO). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDequeueValue( + OrthancPluginContext* context, + uint8_t* found, /* out */ + OrthancPluginMemoryBuffer* target, /* out */ + const char* queueId, /* in */ + OrthancPluginQueueOrigin origin /* in */) + { + _OrthancPluginDequeueValue params; + params.found = found; + params.target = target; + params.queueId = queueId; + params.origin = origin; + + return context->InvokeService(context, _OrthancPluginService_DequeueValue, ¶ms); + } + + typedef struct + { + const char* queueId; + uint64_t* size; + } _OrthancPluginGetQueueSize; + + /** + * @brief Get the number of elements in a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param size The number of elements in the queue. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetQueueSize( + OrthancPluginContext* context, + const char* queueId, /* in */ + uint64_t* size /* out */) + { + _OrthancPluginGetQueueSize params; + params.queueId = queueId; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_GetQueueSize, ¶ms); + } + #ifdef __cplusplus } #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Wed Jun 11 17:52:06 2025 +0200 @@ -55,6 +55,7 @@ int32 compression_type = 5; // opaque "CompressionType" in Orthanc uint64 compressed_size = 6; string compressed_hash = 7; + bytes custom_data = 8; // New in 1.12.8 } enum ResourceType { @@ -94,6 +95,11 @@ ORDERING_CAST_FLOAT = 2; } +enum QueueOrigin { + QUEUE_ORIGIN_FRONT = 0; + QUEUE_ORIGIN_BACK = 1; +} + message ServerIndexChange { int64 seq = 1; int32 change_type = 2; // opaque "ChangeType" in Orthanc @@ -166,6 +172,9 @@ bool has_measure_latency = 7; bool supports_find = 8; // New in Orthanc 1.12.5 bool has_extended_changes = 9; // New in Orthanc 1.12.5 + bool supports_key_value_stores = 10; // New in Orthanc 1.12.8 + bool supports_queues = 11; // New in Orthanc 1.12.8 + bool has_attachment_custom_data = 12; // New in Orthanc 1.12.8 } } @@ -321,6 +330,16 @@ OPERATION_FIND = 50; // New in Orthanc 1.12.5 OPERATION_GET_CHANGES_EXTENDED = 51; // New in Orthanc 1.12.5 OPERATION_COUNT_RESOURCES = 52; // New in Orthanc 1.12.5 + OPERATION_STORE_KEY_VALUE = 53; // New in Orthanc 1.12.8 + OPERATION_DELETE_KEY_VALUE = 54; // New in Orthanc 1.12.8 + OPERATION_GET_KEY_VALUE = 55; // New in Orthanc 1.12.8 + OPERATION_LIST_KEY_VALUES = 56; // New in Orthanc 1.12.8 + OPERATION_ENQUEUE_VALUE = 57; // New in Orthanc 1.12.8 + OPERATION_DEQUEUE_VALUE = 58; // New in Orthanc 1.12.8 + OPERATION_GET_QUEUE_SIZE = 59; // New in Orthanc 1.12.8 + OPERATION_GET_ATTACHMENT = 60; // New in Orthanc 1.12.8 + OPERATION_SET_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; + bytes value = 3; + } + + message Response { + } +} + +message DeleteKeyValue { + message Request { + string store_id = 1; + string key = 2; + } + + message Response { + } +} + +message GetKeyValue { + message Request { + string store_id = 1; + string key = 2; + } + + message Response { + bool found = 1; + bytes value = 2; + } +} + +message ListKeysValues { + message Request { + string store_id = 1; + bool from_first = 2; + string from_key = 3; // Only meaningful if "from_first == false" + uint64 limit = 4; + } + + message Response { + message KeyValue { + string key = 1; + bytes value = 2; + } + repeated KeyValue keys_values = 1; + } +} + +message EnqueueValue { + message Request { + string queue_id = 1; + bytes value = 2; + } + + message Response { + } +} + +message DequeueValue { + message Request { + string queue_id = 1; + QueueOrigin origin = 2; + } + + message Response { + bool found = 1; + bytes value = 2; + } +} + +message GetQueueSize { + message Request { + string queue_id = 1; + } + + message Response { + uint64 size = 1; + } +} + +message GetAttachment { + message Request { + string uuid = 1; + } + + message Response { + bool found = 1; + FileInfo attachment = 2; + int64 revision = 3; + } +} + +message SetAttachmentCustomData { + message Request { + string uuid = 1; + bytes 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; + SetAttachmentCustomData.Request set_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; + SetAttachmentCustomData.Response set_attachment_custom_data = 161; } enum RequestType {
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -4347,4 +4347,209 @@ } } #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator *iterator) : + iterator_(iterator) + { + if (iterator_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::~Iterator() + { + OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::Iterator::Next() + { + uint8_t done; + OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorNext(OrthancPlugins::GetGlobalContext(), &done, iterator_); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return (done != 0); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + std::string KeyValueStore::Iterator::GetKey() const + { + const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + else + { + return s; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::Iterator::GetValue(std::string& value) const + { + OrthancPlugins::MemoryBuffer valueBuffer; + OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorGetValue(OrthancPlugins::GetGlobalContext(), *valueBuffer, iterator_); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + valueBuffer.ToString(value); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::Store(const std::string& key, + const void* value, + size_t valueSize) + { + if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(), storeId_.c_str(), + key.c_str(), value, static_cast<uint32_t>(valueSize)); + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::GetValue(std::string& value, + const std::string& key) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + OrthancPluginErrorCode code = OrthancPluginGetKeyValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::DeleteKey(const std::string& key) + { + OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(), + storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator* KeyValueStore::CreateIterator() + { + return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str())); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + void Queue::Enqueue(const void* value, + size_t valueSize) + { + if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(), + queueId_.c_str(), value, static_cast<uint32_t>(valueSize)); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + bool Queue::DequeueInternal(std::string& value, + OrthancPluginQueueOrigin origin) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + + OrthancPluginErrorCode code = OrthancPluginDequeueValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, queueId_.c_str(), origin); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + uint64_t Queue::GetSize() + { + uint64_t size = 0; + OrthancPluginErrorCode code = OrthancPluginGetQueueSize(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), &size); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return size; + } + } +#endif }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Jun 11 17:52:06 2025 +0200 @@ -134,6 +134,14 @@ # define HAS_ORTHANC_PLUGIN_LOG_MESSAGE 0 #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 1 +# define HAS_ORTHANC_PLUGIN_QUEUES 1 +#else +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 0 +# define HAS_ORTHANC_PLUGIN_QUEUES 0 +#endif + // Macro to tag a function as having been deprecated #if (__cplusplus >= 201402L) // C++14 @@ -1618,4 +1626,101 @@ bool GetAnswerJson(Json::Value& output) const; }; #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + class KeyValueStore : public boost::noncopyable + { + public: + class Iterator : public boost::noncopyable + { + private: + OrthancPluginKeysValuesIterator *iterator_; + + public: + Iterator(OrthancPluginKeysValuesIterator *iterator); + + ~Iterator(); + + bool Next(); + + std::string GetKey() const; + + void GetValue(std::string& target) const; + }; + + private: + std::string storeId_; + + public: + explicit KeyValueStore(const std::string& storeId) : + storeId_(storeId) + { + } + + const std::string& GetStoreId() const + { + return storeId_; + } + + void Store(const std::string& key, + const void* value, + size_t valueSize); + + void Store(const std::string& key, + const std::string& value) + { + Store(key, value.empty() ? NULL : value.c_str(), value.size()); + } + + bool GetValue(std::string& value, + const std::string& key); + + void DeleteKey(const std::string& key); + + Iterator* CreateIterator(); + }; +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + class Queue : public boost::noncopyable + { + private: + std::string queueId_; + + bool DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin); + + public: + explicit Queue(const std::string& queueId) : + queueId_(queueId) + { + } + + const std::string& GetQueueId() const + { + return queueId_; + } + + void Enqueue(const void* value, + size_t valueSize); + + void Enqueue(const std::string& value) + { + Enqueue(value.empty() ? NULL : value.c_str(), value.size()); + } + + bool DequeueBack(std::string& value) + { + return DequeueInternal(value, OrthancPluginQueueOrigin_Back); + } + + bool DequeueFront(std::string& value) + { + return DequeueInternal(value, OrthancPluginQueueOrigin_Front); + } + + uint64_t GetSize(); + }; +#endif }
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Wed Jun 11 17:52:06 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,14 @@ 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 void* customData, + size_t customDataSize) = 0; + /** * If "shared" is "true", the property is shared by all the * Orthanc servers that access the same database. If "shared" is @@ -390,6 +434,42 @@ int64_t to, uint32_t limit, const std::set<ChangeType>& filterType) = 0; + + // New in Orthanc 1.12.8 + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) = 0; + + // New in Orthanc 1.12.8 + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) = 0; + + // New in Orthanc 1.12.8 + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) = 0; + + // New in Orthanc 1.12.8 + virtual void ListKeysValues(std::list<std::string>& keys /* out */, + std::list<std::string>& values /* out */, + const std::string& storeId, + bool first, + const std::string& from /* exclusive bound, only used if "first == false" */, + uint64_t limit /* maximum number of elements */) = 0; + + // New in Orthanc 1.12.8 + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) = 0; + + // New in Orthanc 1.12.8 + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) = 0; + + // New in Orthanc 1.12.8, for statistics only + virtual uint64_t GetQueueSize(const std::string& queueId) = 0; }; @@ -456,7 +536,7 @@ virtual unsigned int GetDatabaseVersion() = 0; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) = 0; + IPluginStorageArea& storageArea) = 0; virtual const Capabilities GetDatabaseCapabilities() const = 0;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallDeletedFiles.sql Wed Jun 11 17:52:06 2025 +0200 @@ -0,0 +1,51 @@ +-- 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/>. + + +CREATE TABLE DeletedFiles( + uuid TEXT NOT NULL, -- 0 + customData TEXT -- 1 +); + +-- We need to use another AttachedFileDeleted trigger than the legacy one in "Upgrade4To5.sql". +-- +-- We want to keep backward compatibility and avoid changing the database version number (which would force +-- users to upgrade the DB). By keeping backward compatibility, we mean "allow a user to run a previous Orthanc +-- version after it has run this update script". +-- We must preserve the signature of the initial trigger (it is impossible to have 2 triggers on the same event). +-- We tried adding a trigger on "BEFORE DELETE" but then it is being called when running the previous Orthanc +-- which makes it fail. +-- But, we need the customData in the C++ function that is called when a AttachedFiles is deleted. +-- The trick is then to save the customData in a DeletedFiles table. +-- The SignalFileDeleted C++ function will then get the customData from this table and delete the entry. +-- Drawback: if you downgrade Orthanc, the DeletedFiles table will remain and will be populated by the trigger +-- but not consumed by the C++ function -> we consider this is an acceptable drawback for a few people compared +-- to the burden of upgrading the DB. + +DROP TRIGGER IF EXISTS AttachedFileDeleted; + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + INSERT INTO DeletedFiles VALUES(old.uuid, old.customData); + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize, + old.uncompressedMD5, old.compressedMD5 + ); +END;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallKeyValueStoresAndQueues.sql Wed Jun 11 17:52:06 2025 +0200 @@ -0,0 +1,35 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2023 Osimis S.A., Belgium +-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +-- Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +-- +-- This program is free software: you can redistribute it and/or +-- modify it under the terms of the GNU General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. + + +CREATE TABLE KeyValueStores( + storeId TEXT NOT NULL, + key TEXT NOT NULL, + value BLOB NOT NULL, + PRIMARY KEY(storeId, key) -- Prevents duplicates + ); + +CREATE TABLE Queues ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + queueId TEXT NOT NULL, + value BLOB NOT NULL +); + +CREATE INDEX QueuesIndex ON Queues (queueId, id);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql Wed Jun 11 17:52:06 2025 +0200 @@ -0,0 +1,34 @@ +-- 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 BLOB; + +-- Record that this upgrade has been performed + +INSERT INTO GlobalProperties VALUES (7, 1); -- GlobalProperty_SQLiteHasCustomDataAndRevision
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Wed Jun 11 17:52:06 2025 +0200 @@ -55,6 +55,7 @@ id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, type INTEGER, value TEXT, + revision INTEGER, -- New in Orthanc 1.12.8 (added in InstallRevisionAndCustomData.sql) PRIMARY KEY(id, type) ); @@ -67,6 +68,8 @@ compressionType INTEGER, uncompressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) compressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) + revision INTEGER, -- New in Orthanc 1.12.8 (added in InstallRevisionAndCustomData.sql) + customData BLOB, -- New in Orthanc 1.12.8 (added in 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,27 @@ END; +-- new in Orthanc 1.5.1 -------------------------- equivalent to InstallTrackAttachmentsSize.sql +${INSTALL_TRACK_ATTACHMENTS_SIZE} + + +-- new in Orthanc 1.12.0 ------------------------- equivalent to InstallLabelsTable.sql +${INSTALL_LABELS_TABLE} + + +-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallDeletedFiles.sql +${INSTALL_DELETED_FILES} + + +-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallKeyValueStoresAndQueues.sql +${INSTALL_KEY_VALUE_STORES_AND_QUEUES} + + +-- Track the fact that the "revision" column exists in the "Metadata" and "AttachedFiles" +-- tables, and that the "customData" column exists in the "AttachedFiles" table +INSERT INTO GlobalProperties VALUES (7, 1); -- GlobalProperty_SQLiteHasCustomDataAndRevision + + -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration INSERT INTO GlobalProperties VALUES (1, "6");
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -39,8 +39,10 @@ #include <OrthancServerResources.h> #include <stdio.h> +#include <boost/algorithm/string/replace.hpp> #include <boost/lexical_cast.hpp> + namespace Orthanc { static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec) @@ -384,19 +386,22 @@ } } - boost::mutex::scoped_lock lock_; + boost::recursive_mutex::scoped_lock lock_; IDatabaseListener& listener_; SignalRemainingAncestor& signalRemainingAncestor_; + bool hasFastTotalSize_; public: - TransactionBase(boost::mutex& mutex, + TransactionBase(boost::recursive_mutex& mutex, SQLite::Connection& db, IDatabaseListener& listener, - SignalRemainingAncestor& signalRemainingAncestor) : + SignalRemainingAncestor& signalRemainingAncestor, + bool hasFastTotalSize) : UnitTestsTransaction(db), lock_(mutex), listener_(listener), - signalRemainingAncestor_(signalRemainingAncestor) + signalRemainingAncestor_(signalRemainingAncestor), + hasFastTotalSize_(hasFastTotalSize) { } @@ -410,8 +415,9 @@ const FileInfo& attachment, int64_t revision) ORTHANC_OVERRIDE { - // TODO - REVISIONS - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5) VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5, revision, customData) " + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); s.BindInt64(0, id); s.BindInt(1, attachment.GetContentType()); s.BindString(2, attachment.GetUuid()); @@ -420,10 +426,11 @@ s.BindInt(5, attachment.GetCompressionType()); s.BindString(6, attachment.GetUncompressedMD5()); s.BindString(7, attachment.GetCompressedMD5()); + s.BindInt(8, revision); + s.BindBlob(9, attachment.GetCustomData()); s.Run(); } - virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, const DatabaseDicomTagConstraints& lookup, @@ -473,10 +480,12 @@ #define C3_STRING_1 3 #define C4_STRING_2 4 #define C5_STRING_3 5 -#define C6_INT_1 6 -#define C7_INT_2 7 -#define C8_BIG_INT_1 8 -#define C9_BIG_INT_2 9 +#define C6_STRING_4 6 +#define C7_INT_1 7 +#define C8_INT_2 8 +#define C9_INT_3 9 +#define C10_BIG_INT_1 10 +#define C11_BIG_INT_2 11 #define QUERY_LOOKUP 1 #define QUERY_MAIN_DICOM_TAGS 2 @@ -525,6 +534,19 @@ } + static void ReadCustomData(FileInfo& info, + SQLite::Statement& statement, + int column) + { + std::string customData; + if (!statement.ColumnIsNull(column) && + statement.ColumnBlobAsString(column, &customData)) + { + info.SwapCustomData(customData); + } + } + + virtual void ExecuteFind(FindResponse& response, const FindRequest& request, const Capabilities& capabilities) ORTHANC_OVERRIDE @@ -588,10 +610,12 @@ " Lookup.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " " FROM Lookup "; // need one instance info ? (part 2: execute the queries) @@ -605,10 +629,12 @@ " instancePublicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " instanceInternalId AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " instanceInternalId AS c10_big_int1, " + " NULL AS c11_big_int2 " " FROM OneInstance "; sql += " UNION SELECT" @@ -618,10 +644,12 @@ " Metadata.value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " Metadata.type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " Metadata.type AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " " FROM OneInstance " " INNER JOIN Metadata ON Metadata.id = OneInstance.instanceInternalId "; @@ -632,10 +660,12 @@ " uuid AS c3_string1, " " uncompressedMD5 AS c4_string2, " " compressedMD5 AS c5_string3, " - " fileType AS c6_int1, " - " compressionType AS c7_int2, " - " compressedSize AS c8_big_int1, " - " uncompressedSize AS c9_big_int2 " + " customData AS c6_string4, " + " fileType AS c7_int1, " + " compressionType AS c8_int2, " + " revision AS c9_int3, " + " compressedSize AS c10_big_int1, " + " uncompressedSize AS c11_big_int2 " " FROM OneInstance " " INNER JOIN AttachedFiles ON AttachedFiles.id = OneInstance.instanceInternalId "; @@ -651,10 +681,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId "; } @@ -669,10 +701,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Metadata ON Metadata.id = Lookup.internalId "; } @@ -687,10 +721,12 @@ " uuid AS c3_string1, " " uncompressedMD5 AS c4_string2, " " compressedMD5 AS c5_string3, " - " fileType AS c6_int1, " - " compressionType AS c7_int2, " - " compressedSize AS c8_big_int1, " - " uncompressedSize AS c9_big_int2 " + " customData AS c6_string4, " + " fileType AS c7_int1, " + " compressionType AS c8_int2, " + " revision AS c9_int3, " + " compressedSize AS c10_big_int1, " + " uncompressedSize AS c11_big_int2 " "FROM Lookup " "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId "; } @@ -706,10 +742,12 @@ " label AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Labels ON Labels.id = Lookup.internalId "; } @@ -726,10 +764,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId "; @@ -745,10 +785,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId "; @@ -766,10 +808,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " @@ -786,10 +830,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " @@ -808,10 +854,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))); @@ -827,10 +875,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " @@ -847,10 +897,12 @@ " parentLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId " " INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "; @@ -866,10 +918,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))) + ") "; @@ -885,10 +939,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " @@ -907,10 +963,12 @@ " childLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "; } @@ -926,10 +984,12 @@ " NULL AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " COUNT(*) AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " COUNT(*) AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId GROUP BY Lookup.internalId "; } @@ -945,10 +1005,12 @@ " grandChildLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "; @@ -964,10 +1026,12 @@ " NULL AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " COUNT(*) AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " COUNT(*) AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId GROUP BY Lookup.internalId "; @@ -983,10 +1047,12 @@ " grandGrandChildLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " @@ -1002,10 +1068,12 @@ " NULL AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " COUNT(*) AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " COUNT(*) AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " @@ -1043,19 +1111,21 @@ case QUERY_ATTACHMENTS: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); - FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), - s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2), - static_cast<CompressionType>(s.ColumnInt(C7_INT_2)), - s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3)); - res.AddAttachment(file, 0 /* TODO - REVISIONS */); + FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)), + s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2), + static_cast<CompressionType>(s.ColumnInt(C8_INT_2)), + s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3)); + ReadCustomData(file, s, C6_STRING_4); + + res.AddAttachment(file, s.ColumnInt(C9_INT_3)); }; break; case QUERY_MAIN_DICOM_TAGS: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddStringDicomTag(requestLevel, - static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), - static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C8_INT_2)), s.ColumnString(C3_STRING_1)); }; break; @@ -1063,8 +1133,8 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 1), - static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), - static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C8_INT_2)), s.ColumnString(C3_STRING_1)); }; break; @@ -1072,8 +1142,8 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 2), - static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), - static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C8_INT_2)), s.ColumnString(C3_STRING_1)); }; break; @@ -1081,7 +1151,7 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 1), - DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))), + DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))), s.ColumnString(C3_STRING_1)); }; break; @@ -1089,7 +1159,7 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 2), - DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))), + DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))), s.ColumnString(C3_STRING_1)); }; break; @@ -1097,31 +1167,31 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddMetadata(static_cast<ResourceType>(requestLevel), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), - s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), + s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2)); }; break; case QUERY_PARENT_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddMetadata(static_cast<ResourceType>(requestLevel - 1), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), - s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), + s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2)); }; break; case QUERY_GRAND_PARENT_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddMetadata(static_cast<ResourceType>(requestLevel - 2), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), - s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), + s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2)); }; break; case QUERY_CHILDREN_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 1), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1)); }; break; @@ -1129,7 +1199,7 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 2), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1)); }; break; @@ -1170,21 +1240,21 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 1), - static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + static_cast<uint64_t>(s.ColumnInt64(C7_INT_1))); }; break; case QUERY_GRAND_CHILDREN_COUNT: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 2), - static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + static_cast<uint64_t>(s.ColumnInt64(C7_INT_1))); }; break; case QUERY_GRAND_GRAND_CHILDREN_COUNT: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 3), - static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + static_cast<uint64_t>(s.ColumnInt64(C7_INT_1))); }; break; case QUERY_ONE_INSTANCE_IDENTIFIER: @@ -1196,16 +1266,18 @@ case QUERY_ONE_INSTANCE_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); - res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), s.ColumnString(C3_STRING_1)); + res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1)); }; break; case QUERY_ONE_INSTANCE_ATTACHMENTS: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); - FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), - s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2), - static_cast<CompressionType>(s.ColumnInt(C7_INT_2)), - s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3)); + FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)), + s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2), + static_cast<CompressionType>(s.ColumnInt(C8_INT_2)), + s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3)); + ReadCustomData(file, s, C6_STRING_4); + res.AddOneInstanceAttachment(file); }; break; @@ -1312,6 +1384,32 @@ } } + void DeleteDeletedFile(const std::string& uuid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DeletedFiles WHERE uuid=?"); + s.BindString(0, uuid); + s.Run(); + } + + void GetDeletedFileCustomData(std::string& customData, const std::string& uuid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT customData FROM DeletedFiles WHERE uuid=?"); + s.BindString(0, uuid); + + if (s.Step()) + { + if (s.ColumnIsNull(C6_STRING_4) || + !s.ColumnBlobAsString(C6_STRING_4, &customData)) + { + customData.clear(); + } + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } virtual void GetAllMetadata(std::map<MetadataType, std::string>& target, int64_t id) ORTHANC_OVERRIDE @@ -1597,23 +1695,39 @@ virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE { - // Old SQL query that was used in Orthanc <= 1.5.0: - // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0"); - s.Run(); - return static_cast<uint64_t>(s.ColumnInt64(0)); + std::unique_ptr<SQLite::Statement> statement; + + if (hasFastTotalSize_) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0")); + } + else + { + // Old SQL query that was used in Orthanc <= 1.5.0: + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles")); + } + + statement->Run(); + return static_cast<uint64_t>(statement->ColumnInt64(0)); } virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE { - // Old SQL query that was used in Orthanc <= 1.5.0: - // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1"); - s.Run(); - return static_cast<uint64_t>(s.ColumnInt64(0)); + std::unique_ptr<SQLite::Statement> statement; + + if (hasFastTotalSize_) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1")); + } + else + { + // Old SQL query that was used in Orthanc <= 1.5.0: + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles")); + } + + statement->Run(); + return static_cast<uint64_t>(statement->ColumnInt64(0)); } @@ -1687,7 +1801,7 @@ { SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT uuid, uncompressedSize, compressionType, compressedSize, " - "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); + "uncompressedMD5, compressedMD5, revision, customData FROM AttachedFiles WHERE id=? AND fileType=?"); s.BindInt64(0, id); s.BindInt(1, contentType); @@ -1704,11 +1818,50 @@ static_cast<CompressionType>(s.ColumnInt(2)), s.ColumnInt64(3), s.ColumnString(5)); - revision = 0; // TODO - REVISIONS + ReadCustomData(attachment, s, 7); + revision = s.ColumnInt(6); return true; } } + virtual 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)); + ReadCustomData(attachment, s, 7); + revision = s.ColumnInt(6); + return true; + } + } + + virtual void UpdateAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "UPDATE AttachedFiles SET customData=? WHERE uuid=?"); + s.BindBlob(0, customData, customDataSize); + s.BindString(1, attachmentUuid); + s.Run(); + } virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, @@ -1739,7 +1892,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 +1903,7 @@ else { target = s.ColumnString(0); - revision = 0; // TODO - REVISIONS + revision = s.ColumnInt(1); return true; } } @@ -1922,11 +2075,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 +2180,170 @@ target.insert(s.ColumnString(0)); } } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO KeyValueStores (storeId, key, value) VALUES(?, ?, ?)"); + s.BindString(0, storeId); + s.BindString(1, key); + s.BindBlob(2, value, valueSize); + s.Run(); + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM KeyValueStores WHERE storeId = ? AND key = ?"); + s.BindString(0, storeId); + s.BindString(1, key); + s.Run(); + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM KeyValueStores WHERE storeId=? AND key=?"); + s.BindString(0, storeId); + s.BindString(1, key); + + if (!s.Step()) + { + // No value found + return false; + } + else + { + if (!s.ColumnBlobAsString(0, &value)) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + else + { + return true; + } + } + } + + // New in Orthanc 1.12.8 + virtual void ListKeysValues(std::list<std::string>& keys /* out */, + std::list<std::string>& values /* out */, + const std::string& storeId, + bool first, + const std::string& from /* only used if "first == false" */, + uint64_t limit) ORTHANC_OVERRIDE + { + int64_t actualLimit = limit; + if (limit == 0) + { + actualLimit = -1; // In SQLite, "if negative, there is no upper bound on the number of rows returned" + } + + std::unique_ptr<SQLite::Statement> statement; + + if (first) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? ORDER BY key ASC LIMIT ?")); + statement->BindString(0, storeId); + statement->BindInt64(1, actualLimit); + } + else + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? AND key>? ORDER BY key ASC LIMIT ?")); + statement->BindString(0, storeId); + statement->BindString(1, from); + statement->BindInt64(2, actualLimit); + } + + while (statement->Step()) + { + std::string value; + if (!statement->ColumnBlobAsString(1, &value)) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + keys.push_back(statement->ColumnString(0)); + values.push_back(value); + } + } + + + // New in Orthanc 1.12.8 + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + if (static_cast<size_t>(static_cast<int>(valueSize)) != valueSize) + { + throw OrthancException(ErrorCode_NotEnoughMemory, "Value is too large for a SQLite database"); + } + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO Queues (queueId, value) VALUES (?, ?)"); + s.BindString(0, queueId); + s.BindBlob(1, value, valueSize); + s.Run(); + } + + // New in Orthanc 1.12.8 + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + int64_t rowId; + std::unique_ptr<SQLite::Statement> s; + + switch (origin) + { + case QueueOrigin_Front: + s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id ASC LIMIT 1")); + break; + + case QueueOrigin_Back: + s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id DESC LIMIT 1")); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + s->BindString(0, queueId); + if (!s->Step()) + { + // No value found + return false; + } + else + { + rowId = s->ColumnInt64(0); + + if (!s->ColumnBlobAsString(1, &value)) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + SQLite::Statement s2(db_, SQLITE_FROM_HERE, + "DELETE FROM Queues WHERE id = ?"); + s2.BindInt64(0, rowId); + s2.Run(); + + return true; + } + } + + // New in Orthanc 1.12.8 + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT COUNT(*) FROM Queues WHERE queueId=?"); + s.BindString(0, queueId); + s.Step(); + return s.ColumnInt64(0); + } }; @@ -2055,6 +2372,11 @@ { if (sqlite_.activeTransaction_ != NULL) { + std::string id = context.GetStringValue(0); + + std::string customData; + sqlite_.activeTransaction_->GetDeletedFileCustomData(customData, id); + std::string uncompressedMD5, compressedMD5; if (!context.IsNullValue(5)) @@ -2074,8 +2396,10 @@ static_cast<CompressionType>(context.GetIntValue(3)), static_cast<uint64_t>(context.GetInt64Value(4)), compressedMD5); + info.SwapCustomData(customData); sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info); + sqlite_.activeTransaction_->DeleteDeletedFile(id); } } }; @@ -2120,20 +2444,40 @@ SQLiteDatabaseWrapper& that_; std::unique_ptr<SQLite::Transaction> transaction_; int64_t initialDiskSize_; + bool isNested_; + + // Rationale for the isNested_ field: + // This was added while implementing the DelayedDeletion part of the advanced-storage plugin. + // When Orthanc deletes an attachment, a SQLite transaction is created to delete the attachment from + // the SQLite DB and, while the transaction is still active, the StorageRemove callback is called. + // The DelayedDeleter does not delete the file directly but, instead, it queues it for deletion. + // Queuing is done through the Orthanc SDK that creates a RW transaction (because it is a generic function). + // Since there is already an active RW transaction, this "nested" transaction does not need to perform anything + // in its Begin/Commit since this will be performed at higher level by the current activeTransaction_. + // However, in case of Rollback, this nested transaction must call the top level transaction Rollback. public: ReadWriteTransaction(SQLiteDatabaseWrapper& that, - IDatabaseListener& listener) : - TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), + IDatabaseListener& listener, + bool hasFastTotalSize) : + TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_, hasFastTotalSize), that_(that), - transaction_(new SQLite::Transaction(that_.db_)) + transaction_(new SQLite::Transaction(that_.db_)), + isNested_(false) { if (that_.activeTransaction_ != NULL) { - throw OrthancException(ErrorCode_InternalError); + if (dynamic_cast<SQLiteDatabaseWrapper::ReadWriteTransaction*>(that_.activeTransaction_) == NULL) + { + throw OrthancException(ErrorCode_InternalError, "Unable to create a nested RW transaction, the current transaction is not a RW transaction"); + } + + isNested_ = true; } - - that_.activeTransaction_ = this; + else + { + that_.activeTransaction_ = this; + } #if defined(NDEBUG) // Release mode @@ -2146,26 +2490,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 +2534,34 @@ { private: SQLiteDatabaseWrapper& that_; + bool isNested_; // see explanation on the ReadWriteTransaction public: ReadOnlyTransaction(SQLiteDatabaseWrapper& that, - IDatabaseListener& listener) : - TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), - that_(that) + IDatabaseListener& listener, + bool hasFastTotalSize) : + TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_, hasFastTotalSize), + that_(that), + isNested_(false) { if (that_.activeTransaction_ != NULL) { - throw OrthancException(ErrorCode_InternalError); + isNested_ = true; + // throw OrthancException(ErrorCode_InternalError); } - - that_.activeTransaction_ = this; + else + { + that_.activeTransaction_ = this; + } } virtual ~ReadOnlyTransaction() { - assert(that_.activeTransaction_ != NULL); - that_.activeTransaction_ = NULL; + if (!isNested_) + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } } virtual void Rollback() ORTHANC_OVERRIDE @@ -2214,11 +2583,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 +2600,14 @@ signalRemainingAncestor_(NULL), version_(0) { - // TODO: implement revisions in SQLite + dbCapabilities_.SetRevisionsSupport(true); dbCapabilities_.SetFlushToDisk(true); dbCapabilities_.SetLabelsSupport(true); dbCapabilities_.SetHasExtendedChanges(true); dbCapabilities_.SetHasFindSupport(HasIntegratedFind()); + dbCapabilities_.SetKeyValueStoresSupport(true); + dbCapabilities_.SetQueuesSupport(true); + dbCapabilities_.SetAttachmentCustomDataSupport(true); db_.OpenInMemory(); } @@ -2245,10 +2620,29 @@ } + static void ExecuteEmbeddedScript(SQLite::Connection& db, + ServerResources::FileResourceId resourceId) + { + std::string script; + ServerResources::GetFileResource(script, resourceId); + db.Execute(script); + } + + + static void InjectEmbeddedScript(std::string& sql, + const std::string& name, + ServerResources::FileResourceId resourceId) + { + std::string script; + ServerResources::GetFileResource(script, resourceId); + boost::replace_all(sql, name, script); + } + + void SQLiteDatabaseWrapper::Open() { { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); if (signalRemainingAncestor_ != NULL) { @@ -2283,6 +2677,12 @@ LOG(INFO) << "Creating the database"; std::string query; ServerResources::GetFileResource(query, ServerResources::PREPARE_DATABASE); + + InjectEmbeddedScript(query, "${INSTALL_TRACK_ATTACHMENTS_SIZE}", ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); + InjectEmbeddedScript(query, "${INSTALL_LABELS_TABLE}", ServerResources::INSTALL_LABELS_TABLE); + InjectEmbeddedScript(query, "${INSTALL_DELETED_FILES}", ServerResources::INSTALL_DELETED_FILES); + InjectEmbeddedScript(query, "${INSTALL_KEY_VALUE_STORES_AND_QUEUES}", ServerResources::INSTALL_KEY_VALUE_STORES_AND_QUEUES); + db_.Execute(query); } @@ -2317,18 +2717,35 @@ tmp != "1") { LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments"; - std::string query; - ServerResources::GetFileResource(query, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); - db_.Execute(query); + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); } // New in Orthanc 1.12.0 if (!db_.DoesTableExist("Labels")) { LOG(INFO) << "Installing the \"Labels\" table"; - std::string query; - ServerResources::GetFileResource(query, ServerResources::INSTALL_LABELS_TABLE); - db_.Execute(query); + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_LABELS_TABLE); + } + + // New in Orthanc 1.12.8 + if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasRevisionAndCustomData, true /* unused in SQLite */) + || tmp != "1") + { + LOG(INFO) << "Upgrading SQLite schema to support revision and customData"; + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA); + } + + // New in Orthanc 1.12.8 + if (!db_.DoesTableExist("DeletedFiles")) + { + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_DELETED_FILES); + } + + // New in Orthanc 1.12.8 + if (!db_.DoesTableExist("KeyValueStores")) + { + LOG(INFO) << "Installing the \"KeyValueStores\" and \"Queues\" tables"; + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_KEY_VALUE_STORES_AND_QUEUES); } } @@ -2339,7 +2756,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 +2775,9 @@ void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); if (targetVersion != 6) { @@ -2401,33 +2818,59 @@ VoidDatabaseListener listener; { - std::unique_ptr<ITransaction> transaction(StartTransaction(TransactionType_ReadWrite, listener)); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Patient); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Study); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Series); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Instance); + ReadWriteTransaction transaction(*this, listener, false /* GetTotalSizeIsFast necessitates the table "GlobalIntegers" */); + transaction.Begin(); + + // ReconstructMaindDicomTags uses LookupAttachment that needs revision and customData. Since we don't want to maintain a legacy version + // of LookupAttachment, we modify the table now) + LOG(INFO) << "First upgrading SQLite schema to support revision and customData to be able to reconstruct main DICOM tags"; + std::string query; + ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA); + db_.Execute(query); + + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Patient); + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Study); + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Series); + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Instance); db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" + boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";"); - transaction->Commit(0); + transaction.Commit(0); } version_ = 6; } + } + // class RaiiTransactionLogger + // { + // TransactionType type_; + // public: + // RaiiTransactionLogger(TransactionType type) + // : type_(type) + // { + // LOG(INFO) << "IN " << (type_ == TransactionType_ReadOnly ? "RO" : "RW"); + // } + // ~RaiiTransactionLogger() + // { + // LOG(INFO) << "OUT " << (type_ == TransactionType_ReadOnly ? "RO" : "RW"); + // } + // }; IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction(TransactionType type, IDatabaseListener& listener) { + // RaiiTransactionLogger logger(type); + switch (type) { case TransactionType_ReadOnly: - return new ReadOnlyTransaction(*this, listener); // This is a no-op transaction in SQLite (thanks to mutex) + return new ReadOnlyTransaction(*this, listener, true); // This is a no-op transaction in SQLite (thanks to mutex) case TransactionType_ReadWrite: { std::unique_ptr<ReadWriteTransaction> transaction; - transaction.reset(new ReadWriteTransaction(*this, listener)); + transaction.reset(new ReadWriteTransaction(*this, listener, true)); transaction->Begin(); return transaction.release(); } @@ -2440,7 +2883,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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -3200,6 +3200,24 @@ return db_.GetDatabaseCapabilities().HasFindSupport(); } + bool StatelessDatabaseOperations::HasAttachmentCustomDataSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport(); + } + + bool StatelessDatabaseOperations::HasKeyValueStoresSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasKeyValueStoresSupport(); + } + + bool StatelessDatabaseOperations::HasQueuesSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasQueuesSupport(); + } + void StatelessDatabaseOperations::ExecuteCount(uint64_t& count, const FindRequest& request) { @@ -3320,4 +3338,424 @@ } } } + + void StatelessDatabaseOperations::StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (value == NULL && + valueSize > 0) + { + throw OrthancException(ErrorCode_NullPointer); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& storeId_; + const std::string& key_; + const void* value_; + size_t valueSize_; + + public: + Operations(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) : + storeId_(storeId), + key_(key), + value_(value), + valueSize_(valueSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.StoreKeyValue(storeId_, key_, value_, valueSize_); + } + }; + + Operations operations(storeId, key, value, valueSize); + Apply(operations); + } + + void StatelessDatabaseOperations::DeleteKeyValue(const std::string& storeId, + const std::string& key) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& storeId_; + const std::string& key_; + + public: + Operations(const std::string& storeId, + const std::string& key) : + storeId_(storeId), + key_(key) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.DeleteKeyValue(storeId_, key_); + } + }; + + Operations operations(storeId, key); + Apply(operations); + } + + bool StatelessDatabaseOperations::GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public ReadOnlyOperationsT3<std::string&, const std::string&, const std::string& > + { + bool found_; + public: + Operations(): + found_(false) + {} + + bool HasFound() + { + return found_; + } + + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + found_ = transaction.GetKeyValue(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + Operations operations; + operations.Apply(*this, value, storeId, key); + + return operations.HasFound(); + } + + void StatelessDatabaseOperations::EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) + { + if (queueId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (value == NULL && + valueSize > 0) + { + throw OrthancException(ErrorCode_NullPointer); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& queueId_; + const void* value_; + size_t valueSize_; + + public: + Operations(const std::string& queueId, + const void* value, + size_t valueSize) : + queueId_(queueId), + value_(value), + valueSize_(valueSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.EnqueueValue(queueId_, value_, valueSize_); + } + }; + + Operations operations(queueId, value, valueSize); + Apply(operations); + } + + bool StatelessDatabaseOperations::DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) + { + if (queueId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& queueId_; + std::string& value_; + QueueOrigin origin_; + bool found_; + + public: + Operations(std::string& value, + const std::string& queueId, + QueueOrigin origin) : + queueId_(queueId), + value_(value), + origin_(origin), + found_(false) + { + } + + bool HasFound() + { + return found_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + found_ = transaction.DequeueValue(value_, queueId_, origin_); + } + }; + + Operations operations(value, queueId, origin); + Apply(operations); + + return operations.HasFound(); + } + + uint64_t StatelessDatabaseOperations::GetQueueSize(const std::string& queueId) + { + if (queueId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public ReadOnlyOperationsT2<uint64_t&, const std::string& > + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + tuple.get<0>() = transaction.GetQueueSize(tuple.get<1>()); + } + }; + + uint64_t size; + + Operations operations; + operations.Apply(*this, size, queueId); + + return size; + } + + + 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::SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& attachmentUuid_; + const void* customData_; + size_t customDataSize_; + + public: + Operations(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) : + attachmentUuid_(attachmentUuid), + customData_(customData), + customDataSize_(customDataSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.SetAttachmentCustomData(attachmentUuid_, customData_, customDataSize_); + } + }; + + Operations operations(attachmentUuid, customData, customDataSize); + Apply(operations); + } + + StatelessDatabaseOperations::KeysValuesIterator::KeysValuesIterator(StatelessDatabaseOperations& db, + const std::string& storeId) : + db_(db), + state_(State_Waiting), + storeId_(storeId), + limit_(100) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + bool StatelessDatabaseOperations::KeysValuesIterator::Next() + { + if (state_ == State_Done) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (state_ == State_Available) + { + assert(currentKey_ != keys_.end()); + assert(currentValue_ != values_.end()); + ++currentKey_; + ++currentValue_; + + if (currentKey_ != keys_.end() && + currentValue_ != values_.end()) + { + // A value is still available in the last keys-values block fetched from the database + return true; + } + else if (currentKey_ != keys_.end() || + currentValue_ != values_.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + class Operations : public ReadOnlyOperationsT6<std::list<std::string>&, std::list<std::string>&, const std::string&, bool, const std::string&, uint64_t> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ListKeysValues(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>()); + } + }; + + if (state_ == State_Waiting) + { + keys_.clear(); + values_.clear(); + + Operations operations; + operations.Apply(db_, keys_, values_, storeId_, true, "", limit_); + } + else + { + assert(state_ == State_Available); + if (keys_.empty()) + { + state_ = State_Done; + return false; + } + else + { + const std::string lastKey = keys_.back(); + keys_.clear(); + values_.clear(); + + Operations operations; + operations.Apply(db_, keys_, values_, storeId_, false, lastKey, limit_); + } + } + + if (keys_.size() != values_.size()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (limit_ != 0 && + keys_.size() > limit_) + { + // The database plugin has returned too many key-value pairs + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (keys_.empty() && + values_.empty()) + { + state_ = State_Done; + return false; + } + else if (!keys_.empty() && + !values_.empty()) + { + state_ = State_Available; + currentKey_ = keys_.begin(); + currentValue_ = values_.begin(); + return true; + } + else + { + throw OrthancException(ErrorCode_InternalError); // Should never happen + } + } + + const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetKey() const + { + if (state_ == State_Available) + { + return *currentKey_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetValue() const + { + if (state_ == State_Available) + { + return *currentValue_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed Jun 11 17:52:06 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,42 @@ { transaction_.RemoveLabel(id, label); } + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) + { + transaction_.StoreKeyValue(storeId, key, value, valueSize); + } + + void DeleteKeyValue(const std::string& storeId, + const std::string& key) + { + transaction_.DeleteKeyValue(storeId, key); + } + + void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) + { + transaction_.EnqueueValue(queueId, value, valueSize); + } + + bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) + { + return transaction_.DequeueValue(value, queueId, origin); + } + + void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) + { + return transaction_.UpdateAttachmentCustomData(attachmentUuid, customData, customDataSize); + } + }; @@ -523,6 +588,14 @@ /* out */ uint64_t& countSeries, /* out */ uint64_t& countInstances); + bool GetAttachment(FileInfo& attachment, + int64_t& revision, + const std::string& attachmentUuid); + + void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize); + bool LookupAttachment(FileInfo& attachment, int64_t& revision, ResourceType level, @@ -544,6 +617,12 @@ bool HasExtendedChanges(); bool HasFindSupport(); + + bool HasAttachmentCustomDataSupport(); + + bool HasKeyValueStoresSupport(); + + bool HasQueuesSupport(); void GetExportedResources(Json::Value& target, int64_t since, @@ -724,5 +803,80 @@ void ExecuteCount(uint64_t& count, const FindRequest& request); + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize); + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) + { + StoreKeyValue(storeId, key, value.empty() ? NULL : value.c_str(), value.size()); + } + + void DeleteKeyValue(const std::string& storeId, + const std::string& key); + + bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key); + + void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize); + + void EnqueueValue(const std::string& queueId, + const std::string& value) + { + EnqueueValue(queueId, value.empty() ? NULL : value.c_str(), value.size()); + } + + bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin); + + uint64_t GetQueueSize(const std::string& queueId); + + class KeysValuesIterator : public boost::noncopyable + { + private: + enum State + { + State_Waiting, + State_Available, + State_Done + }; + + StatelessDatabaseOperations& db_; + State state_; + std::string storeId_; + uint64_t limit_; + std::list<std::string> keys_; + std::list<std::string> values_; + std::list<std::string>::const_iterator currentKey_; + std::list<std::string>::const_iterator currentValue_; + + public: + KeysValuesIterator(StatelessDatabaseOperations& db, + const std::string& storeId); + + void SetLimit(uint64_t limit) + { + limit_ = limit; + } + + uint64_t GetLimit() const + { + return limit_; + } + + bool Next(); + + const std::string& GetKey() const; + + const std::string& GetValue() const; + }; }; }
--- a/OrthancServer/Sources/OrthancInitialization.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.h Wed Jun 11 17:52:06 2025 +0200 @@ -35,7 +35,7 @@ IDatabaseWrapper* CreateDatabaseWrapper(); - IStorageArea* CreateStorageArea(); + IPluginStorageArea* CreateStorageArea(); void SetGlobalVerbosity(Verbosity verbosity);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -356,7 +356,7 @@ ServerContext::ServerContext(IDatabaseWrapper& database, - IStorageArea& area, + IPluginStorageArea& area, bool unitTesting, size_t maxCompletedJobs, bool readOnly, @@ -613,10 +613,11 @@ void ServerContext::RemoveFile(const std::string& fileUuid, - FileContentType type) + FileContentType type, + const std::string& customData) { StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - accessor.Remove(fileUuid, type); + accessor.Remove(fileUuid, type, customData); } @@ -625,6 +626,37 @@ StoreInstanceMode mode, bool isReconstruct) { + FileInfo adoptedFileNotUsed; + + return StoreAfterTranscoding(resultPublicId, + dicom, + mode, + isReconstruct, + false, + adoptedFileNotUsed); + } + + ServerContext::StoreResult ServerContext::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) { @@ -727,19 +759,25 @@ // TODO Should we use "gzip" instead? CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); - FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), - FileContentType_Dicom, compression, storeMD5_); + StatelessDatabaseOperations::Attachments attachments; + FileInfo dicomInfo; - ServerIndex::Attachments attachments; - attachments.push_back(dicomInfo); + if (!isAdoption) + { + accessor.Write(dicomInfo, dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom, compression, storeMD5_, &dicom); + attachments.push_back(dicomInfo); + } + else + { + attachments.push_back(adoptedFile); + } FileInfo dicomUntilPixelData; if (hasPixelDataOffset && - (!area_.HasReadRange() || + (!area_.HasEfficientReadRange() || compressionEnabled_)) { - dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, - FileContentType_DicomUntilPixelData, compression, storeMD5_); + accessor.Write(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL); attachments.push_back(dicomUntilPixelData); } @@ -784,7 +822,10 @@ if (result.GetStatus() != StoreStatus_Success) { - accessor.Remove(dicomInfo); + if (!isAdoption) + { + accessor.Remove(dicomInfo); + } if (dicomUntilPixelData.IsValid()) { @@ -798,7 +839,14 @@ switch (result.GetStatus()) { case StoreStatus_Success: - LOG(INFO) << "New instance stored (" << resultPublicId << ")"; + if (isAdoption) + { + LOG(INFO) << "New instance adopted (" << resultPublicId << ")"; + } + else + { + LOG(INFO) << "New instance stored (" << resultPublicId << ")"; + } break; case StoreStatus_AlreadyStored: @@ -1038,8 +1086,8 @@ StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); accessor.Read(content, attachment); - FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(), - content.size(), attachmentType, compression, storeMD5_); + FileInfo modified; + accessor.Write(modified, content.empty() ? NULL : content.c_str(), content.size(), attachmentType, compression, storeMD5_, NULL); try { @@ -1221,7 +1269,7 @@ if (hasPixelDataOffset && - area_.HasReadRange() && + area_.HasEfficientReadRange() && LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments) && attachment.GetCompressionType() == CompressionType_None) { @@ -1299,13 +1347,13 @@ index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset, boost::lexical_cast<std::string>(pixelDataOffset)); - if (!area_.HasReadRange() || + if (!area_.HasEfficientReadRange() || compressionEnabled_) { int64_t newRevision; - AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData, + AddAttachment(newRevision, instancePublicId, ResourceType_Instance, FileContentType_DicomUntilPixelData, dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset, - false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); + false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); } } } @@ -1374,7 +1422,7 @@ return true; } - if (!area_.HasReadRange()) + if (!area_.HasEfficientReadRange()) { return false; } @@ -1533,6 +1581,7 @@ bool ServerContext::AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -1546,7 +1595,11 @@ CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_); + + assert(attachmentType != FileContentType_Dicom && attachmentType != FileContentType_DicomUntilPixelData); // this method can not be used to store instances + + FileInfo attachment; + accessor.Write(attachment, data, size, attachmentType, compression, storeMD5_, NULL); try {
--- a/OrthancServer/Sources/ServerContext.h Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/ServerContext.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Wed Jun 11 17:52:06 2025 +0200 @@ -173,6 +173,7 @@ GlobalProperty_AnonymizationSequence = 3, GlobalProperty_JobsRegistry = 5, GlobalProperty_GetTotalSizeIsFast = 6, // New in Orthanc 1.5.2 + GlobalProperty_SQLiteHasRevisionAndCustomData = 7, // New in Orthanc 1.12.8 GlobalProperty_Modalities = 20, // New in Orthanc 1.5.0 GlobalProperty_Peers = 21, // New in Orthanc 1.5.0 @@ -260,6 +261,11 @@ Warnings_007_MissingRequestedTagsNotReadFromDisk // new in Orthanc 1.12.5 }; + enum QueueOrigin + { + QueueOrigin_Front, + QueueOrigin_Back + }; void InitializeServerEnumerations();
--- a/OrthancServer/Sources/ServerIndex.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/ServerToolbox.h Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/Sources/main.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Wed Jun 11 17:52:06 2025 +0200 @@ -27,6 +27,7 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/Images/Image.h" #include "../../OrthancFramework/Sources/Logging.h" @@ -196,6 +197,74 @@ transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */); } }; + + class DummyTransactionContextFactory : public StatelessDatabaseOperations::ITransactionContextFactory + { + public: + virtual StatelessDatabaseOperations::ITransactionContext* Create() + { + class DummyTransactionContext : public StatelessDatabaseOperations::ITransactionContext + { + public: + virtual void SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalResourceDeleted(ResourceType type, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void Commit() ORTHANC_OVERRIDE + { + } + + virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE + { + return 0; + } + + virtual bool IsUnstableResource(Orthanc::ResourceType type, + int64_t id) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */, + ResourceType& remainingLevel /* out */) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void MarkAsUnstable(Orthanc::ResourceType type, + int64_t id, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalAttachmentsAdded(uint64_t compressedSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + }; + + return new DummyTransactionContext; + } + }; } @@ -295,12 +364,18 @@ transaction_->GetAllMetadata(md, a[4]); ASSERT_EQ(0u, md.size()); - transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", - CompressionType_ZlibWithSize, 21, "compressedMD5"), 42); - transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"), 43); - transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"), 44); + FileInfo attachment1("my json file", FileContentType_DicomAsJson, 42, "md5", + CompressionType_ZlibWithSize, 21, "compressedMD5"); + attachment1.SetCustomData("hello"); + transaction_->AddAttachment(a[4], attachment1, 42); + + FileInfo attachment2("my dicom file", FileContentType_Dicom, 43, "md5_2"); + transaction_->AddAttachment(a[4], attachment2, 43); + + FileInfo attachment3("world", FileContentType_Dicom, 44, "md5_3"); + attachment3.SetCustomData("world"); + transaction_->AddAttachment(a[6], attachment3, 44); - // TODO - REVISIONS - "42" is revision number, that is not currently stored (*) transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42); transaction_->GetAllMetadata(md, a[4]); @@ -326,8 +401,8 @@ ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]); - ASSERT_EQ(21u + 42u + 44u, transaction_->GetTotalCompressedSize()); - ASSERT_EQ(42u + 42u + 44u, transaction_->GetTotalUncompressedSize()); + ASSERT_EQ(21u + 43u + 44u, transaction_->GetTotalCompressedSize()); + ASSERT_EQ(42u + 43u + 44u, transaction_->GetTotalUncompressedSize()); transaction_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName"); @@ -339,17 +414,17 @@ int64_t revision; ASSERT_TRUE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_RemoteAet)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(42, revision); ASSERT_FALSE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_Instance_IndexInSeries)); - ASSERT_EQ(0, revision); + ASSERT_EQ(42, revision); ASSERT_EQ("PINNACLE", s); std::string u; ASSERT_TRUE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_RemoteAet)); - ASSERT_EQ(0, revision); + ASSERT_EQ(42, revision); ASSERT_EQ("PINNACLE", u); ASSERT_FALSE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_Instance_IndexInSeries)); - ASSERT_EQ(0, revision); + ASSERT_EQ(42, revision); ASSERT_TRUE(transaction_->LookupGlobalProperty(s, GlobalProperty_FlushSleep, true)); ASSERT_FALSE(transaction_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42), true)); @@ -357,22 +432,34 @@ FileInfo att; ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(42, revision); ASSERT_EQ("my json file", att.GetUuid()); ASSERT_EQ(21u, att.GetCompressedSize()); ASSERT_EQ("md5", att.GetUncompressedMD5()); ASSERT_EQ("compressedMD5", att.GetCompressedMD5()); ASSERT_EQ(42u, att.GetUncompressedSize()); ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType()); + ASSERT_EQ("hello", att.GetCustomData()); + + ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_Dicom)); + ASSERT_EQ(43, revision); + ASSERT_EQ("my dicom file", att.GetUuid()); + ASSERT_EQ(43u, att.GetCompressedSize()); + ASSERT_EQ("md5_2", att.GetUncompressedMD5()); + ASSERT_EQ("md5_2", att.GetCompressedMD5()); + ASSERT_EQ(43u, att.GetUncompressedSize()); + ASSERT_EQ(CompressionType_None, att.GetCompressionType()); + ASSERT_TRUE(att.GetCustomData().empty()); ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(44, revision); ASSERT_EQ("world", att.GetUuid()); ASSERT_EQ(44u, att.GetCompressedSize()); - ASSERT_EQ("md5", att.GetUncompressedMD5()); - ASSERT_EQ("md5", att.GetCompressedMD5()); + ASSERT_EQ("md5_3", att.GetUncompressedMD5()); + ASSERT_EQ("md5_3", att.GetCompressedMD5()); ASSERT_EQ(44u, att.GetUncompressedSize()); ASSERT_EQ(CompressionType_None, att.GetCompressionType()); + ASSERT_EQ("world", att.GetCustomData()); ASSERT_EQ(0u, listener_->deletedFiles_.size()); ASSERT_EQ(0u, listener_->deletedResources_.size()); @@ -402,7 +489,7 @@ CheckTableRecordCount(0, "Resources"); CheckTableRecordCount(0, "AttachedFiles"); - CheckTableRecordCount(3, "GlobalProperties"); + CheckTableRecordCount(4, "GlobalProperties"); std::string tmp; ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true)); @@ -618,7 +705,7 @@ const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); - FilesystemStorage storage(path); + PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -700,7 +787,7 @@ const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); - FilesystemStorage storage(path); + PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -817,7 +904,7 @@ { bool overwrite = (i == 0); - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -982,7 +1069,7 @@ { const bool compression = (i == 0); - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -1058,3 +1145,215 @@ ASSERT_FALSE(ServerToolbox::IsValidLabel("&")); ASSERT_FALSE(ServerToolbox::IsValidLabel(".")); } + + +TEST(SQLiteDatabaseWrapper, KeyValueStores) +{ + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + + { + StatelessDatabaseOperations op(db, false); + op.SetTransactionContextFactory(new DummyTransactionContextFactory); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.StoreKeyValue("test", "hello", "world"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.StoreKeyValue("test", "hello2", "world2"); + op.StoreKeyValue("test", "hello3", "world3"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello2", it.GetKey()); + ASSERT_EQ("world2", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello3", it.GetKey()); + ASSERT_EQ("world3", it.GetValue()); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.DeleteKeyValue("test", "hello2"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello3", it.GetKey()); + ASSERT_EQ("world3", it.GetValue()); + ASSERT_FALSE(it.Next()); + } + + std::string s; + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("world", s); + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello3")); ASSERT_EQ("world3", s); + ASSERT_FALSE(op.GetKeyValue(s, "test", "hello2")); + + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("world", s); + op.StoreKeyValue("test", "hello", "overwritten"); + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("overwritten", s); + + op.DeleteKeyValue("test", "nope"); + + op.DeleteKeyValue("test", "hello"); + op.DeleteKeyValue("test", "hello3"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_FALSE(it.Next()); + } + + { + std::string blob; + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); + op.StoreKeyValue("test", "blob", blob); // Storing binary values + } + + ASSERT_TRUE(op.GetKeyValue(s, "test", "blob")); + ASSERT_EQ(4u, s.size()); + ASSERT_EQ(0, static_cast<uint8_t>(s[0])); + ASSERT_EQ(1, static_cast<uint8_t>(s[1])); + ASSERT_EQ(0, static_cast<uint8_t>(s[2])); + ASSERT_EQ(2, static_cast<uint8_t>(s[3])); + op.DeleteKeyValue("test", "blob"); + ASSERT_FALSE(op.GetKeyValue(s, "test", "blob")); + } + + db.Close(); +} + + +TEST(SQLiteDatabaseWrapper, Queues) +{ + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + + { + StatelessDatabaseOperations op(db, false); + op.SetTransactionContextFactory(new DummyTransactionContextFactory); + + ASSERT_EQ(0u, op.GetQueueSize("test")); + op.EnqueueValue("test", "hello"); + ASSERT_EQ(1u, op.GetQueueSize("test")); + op.EnqueueValue("test", "world"); + ASSERT_EQ(2u, op.GetQueueSize("test")); + + std::string s; + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back)); ASSERT_EQ("world", s); + ASSERT_EQ(1u, op.GetQueueSize("test")); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back)); ASSERT_EQ("hello", s); + ASSERT_EQ(0u, op.GetQueueSize("test")); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Back)); + + op.EnqueueValue("test", "hello"); + op.EnqueueValue("test", "world"); + ASSERT_EQ(2u, op.GetQueueSize("test")); + + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); ASSERT_EQ("hello", s); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); ASSERT_EQ("world", s); + ASSERT_EQ(0u, op.GetQueueSize("test")); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front)); + + { + std::string blob; + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); + op.EnqueueValue("test", blob); // Storing binary values + } + + ASSERT_EQ(1u, op.GetQueueSize("test")); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); + ASSERT_EQ(0u, op.GetQueueSize("test")); + ASSERT_EQ(4u, s.size()); + ASSERT_EQ(0, static_cast<uint8_t>(s[0])); + ASSERT_EQ(1, static_cast<uint8_t>(s[1])); + ASSERT_EQ(0, static_cast<uint8_t>(s[2])); + ASSERT_EQ(2, static_cast<uint8_t>(s[3])); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front)); + } + + db.Close(); +} + + +TEST_F(DatabaseWrapperTest, BinaryCustomData) +{ + int64_t patient = transaction_->CreateResource("Patient", ResourceType_Patient); + + { + FileInfo info("hello", FileContentType_Dicom, 10, "md5"); + + { + std::string blob; + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); + info.SetCustomData(blob); + } + + transaction_->AddAttachment(patient, info, 43); + } + + { + FileInfo info; + int64_t revision; + ASSERT_TRUE(transaction_->LookupAttachment(info, revision, patient, FileContentType_Dicom)); + ASSERT_EQ(43u, revision); + ASSERT_EQ("hello", info.GetUuid()); + ASSERT_EQ(CompressionType_None, info.GetCompressionType()); + ASSERT_EQ(10u, info.GetCompressedSize()); + ASSERT_EQ("md5", info.GetCompressedMD5()); + ASSERT_EQ(4u, info.GetCustomData().size()); + ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[0])); + ASSERT_EQ(1, static_cast<uint8_t>(info.GetCustomData()[1])); + ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[2])); + ASSERT_EQ(2, static_cast<uint8_t>(info.GetCustomData()[3])); + } + + transaction_->DeleteResource(patient); +} \ No newline at end of file
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Wed Jun 11 17:51:11 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Wed Jun 11 17:52:06 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 Wed Jun 11 17:51:11 2025 +0200 +++ b/TODO Wed Jun 11 17:52:06 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 === =======================