Mercurial > hg > orthanc
changeset 5807:8279eaab0d1d attach-custom-data
merged default -> attach-custom-data
line wrap: on
line diff
--- a/NEWS Tue Sep 24 09:26:42 2024 +0200 +++ b/NEWS Tue Sep 24 11:39:52 2024 +0200 @@ -1,6 +1,25 @@ Pending changes in the mainline =============================== +<<<<<<< working copy +General +------- + +* SQLite default DB engine now supports metadata and attachment revisions +* Upgraded the DB to allow plugins to store customData for each attachment. +* New sample Advanced Storage plugin that allows: + - using multiple disk for image storage + - use more human friendly storage structure (experimental feature) + +Plugins +------- + +* New database plugin SDK (v4) to handle customData for attachments. +* New storage plugin SDK (v3) to handle customData for attachments, + + +version 1.11.2 (2022-08-30) +======= REST API ----------- @@ -380,6 +399,7 @@ Version 1.11.2 (2022-08-30) +>>>>>>> merge rev =========================== General
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue Sep 24 11:39:52 2024 +0200 @@ -33,7 +33,7 @@ # * Orthanc 0.4.0 -> Orthanc 0.7.2 = version 3 # * Orthanc 0.7.3 -> Orthanc 0.8.4 = version 4 # * Orthanc 0.8.5 -> Orthanc 0.9.4 = version 5 -# * Orthanc 0.9.5 -> mainline = version 6 +# * Orthanc 0.9.5 -> Orthanc 1.11.X = version 6 set(ORTHANC_DATABASE_VERSION 6) # Version of the Orthanc API, can be retrieved from "/system" URI in
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -42,7 +42,8 @@ FileInfo::FileInfo(const std::string& uuid, FileContentType contentType, uint64_t size, - const std::string& md5) : + const std::string& md5, + const std::string& customData) : valid_(true), uuid_(uuid), contentType_(contentType), @@ -50,7 +51,8 @@ uncompressedMD5_(md5), compressionType_(CompressionType_None), compressedSize_(size), - compressedMD5_(md5) + compressedMD5_(md5), + customData_(customData) { } @@ -61,7 +63,8 @@ const std::string& uncompressedMD5, CompressionType compressionType, uint64_t compressedSize, - const std::string& compressedMD5) : + const std::string& compressedMD5, + const std::string& customData) : valid_(true), uuid_(uuid), contentType_(contentType), @@ -69,7 +72,8 @@ uncompressedMD5_(uncompressedMD5), compressionType_(compressionType), compressedSize_(compressedSize), - compressedMD5_(compressedMD5) + compressedMD5_(compressedMD5), + customData_(customData) { } @@ -169,4 +173,16 @@ throw OrthancException(ErrorCode_BadSequenceOfCalls); } } + + const std::string& FileInfo::GetCustomData() const + { + if (valid_) + { + return customData_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } }
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.h Tue Sep 24 11:39:52 2024 +0200 @@ -42,6 +42,7 @@ CompressionType compressionType_; uint64_t compressedSize_; std::string compressedMD5_; + std::string customData_; public: FileInfo(); @@ -52,7 +53,8 @@ FileInfo(const std::string& uuid, FileContentType contentType, uint64_t size, - const std::string& md5); + const std::string& md5, + const std::string& customData); /** * Constructor for a compressed attachment. @@ -63,7 +65,8 @@ const std::string& uncompressedMD5, CompressionType compressionType, uint64_t compressedSize, - const std::string& compressedMD5); + const std::string& compressedMD5, + const std::string& customData); bool IsValid() const; @@ -80,5 +83,7 @@ const std::string& GetCompressedMD5() const; const std::string& GetUncompressedMD5() const; + + const std::string& GetCustomData() const; }; }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -279,6 +279,7 @@ { namespace fs = boost::filesystem; typedef std::set<std::string> List; + std::string customDataNotUsed; List result; ListAllFiles(result);
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Tue Sep 24 11:39:52 2024 +0200 @@ -43,7 +43,7 @@ namespace Orthanc { - class ORTHANC_PUBLIC FilesystemStorage : public IStorageArea + class ORTHANC_PUBLIC FilesystemStorage : public ICoreStorageArea { // TODO REMOVE THIS friend class FilesystemHttpSender;
--- a/OrthancFramework/Sources/FileStorage/IStorageArea.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h Tue Sep 24 11:39:52 2024 +0200 @@ -33,6 +33,8 @@ namespace Orthanc { + class DicomInstanceToStore; + class IStorageArea : public boost::noncopyable { public: @@ -40,8 +42,92 @@ { } + virtual void CreateInstance(std::string& customData, + const DicomInstanceToStore& instance, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + bool isCompressed) = 0; + + virtual void CreateAttachment(std::string& customData, + const std::string& resourceId, + ResourceType resourceLevel, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + bool isCompressed) = 0; + + virtual IMemoryBuffer* Read(const std::string& uuid, + FileContentType type, + const std::string& customData) = 0; + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) = 0; + + virtual bool HasReadRange() const = 0; + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) = 0; + }; + + // storage area without customData (customData are used only in plugins) + class ICoreStorageArea : public IStorageArea + { + public: + virtual void CreateInstance(std::string& customData, + const DicomInstanceToStore& instance, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + bool isCompressed) + { + Create(uuid, content, size, type); + } + + virtual void CreateAttachment(std::string& customData, + const std::string& resourceId, + ResourceType resourceLevel, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + bool isCompressed) + { + Create(uuid, content, size, type); + } + + virtual IMemoryBuffer* Read(const std::string& uuid, + FileContentType type, + const std::string& /*customData*/) + { + return Read(uuid, type); + } + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& /*customData */) + { + return ReadRange(uuid, type, start, end); + } + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) + { + Remove(uuid, type); + } + virtual void Create(const std::string& uuid, - const void* content, + const void* content, size_t size, FileContentType type) = 0; @@ -53,9 +139,8 @@ uint64_t start /* inclusive */, uint64_t end /* exclusive */) = 0; - virtual bool HasReadRange() const = 0; - virtual void Remove(const std::string& uuid, FileContentType type) = 0; + }; }
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Tue Sep 24 11:39:52 2024 +0200 @@ -33,7 +33,7 @@ namespace Orthanc { - class MemoryStorageArea : public IStorageArea + class MemoryStorageArea : public ICoreStorageArea { private: typedef std::map<std::string, std::string*> Content; @@ -44,6 +44,7 @@ public: virtual ~MemoryStorageArea(); + protected: virtual void Create(const std::string& uuid, const void* content, size_t size,
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -102,14 +102,15 @@ } - FileInfo StorageAccessor::Write(const void* data, - size_t size, - FileContentType type, - CompressionType compression, - bool storeMd5) + FileInfo StorageAccessor::WriteInstance(std::string& customData, + const DicomInstanceToStore& instance, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const std::string& uuid) { - std::string uuid = Toolbox::GenerateUuid(); - std::string md5; if (storeMd5) @@ -123,7 +124,7 @@ { { MetricsTimer timer(*this, METRICS_CREATE_DURATION); - area_.Create(uuid, data, size, type); + area_.CreateInstance(customData, instance, uuid, data, size, type, false); } if (metrics_ != NULL) @@ -137,7 +138,7 @@ cacheAccessor.Add(uuid, type, data, size); } - return FileInfo(uuid, type, size, md5); + return FileInfo(uuid, type, size, md5, customData); } case CompressionType_ZlibWithSize: @@ -159,11 +160,11 @@ if (compressed.size() > 0) { - area_.Create(uuid, &compressed[0], compressed.size(), type); + area_.CreateInstance(customData, instance, uuid, &compressed[0], compressed.size(), type, true); } else { - area_.Create(uuid, NULL, 0, type); + area_.CreateInstance(customData, instance, uuid, NULL, 0, type, true); } } @@ -179,7 +180,7 @@ } return FileInfo(uuid, type, size, md5, - CompressionType_ZlibWithSize, compressed.size(), compressedMD5); + CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData); } default: @@ -187,13 +188,92 @@ } } - FileInfo StorageAccessor::Write(const std::string &data, - FileContentType type, - CompressionType compression, - bool storeMd5) + FileInfo StorageAccessor::WriteAttachment(std::string& customData, + const std::string& resourceId, + ResourceType resourceType, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const std::string& uuid) { - return Write((data.size() == 0 ? NULL : data.c_str()), - data.size(), type, compression, storeMd5); + std::string md5; + + if (storeMd5) + { + Toolbox::ComputeMD5(md5, data, size); + } + + switch (compression) + { + case CompressionType_None: + { + { + MetricsTimer timer(*this, METRICS_CREATE_DURATION); + area_.CreateAttachment(customData, resourceId, resourceType, uuid, data, size, type, false); + } + + if (metrics_ != NULL) + { + metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, size); + } + + if (cache_ != NULL) + { + StorageCache::Accessor cacheAccessor(*cache_); + cacheAccessor.Add(uuid, type, data, size); + } + + + return FileInfo(uuid, type, size, md5, customData); + } + + case CompressionType_ZlibWithSize: + { + ZlibCompressor zlib; + + std::string compressed; + zlib.Compress(compressed, data, size); + + std::string compressedMD5; + + if (storeMd5) + { + Toolbox::ComputeMD5(compressedMD5, compressed); + } + + { + MetricsTimer timer(*this, METRICS_CREATE_DURATION); + + if (compressed.size() > 0) + { + area_.CreateAttachment(customData, resourceId, resourceType, uuid, &compressed[0], compressed.size(), type, true); + } + else + { + area_.CreateAttachment(customData, resourceId, resourceType, uuid, NULL, 0, type, true); + } + } + + if (metrics_ != NULL) + { + metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, compressed.size()); + } + + if (cache_ != NULL) + { + StorageCache::Accessor cacheAccessor(*cache_); + cacheAccessor.Add(uuid, type, data, size); // always add uncompressed data to cache + } + + return FileInfo(uuid, type, size, md5, + CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData); + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } } @@ -238,7 +318,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData())); } if (metrics_ != NULL) @@ -259,7 +339,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - compressed.reset(area_.Read(info.GetUuid(), info.GetContentType())); + compressed.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData())); } if (metrics_ != NULL) @@ -318,7 +398,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData())); } if (metrics_ != NULL) @@ -331,7 +411,8 @@ void StorageAccessor::Remove(const std::string& fileUuid, - FileContentType type) + FileContentType type, + const std::string& customData) { if (cache_ != NULL) { @@ -340,14 +421,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()); } @@ -417,7 +498,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); } @@ -507,4 +588,21 @@ output.AnswerStream(transcoder); } #endif + + // bool StorageAccessor::HandlesCustomData() + // { + // return area_.HandlesCustomData(); + // } + + // void StorageAccessor::GetCustomData(std::string& customData, + // const std::string& uuid, + // const DicomInstanceToStore* instance, + // const void* content, + // size_t size, + // FileContentType type, + // bool compression) + // { + // area_.GetCustomData(customData, uuid, instance, content, size, type, compression); + // } + }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Tue Sep 24 11:39:52 2024 +0200 @@ -91,16 +91,47 @@ StorageCache& cache, MetricsRegistry& metrics); - FileInfo Write(const void* data, - size_t size, - FileContentType type, - CompressionType compression, - bool storeMd5); + // FileInfo Write(const void* data, + // size_t size, + // FileContentType type, + // CompressionType compression, + // bool storeMd5, + // const std::string& uuid, + // const std::string& customData); + + // FileInfo Write(const std::string& data, + // FileContentType type, + // CompressionType compression, + // bool storeMd5, + // const std::string& uuid, + // const std::string& customData); - FileInfo Write(const std::string& data, - FileContentType type, - CompressionType compression, - bool storeMd5); + FileInfo WriteInstance(std::string& customData, + const DicomInstanceToStore& instance, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const std::string& uuid); + + FileInfo WriteAttachment(std::string& customData, + const std::string& resourceId, + ResourceType resourceType, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const std::string& uuid); + + // FileInfo Write(std::string& customData, + // const std::string& data, + // FileContentType type, + // CompressionType compression, + // bool storeMd5, + // const std::string& uuid, + // const std::string& customData); void Read(std::string& content, const FileInfo& info); @@ -113,7 +144,8 @@ uint64_t end /* exclusive */); void Remove(const std::string& fileUuid, - FileContentType type); + FileContentType type, + const std::string& customData); void Remove(const FileInfo& info); @@ -134,6 +166,17 @@ const FileInfo& info, const std::string& mime); #endif + + bool HandlesCustomData(); + + // void GetCustomData(std::string& customData, + // const std::string& uuid, + // const DicomInstanceToStore* instance, + // const void* content, + // size_t size, + // FileContentType type, + // bool compression); + private: void ReadStartRangeInternal(std::string& target, const FileInfo& info,
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -174,7 +174,8 @@ StorageAccessor accessor(s, cache); std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true); + std::string uuid = Toolbox::GenerateUuid(); + FileInfo info = accessor.WriteAttachment(data, "", ResourceType_Instance, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, uuid); std::string r; accessor.Read(r, info); @@ -196,7 +197,8 @@ StorageAccessor accessor(s, cache); std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true); + std::string uuid = Toolbox::GenerateUuid(); + FileInfo info = accessor.WriteAttachment(data, "", ResourceType_Instance, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, uuid); std::string r; accessor.Read(r, info); @@ -207,6 +209,7 @@ ASSERT_EQ(FileContentType_Dicom, info.GetContentType()); ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5()); ASSERT_NE(info.GetUncompressedMD5(), info.GetCompressedMD5()); + ASSERT_EQ(uuid, info.GetUuid()); } @@ -220,9 +223,9 @@ std::string compressedData = "Hello"; 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.WriteAttachment(compressedData, "", ResourceType_Instance, compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, Toolbox::GenerateUuid()); + FileInfo uncompressedInfo = accessor.WriteAttachment(uncompressedData, "", ResourceType_Instance, uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, Toolbox::GenerateUuid()); + accessor.Read(r, compressedInfo); ASSERT_EQ(compressedData, r);
--- a/OrthancServer/CMakeLists.txt Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/CMakeLists.txt Tue Sep 24 11:39:52 2024 +0200 @@ -62,6 +62,7 @@ SET(BUILD_CONNECTIVITY_CHECKS ON CACHE BOOL "Whether to build the ConnectivityChecks plugin") SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin") SET(BUILD_DELAYED_DELETION ON CACHE BOOL "Whether to build the DelayedDeletion plugin") +SET(BUILD_ADVANCED_STORAGE ON CACHE BOOL "Whether to build the AdvancedStorage plugin") SET(BUILD_MULTITENANT_DICOM ON CACHE BOOL "Whether to build the MultitenantDicom plugin") SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins") SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") @@ -231,15 +232,16 @@ ##################################################################### set(ORTHANC_EMBEDDED_FILES - CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json - DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt - FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json - LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua - PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql - UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql - INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql - INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua + PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql + INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql + INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + INSTALL_REVISION_AND_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql ) if (STANDALONE_BUILD) @@ -485,7 +487,7 @@ if (ENABLE_PLUGINS AND (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER OR - BUILD_DELAYED_DELETION OR BUILD_MULTITENANT_DICOM)) + BUILD_DELAYED_DELETION OR BUILD_MULTITENANT_DICOM OR BUILD_ADVANCED_STORAGE)) set(PLUGINS_DEPENDENCIES_SOURCES ${BOOST_SOURCES} ${JSONCPP_SOURCES} @@ -797,6 +799,60 @@ ##################################################################### +## Build the "AdvancedStorage" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_ADVANCED_STORAGE) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} AdvancedStorage AdvancedStorage.dll "Orthanc plugin to provide advanced file storage" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/AdvancedStorage.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND ADVANCED_STORAGE_RESOURCES ${AUTOGENERATED_DIR}/AdvancedStorage.rc) + endif() + + set_source_files_properties( + ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/Plugin.cpp + PROPERTIES COMPILE_DEFINITIONS "ADVANCED_STORAGE_VERSION=\"${ORTHANC_VERSION}\"" + ) + + add_library(AdvancedStorage SHARED + ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/Plugin.cpp + ${ORTHANC} + ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/OrthancException.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/SystemToolbox.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Toolbox.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Logging.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/ChunkedBuffer.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Enumerations.cpp + ) + + DefineSourceBasenameForTarget(AdvancedStorage) + + target_link_libraries(AdvancedStorage PluginsDependencies) + + set_target_properties( + AdvancedStorage PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS AdvancedStorage + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + +##################################################################### ## Build the "MultitenantDicom" plugin #####################################################################
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -83,13 +83,15 @@ static FileInfo Convert(const OrthancPluginAttachment& attachment) { + std::string customData; return FileInfo(attachment.uuid, static_cast<FileContentType>(attachment.contentType), attachment.uncompressedSize, attachment.uncompressedHash, static_cast<CompressionType>(attachment.compressionType), attachment.compressedSize, - attachment.compressedHash); + attachment.compressedHash, + customData); }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -62,13 +62,15 @@ static FileInfo Convert(const OrthancPluginAttachment& attachment) { + std::string customData; return FileInfo(attachment.uuid, static_cast<FileContentType>(attachment.contentType), attachment.uncompressedSize, attachment.uncompressedHash, static_cast<CompressionType>(attachment.compressionType), attachment.compressedSize, - attachment.compressedHash); + attachment.compressedHash, + customData); }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -106,7 +106,8 @@ source.uncompressed_hash(), static_cast<CompressionType>(source.compression_type()), source.compressed_size(), - source.compressed_hash()); + source.compressed_hash(), + source.custom_data()); }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -59,6 +59,7 @@ #include "../../Sources/Database/VoidDatabaseListener.h" #include "../../Sources/OrthancConfiguration.h" #include "../../Sources/OrthancFindRequestHandler.h" +#include "../../Sources/Search/DatabaseConstraint.h" #include "../../Sources/Search/HierarchicalMatcher.h" #include "../../Sources/ServerContext.h" #include "../../Sources/ServerToolbox.h" @@ -79,6 +80,125 @@ namespace Orthanc { + class OrthancPlugins::IDicomInstance : public boost::noncopyable + { + public: + virtual ~IDicomInstance() + { + } + + virtual bool CanBeFreed() const = 0; + + virtual const DicomInstanceToStore& GetInstance() const = 0; + }; + + + class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance + { + private: + const DicomInstanceToStore& instance_; + + public: + explicit DicomInstanceFromCallback(const DicomInstanceToStore& instance) : + instance_(instance) + { + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return false; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance + { + private: + std::string buffer_; + std::unique_ptr<DicomInstanceToStore> instance_; + + void Setup(const void* buffer, + size_t size) + { + buffer_.assign(reinterpret_cast<const char*>(buffer), size); + + instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_)); + instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + + public: + DicomInstanceFromBuffer(const void* buffer, + size_t size) + { + Setup(buffer, size); + } + + explicit DicomInstanceFromBuffer(const std::string& buffer) + { + Setup(buffer.empty() ? NULL : buffer.c_str(), buffer.size()); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return *instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance + { + private: + std::unique_ptr<ParsedDicomFile> parsed_; + std::unique_ptr<DicomInstanceToStore> instance_; + + void Setup(ParsedDicomFile* parsed) + { + parsed_.reset(parsed); + + if (parsed_.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_)); + instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + } + + public: + explicit DicomInstanceFromParsed(IDicomTranscoder::DicomImage& transcoded) + { + Setup(transcoded.ReleaseAsParsedDicomFile()); + } + + explicit DicomInstanceFromParsed(ParsedDicomFile* parsed /* takes ownership */) + { + Setup(parsed); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return *instance_; + }; + }; + + class OrthancPlugins::WebDavCollection : public IWebDavBucket { private: @@ -550,8 +670,8 @@ } }; - - class StorageAreaBase : public IStorageArea + // "legacy" storage plugins don't store customData -> derive from ICoreStorageArea + class PluginStorageAreaBase : public ICoreStorageArea { private: OrthancPluginStorageCreate create_; @@ -605,9 +725,9 @@ } public: - StorageAreaBase(OrthancPluginStorageCreate create, - OrthancPluginStorageRemove remove, - PluginsErrorDictionary& errorDictionary) : + PluginStorageAreaBase(OrthancPluginStorageCreate create, + OrthancPluginStorageRemove remove, + PluginsErrorDictionary& errorDictionary) : create_(create), remove_(remove), errorDictionary_(errorDictionary) @@ -649,7 +769,7 @@ }; - class PluginStorageArea : public StorageAreaBase + class PluginStorageArea : public PluginStorageAreaBase { private: OrthancPluginStorageRead read_; @@ -666,7 +786,7 @@ public: PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks, PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), read_(callbacks.read), free_(callbacks.free) { @@ -715,7 +835,7 @@ // New in Orthanc 1.9.0 - class PluginStorageArea2 : public StorageAreaBase + class PluginStorageArea2 : public PluginStorageAreaBase { private: OrthancPluginStorageReadWhole readWhole_; @@ -724,7 +844,7 @@ public: PluginStorageArea2(const _OrthancPluginRegisterStorageArea2& callbacks, PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), readWhole_(callbacks.readWhole), readRange_(callbacks.readRange) { @@ -809,19 +929,252 @@ }; + // New in Orthanc 1.12.0 + class PluginStorageArea3 : public IStorageArea + { + private: + OrthancPluginStorageCreateInstance createInstance_; + OrthancPluginStorageCreateAttachment createAttachment_; + OrthancPluginStorageRemove2 remove2_; + OrthancPluginStorageReadWhole2 readWhole2_; + OrthancPluginStorageReadRange2 readRange2_; + + PluginsErrorDictionary& errorDictionary_; + + protected: + PluginsErrorDictionary& GetErrorDictionary() const + { + return errorDictionary_; + } + + IMemoryBuffer* RangeFromWhole(const std::string& uuid, + const std::string& customData, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */) + { + if (start > end) + { + throw OrthancException(ErrorCode_BadRange); + } + else if (start == end) + { + return new StringMemoryBuffer; // Empty + } + else + { + std::unique_ptr<IMemoryBuffer> whole(Read(uuid, type, customData)); + + if (start == 0 && + end == whole->GetSize()) + { + return whole.release(); + } + else if (end > whole->GetSize()) + { + throw OrthancException(ErrorCode_BadRange); + } + else + { + std::string range; + range.resize(end - start); + assert(!range.empty()); + + memcpy(&range[0], reinterpret_cast<const char*>(whole->GetData()) + start, range.size()); + + whole.reset(NULL); + return StringMemoryBuffer::CreateFromSwap(range); + } + } + } + + public: + PluginStorageArea3(const _OrthancPluginRegisterStorageArea3& callbacks, + PluginsErrorDictionary& errorDictionary) : + createInstance_(callbacks.createInstance), + createAttachment_(callbacks.createAttachment), + remove2_(callbacks.remove), + readWhole2_(callbacks.readWhole), + readRange2_(callbacks.readRange), + errorDictionary_(errorDictionary) + { + if (createInstance_ == NULL || + createAttachment_ == NULL || + remove2_ == NULL || + readWhole2_ == NULL) + { + throw OrthancException(ErrorCode_Plugin, "Storage area plugin doesn't implement all the required primitives (create, remove, readWhole"); + } + } + + virtual void CreateInstance(std::string& customData, + const DicomInstanceToStore& instance, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + bool isCompressed) ORTHANC_OVERRIDE + { + OrthancPluginMemoryBuffer customDataBuffer; + Orthanc::OrthancPlugins::DicomInstanceFromCallback wrapped(instance); + + OrthancPluginErrorCode error = createInstance_(&customDataBuffer, + uuid.c_str(), + reinterpret_cast<OrthancPluginDicomInstance*>(&wrapped), + content, size, Plugins::Convert(type), + isCompressed); + + if (error != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + + if (customDataBuffer.size > 0) + { + customData.assign(reinterpret_cast<char*>(customDataBuffer.data), + static_cast<size_t>(customDataBuffer.size)); + } + } + + virtual void CreateAttachment(std::string& customData, + const std::string& resourceId, + ResourceType resourceLevel, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + bool isCompressed) ORTHANC_OVERRIDE + { + OrthancPluginMemoryBuffer customDataBuffer; + + OrthancPluginErrorCode error = createAttachment_(&customDataBuffer, + uuid.c_str(), + resourceId.c_str(), + Plugins::Convert(resourceLevel), + content, size, Plugins::Convert(type), + isCompressed); + + if (error != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + + if (customDataBuffer.size > 0) + { + customData.assign(reinterpret_cast<char*>(customDataBuffer.data), + static_cast<size_t>(customDataBuffer.size)); + } + } + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + OrthancPluginErrorCode error = remove2_ + (uuid.c_str(), customData.c_str(), Plugins::Convert(type)); + + if (error != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + + virtual IMemoryBuffer* Read(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer); + + OrthancPluginMemoryBuffer64 buffer; + buffer.size = 0; + buffer.data = NULL; + + OrthancPluginErrorCode error = readWhole2_(&buffer, uuid.c_str(), customData.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 */, + uint64_t end /* exclusive */, + const std::string& customData) ORTHANC_OVERRIDE + { + if (readRange2_ == NULL) + { + return RangeFromWhole(uuid, customData, type, start, end); + } + else + { + if (start > end) + { + throw OrthancException(ErrorCode_BadRange); + } + else if (start == end) + { + return new StringMemoryBuffer; + } + else + { + std::string range; + range.resize(end - start); + assert(!range.empty()); + + OrthancPluginMemoryBuffer64 buffer; + buffer.data = &range[0]; + buffer.size = static_cast<uint64_t>(range.size()); + + OrthancPluginErrorCode error = + readRange2_(&buffer, uuid.c_str(), customData.c_str(), Plugins::Convert(type), start); + + if (error == OrthancPluginErrorCode_Success) + { + return StringMemoryBuffer::CreateFromSwap(range); + } + else + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + } + } + + virtual bool HasReadRange() const ORTHANC_OVERRIDE + { + return (readRange2_ != NULL); + } + + }; + + class StorageAreaFactory : public boost::noncopyable { private: enum Version { Version1, - Version2 + Version2, + Version3 }; SharedLibrary& sharedLibrary_; Version version_; _OrthancPluginRegisterStorageArea callbacks_; _OrthancPluginRegisterStorageArea2 callbacks2_; + _OrthancPluginRegisterStorageArea3 callbacks3_; PluginsErrorDictionary& errorDictionary_; static void WarnNoReadRange() @@ -855,6 +1208,20 @@ } } + StorageAreaFactory(SharedLibrary& sharedLibrary, + const _OrthancPluginRegisterStorageArea3& callbacks, + PluginsErrorDictionary& errorDictionary) : + sharedLibrary_(sharedLibrary), + version_(Version3), + callbacks3_(callbacks), + errorDictionary_(errorDictionary) + { + if (callbacks.readRange == NULL) + { + WarnNoReadRange(); + } + } + SharedLibrary& GetSharedLibrary() { return sharedLibrary_; @@ -870,6 +1237,9 @@ case Version2: return new PluginStorageArea2(callbacks2_, errorDictionary_); + case Version3: + return new PluginStorageArea3(callbacks3_, errorDictionary_); + default: throw OrthancException(ErrorCode_InternalError); } @@ -2493,125 +2863,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) @@ -5021,7 +5272,8 @@ { const _OrthancPluginStorageAreaCreate& p = *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); + PluginStorageAreaBase& storage = *reinterpret_cast<PluginStorageAreaBase*>(p.storageArea); + std::string customDataNotUsed; storage.Create(p.uuid, p.content, static_cast<size_t>(p.size), Plugins::Convert(p.type)); return true; } @@ -5031,7 +5283,8 @@ const _OrthancPluginStorageAreaRead& p = *reinterpret_cast<const _OrthancPluginStorageAreaRead*>(parameters); IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - std::unique_ptr<IMemoryBuffer> content(storage.Read(p.uuid, Plugins::Convert(p.type))); + std::string customDataNotUsed; + std::unique_ptr<IMemoryBuffer> content(storage.Read(p.uuid, Plugins::Convert(p.type), customDataNotUsed)); CopyToMemoryBuffer(*p.target, content->GetData(), content->GetSize()); return true; } @@ -5041,7 +5294,8 @@ const _OrthancPluginStorageAreaRemove& p = *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters); IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - storage.Remove(p.uuid, Plugins::Convert(p.type)); + std::string customDataNotUsed; + storage.Remove(p.uuid, Plugins::Convert(p.type), customDataNotUsed); return true; } @@ -5622,23 +5876,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); @@ -5761,7 +6026,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);
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Tue Sep 24 11:39:52 2024 +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);
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Tue Sep 24 11:39:52 2024 +0200 @@ -63,6 +63,7 @@ _OrthancPluginDatabaseAnswerType_DeletedAttachment = 1, _OrthancPluginDatabaseAnswerType_DeletedResource = 2, _OrthancPluginDatabaseAnswerType_RemainingAncestor = 3, + _OrthancPluginDatabaseAnswerType_DeletedAttachment2 = 4, /* Return value */ _OrthancPluginDatabaseAnswerType_Attachment = 10, @@ -75,6 +76,7 @@ _OrthancPluginDatabaseAnswerType_String = 17, _OrthancPluginDatabaseAnswerType_MatchingResource = 18, /* New in Orthanc 1.5.2 */ _OrthancPluginDatabaseAnswerType_Metadata = 19, /* New in Orthanc 1.5.4 */ + _OrthancPluginDatabaseAnswerType_Attachment2 = 20, /* New in Orthanc 1.12.0 */ _OrthancPluginDatabaseAnswerType_INTERNAL = 0x7fffffff } _OrthancPluginDatabaseAnswerType; @@ -93,6 +95,18 @@ typedef struct { + const char* uuid; + int32_t contentType; + uint64_t uncompressedSize; + const char* uncompressedHash; + int32_t compressionType; + uint64_t compressedSize; + const char* compressedHash; + const char* customData; + } OrthancPluginAttachment2; + + typedef struct + { uint16_t group; uint16_t element; const char* value; @@ -306,6 +320,19 @@ context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); } + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerAttachment2( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const OrthancPluginAttachment2* attachment) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_Attachment2; + params.valueGeneric = attachment; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerResource( OrthancPluginContext* context, OrthancPluginDatabaseContext* database, @@ -366,6 +393,19 @@ context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); } + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment2( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const OrthancPluginAttachment2* attachment) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_DeletedAttachment2; + params.valueGeneric = attachment; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedResource( OrthancPluginContext* context, OrthancPluginDatabaseContext* database, @@ -1361,7 +1401,343 @@ return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, ¶ms); } + + + // typedef struct + // { + // OrthancPluginDatabaseEventType type; + + // union + // { + // struct + // { + // /* For ""DeletedResource" and "RemainingAncestor" */ + // OrthancPluginResourceType level; + // const char* publicId; + // } resource; + + // /* For "DeletedAttachment" */ + // OrthancPluginAttachment2 attachment; + + // } content; + + // } OrthancPluginDatabaseEvent2; + + + // typedef struct + // { + // /** + // * Functions to read the answers inside a transaction + // **/ + + // OrthancPluginErrorCode (*readAnswersCount) (OrthancPluginDatabaseTransaction* transaction, + // uint32_t* target /* out */); + + // OrthancPluginErrorCode (*readAnswerAttachment2) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginAttachment2* target /* out */, // new in v4 + // uint32_t index); + + // OrthancPluginErrorCode (*readAnswerChange) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginChange* target /* out */, + // uint32_t index); + + // OrthancPluginErrorCode (*readAnswerDicomTag) (OrthancPluginDatabaseTransaction* transaction, + // uint16_t* group, + // uint16_t* element, + // const char** value, + // uint32_t index); + + // OrthancPluginErrorCode (*readAnswerExportedResource) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginExportedResource* target /* out */, + // uint32_t index); + + // OrthancPluginErrorCode (*readAnswerInt32) (OrthancPluginDatabaseTransaction* transaction, + // int32_t* target /* out */, + // uint32_t index); + + // OrthancPluginErrorCode (*readAnswerInt64) (OrthancPluginDatabaseTransaction* transaction, + // int64_t* target /* out */, + // uint32_t index); + + // OrthancPluginErrorCode (*readAnswerMatchingResource) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginMatchingResource* target /* out */, + // uint32_t index); + + // OrthancPluginErrorCode (*readAnswerMetadata) (OrthancPluginDatabaseTransaction* transaction, + // int32_t* metadata /* out */, + // const char** value /* out */, + // uint32_t index); + + // OrthancPluginErrorCode (*readAnswerString) (OrthancPluginDatabaseTransaction* transaction, + // const char** target /* out */, + // uint32_t index); + + // OrthancPluginErrorCode (*readEventsCount) (OrthancPluginDatabaseTransaction* transaction, + // uint32_t* target /* out */); + + // OrthancPluginErrorCode (*readEvent2) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginDatabaseEvent2* event /* out */, // new in v4 + // uint32_t index); + + + + // /** + // * Functions to access the global database object + // * (cf. "IDatabaseWrapper" class in Orthanc) + // **/ + + // OrthancPluginErrorCode (*open) (void* database); + + // OrthancPluginErrorCode (*close) (void* database); + + // OrthancPluginErrorCode (*destructDatabase) (void* database); + + // OrthancPluginErrorCode (*getDatabaseVersion) (void* database, + // uint32_t* target /* out */); + + // OrthancPluginErrorCode (*hasRevisionsSupport) (void* database, + // uint8_t* target /* out */); + + // OrthancPluginErrorCode (*hasAttachmentCustomDataSupport) (void* database, // new in v4 + // uint8_t* target /* out */); + + // OrthancPluginErrorCode (*upgradeDatabase) (void* database, + // OrthancPluginStorageArea* storageArea, + // uint32_t targetVersion); + + // OrthancPluginErrorCode (*startTransaction) (void* database, + // OrthancPluginDatabaseTransaction** target /* out */, + // OrthancPluginDatabaseTransactionType type); + + // OrthancPluginErrorCode (*destructTransaction) (OrthancPluginDatabaseTransaction* transaction); + + + // /** + // * Functions to run operations within a database transaction + // * (cf. "IDatabaseWrapper::ITransaction" class in Orthanc) + // **/ + + // OrthancPluginErrorCode (*rollback) (OrthancPluginDatabaseTransaction* transaction); + + // OrthancPluginErrorCode (*commit) (OrthancPluginDatabaseTransaction* transaction, + // int64_t fileSizeDelta); + + // /* A call to "addAttachment()" guarantees that this attachment is not already existing ("INSERT") */ + // OrthancPluginErrorCode (*addAttachment2) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id, + // const OrthancPluginAttachment2* attachment, // new in v4 + // int64_t revision); + + // OrthancPluginErrorCode (*clearChanges) (OrthancPluginDatabaseTransaction* transaction); + + // OrthancPluginErrorCode (*clearExportedResources) (OrthancPluginDatabaseTransaction* transaction); + + // OrthancPluginErrorCode (*clearMainDicomTags) (OrthancPluginDatabaseTransaction* transaction, + // int64_t resourceId); + + // OrthancPluginErrorCode (*createInstance) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginCreateInstanceResult* target /* out */, + // const char* hashPatient, + // const char* hashStudy, + // const char* hashSeries, + // const char* hashInstance); + + // OrthancPluginErrorCode (*deleteAttachment) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id, + // int32_t contentType); + + // OrthancPluginErrorCode (*deleteMetadata) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id, + // int32_t metadataType); + + // OrthancPluginErrorCode (*deleteResource) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id); + + // /* Answers are read using "readAnswerMetadata()" */ + // OrthancPluginErrorCode (*getAllMetadata) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id); + + // /* Answers are read using "readAnswerString()" */ + // OrthancPluginErrorCode (*getAllPublicIds) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginResourceType resourceType); + + // /* Answers are read using "readAnswerString()" */ + // OrthancPluginErrorCode (*getAllPublicIdsWithLimit) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginResourceType resourceType, + // uint64_t since, + // uint64_t limit); + + // /* Answers are read using "readAnswerChange()" */ + // OrthancPluginErrorCode (*getChanges) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* targetDone /* out */, + // int64_t since, + // uint32_t maxResults); + + // /* Answers are read using "readAnswerInt64()" */ + // OrthancPluginErrorCode (*getChildrenInternalId) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id); + + // /* Answers are read using "readAnswerString()" */ + // OrthancPluginErrorCode (*getChildrenMetadata) (OrthancPluginDatabaseTransaction* transaction, + // int64_t resourceId, + // int32_t metadata); + + // /* Answers are read using "readAnswerString()" */ + // OrthancPluginErrorCode (*getChildrenPublicId) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id); + + // /* Answers are read using "readAnswerExportedResource()" */ + // OrthancPluginErrorCode (*getExportedResources) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* targetDone /* out */, + // int64_t since, + // uint32_t maxResults); + + // /* Answer is read using "readAnswerChange()" */ + // OrthancPluginErrorCode (*getLastChange) (OrthancPluginDatabaseTransaction* transaction); + + // OrthancPluginErrorCode (*getLastChangeIndex) (OrthancPluginDatabaseTransaction* transaction, + // int64_t* target /* out */); + + // /* Answer is read using "readAnswerExportedResource()" */ + // OrthancPluginErrorCode (*getLastExportedResource) (OrthancPluginDatabaseTransaction* transaction); + + // /* Answers are read using "readAnswerDicomTag()" */ + // OrthancPluginErrorCode (*getMainDicomTags) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id); + + // /* Answer is read using "readAnswerString()" */ + // OrthancPluginErrorCode (*getPublicId) (OrthancPluginDatabaseTransaction* transaction, + // int64_t internalId); + + // OrthancPluginErrorCode (*getResourcesCount) (OrthancPluginDatabaseTransaction* transaction, + // uint64_t* target /* out */, + // OrthancPluginResourceType resourceType); + + // OrthancPluginErrorCode (*getResourceType) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginResourceType* target /* out */, + // uint64_t resourceId); + + // OrthancPluginErrorCode (*getTotalCompressedSize) (OrthancPluginDatabaseTransaction* transaction, + // uint64_t* target /* out */); + + // OrthancPluginErrorCode (*getTotalUncompressedSize) (OrthancPluginDatabaseTransaction* transaction, + // uint64_t* target /* out */); + + // OrthancPluginErrorCode (*isDiskSizeAbove) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* target /* out */, + // uint64_t threshold); + + // OrthancPluginErrorCode (*isExistingResource) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* target /* out */, + // int64_t resourceId); + + // OrthancPluginErrorCode (*isProtectedPatient) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* target /* out */, + // int64_t resourceId); + + // /* Answers are read using "readAnswerInt32()" */ + // OrthancPluginErrorCode (*listAvailableAttachments) (OrthancPluginDatabaseTransaction* transaction, + // int64_t internalId); + + // OrthancPluginErrorCode (*logChange) (OrthancPluginDatabaseTransaction* transaction, + // int32_t changeType, + // int64_t resourceId, + // OrthancPluginResourceType resourceType, + // const char* date); + + // OrthancPluginErrorCode (*logExportedResource) (OrthancPluginDatabaseTransaction* transaction, + // OrthancPluginResourceType resourceType, + // const char* publicId, + // const char* modality, + // const char* date, + // const char* patientId, + // const char* studyInstanceUid, + // const char* seriesInstanceUid, + // const char* sopInstanceUid); + + // /* Answer is read using "readAnswerAttachment()" */ + // OrthancPluginErrorCode (*lookupAttachment) (OrthancPluginDatabaseTransaction* transaction, + // int64_t* revision /* out */, + // int64_t resourceId, + // int32_t contentType); + + // /* Answer is read using "readAnswerString()" */ + // OrthancPluginErrorCode (*lookupGlobalProperty) (OrthancPluginDatabaseTransaction* transaction, + // const char* serverIdentifier, + // int32_t property); + + // /* Answer is read using "readAnswerString()" */ + // OrthancPluginErrorCode (*lookupMetadata) (OrthancPluginDatabaseTransaction* transaction, + // int64_t* revision /* out */, + // int64_t id, + // int32_t metadata); + + // OrthancPluginErrorCode (*lookupParent) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* isExisting /* out */, + // int64_t* parentId /* out */, + // int64_t id); + + // OrthancPluginErrorCode (*lookupResource) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* isExisting /* out */, + // int64_t* id /* out */, + // OrthancPluginResourceType* type /* out */, + // const char* publicId); + + // /* Answers are read using "readAnswerMatchingResource()" */ + // OrthancPluginErrorCode (*lookupResources) (OrthancPluginDatabaseTransaction* transaction, + // uint32_t constraintsCount, + // const OrthancPluginDatabaseConstraint* constraints, + // OrthancPluginResourceType queryLevel, + // uint32_t limit, + // uint8_t requestSomeInstanceId); + + // /* The public ID of the parent resource is read using "readAnswerString()" */ + // OrthancPluginErrorCode (*lookupResourceAndParent) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* isExisting /* out */, + // int64_t* id /* out */, + // OrthancPluginResourceType* type /* out */, + // const char* publicId); + + // OrthancPluginErrorCode (*selectPatientToRecycle) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* patientAvailable /* out */, + // int64_t* patientId /* out */); + + // OrthancPluginErrorCode (*selectPatientToRecycle2) (OrthancPluginDatabaseTransaction* transaction, + // uint8_t* patientAvailable /* out */, + // int64_t* patientId /* out */, + // int64_t patientIdToAvoid); + + // OrthancPluginErrorCode (*setGlobalProperty) (OrthancPluginDatabaseTransaction* transaction, + // const char* serverIdentifier, + // int32_t property, + // const char* value); + + // /* In "setMetadata()", the metadata might already be existing ("INSERT OR REPLACE") */ + // OrthancPluginErrorCode (*setMetadata) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id, + // int32_t metadata, + // const char* value, + // int64_t revision); + + // OrthancPluginErrorCode (*setProtectedPatient) (OrthancPluginDatabaseTransaction* transaction, + // int64_t id, + // uint8_t isProtected); + + // OrthancPluginErrorCode (*setResourcesContent) (OrthancPluginDatabaseTransaction* transaction, + // uint32_t countIdentifierTags, + // const OrthancPluginResourcesContentTags* identifierTags, + // uint32_t countMainDicomTags, + // const OrthancPluginResourcesContentTags* mainDicomTags, + // uint32_t countMetadata, + // const OrthancPluginResourcesContentMetadata* metadata); + + + // } OrthancPluginDatabaseBackendV4; + +/*<! @endcond */ + + #ifdef __cplusplus } #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue Sep 24 11:39:52 2024 +0200 @@ -490,6 +490,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.0 */ /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -558,7 +559,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, @@ -1362,7 +1363,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 + * @deprecated New plugins should use OrthancPluginStorageReadWhole2 and OrthancPluginStorageReadRange2 * * @warning The "content" buffer *must* have been allocated using * the "malloc()" function of your C standard library (i.e. nor @@ -1437,6 +1438,121 @@ + + /** + * @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 The custom data of the attachment (out) + * @param uuid The UUID of the file. + * @param instance The DICOM instance being stored. + * @param content The content of the file (might be compressed data, hence the need for the DICOM instance arg to access tags). + * @param size The size of the file. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCreateInstance) ( + OrthancPluginMemoryBuffer* customData, + const char* uuid, + const OrthancPluginDicomInstance* instance, + const void* content, + int64_t size, + OrthancPluginContentType type, + bool isCompressed); + + /** + * @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 The custom data of the attachment (out) + * @param uuid The UUID of the file. + * @param resourceId The resource ID the file is attached to. + * @param resourceType The resource Type the file is attached to. + * @param content The content of the file (might be compressed data, hence the need for the DICOM instance arg to access tags). + * @param size The size of the file. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCreateAttachment) ( + OrthancPluginMemoryBuffer* customData, + const char* uuid, + const char* resourceId, + OrthancPluginResourceType resourceType, + const void* content, + int64_t size, + OrthancPluginContentType type, + bool isCompressed); + + + + /** + * @brief Callback for reading a whole file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * reads a whole file from the storage area. + * + * @param target Memory buffer where to store the content of the file. It must be allocated by the + * plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it. + * @param uuid The UUID of the file of interest. + * @param customData The custom data of the file to be removed. + * @param type The content type corresponding to this file. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadWhole2) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + const char* customData, + OrthancPluginContentType type); + + + + /** + * @brief Callback for reading a range of a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * reads a portion of a file from the storage area. Orthanc + * indicates the start position and the length of the range. + * + * @param target Memory buffer where to store the content of the range. + * The memory buffer is allocated and freed by Orthanc. The length of the range + * of interest corresponds to the size of this buffer. + * @param uuid The UUID of the file of interest. + * @param customData The custom data of the file to be removed. + * @param type The content type corresponding to this file. + * @param rangeStart Start position of the requested range in the file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + const char* customData, + OrthancPluginContentType type, + uint64_t rangeStart); + + + + /** + * @brief Callback for removing a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area. + * + * @param uuid The UUID of the file to be removed. + * @param customData The custom data of the file to be removed. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) ( + const char* uuid, + const char* customData, + OrthancPluginContentType type); + + /** * @brief Callback to handle the C-Find SCP requests for worklists. * @@ -9359,6 +9475,50 @@ } + typedef struct + { + OrthancPluginStorageCreateInstance createInstance; + OrthancPluginStorageCreateAttachment createAttachment; + OrthancPluginStorageReadWhole2 readWhole; + OrthancPluginStorageReadRange2 readRange; + OrthancPluginStorageRemove2 remove; + } _OrthancPluginRegisterStorageArea3; + + /** + * @brief Register a custom storage area, with support for custom data. + * + * This function registers a custom storage area, to replace the + * built-in way Orthanc stores its files on the filesystem. This + * function must be called during the initialization of the plugin, + * i.e. inside the OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param create The callback function to store a file on the custom storage area. + * @param readWhole The callback function to read a whole file from the custom storage area. + * @param readRange The callback function to read some range of a file from the custom storage area. + * If this feature is not supported by the plugin, this value can be set to NULL. + * @param remove The callback function to remove a file from the custom storage area. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3( + OrthancPluginContext* context, + // OrthancPluginStorageGetCustomData getCustomData, + OrthancPluginStorageCreateInstance createInstance, + OrthancPluginStorageCreateAttachment createAttachement, + OrthancPluginStorageReadWhole2 readWhole, + OrthancPluginStorageReadRange2 readRange, + OrthancPluginStorageRemove2 remove) + { + _OrthancPluginRegisterStorageArea3 params; + // params.getCustomData = getCustomData; + params.createAttachment = createAttachement; + params.createInstance = createInstance; + params.readWhole = readWhole; + params.readRange = readRange; + params.remove = remove; + context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, ¶ms); + } + /** * @brief Signature of a callback function that is triggered when * the Orthanc core requests an operation from the database plugin. @@ -9565,7 +9725,6 @@ context->InvokeService(context, _OrthancPluginService_LogMessage, &m); } - #ifdef __cplusplus } #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Tue Sep 24 11:39:52 2024 +0200 @@ -55,6 +55,7 @@ int32 compression_type = 5; // opaque "CompressionType" in Orthanc uint64 compressed_size = 6; string compressed_hash = 7; + string custom_data = 8; // added in v 1.12.5 } enum ResourceType {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdvancedStorage/CMakeLists.txt Tue Sep 24 11:39:52 2024 +0200 @@ -0,0 +1,81 @@ +# 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/>. + + +cmake_minimum_required(VERSION 2.8) +cmake_policy(SET CMP0058 NEW) + +project(AdvancedStorage) + +SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin") + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) + +include(${CMAKE_CURRENT_LIST_DIR}/../Common/OrthancPluginsExports.cmake) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/WindowsResources.py + ${PLUGIN_VERSION} AdvancedStorage AdvancedStorage.dll "Orthanc plugin to extend Orthanc Storage" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/AdvancedStorage.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND ADDITIONAL_RESOURCES ${AUTOGENERATED_DIR}/AdvancedStorage.rc) +endif() + +add_definitions( + -DHAS_ORTHANC_EXCEPTION=1 + -DORTHANC_PLUGIN_VERSION="${PLUGIN_VERSION}" + ) + +include_directories( + ${CMAKE_SOURCE_DIR}/../../Include/ + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/ + ) + +add_library(AdvancedStorage SHARED + ${ADDITIONAL_RESOURCES} + ${AUTOGENERATED_SOURCES} + ${ORTHANC_CORE_SOURCES_DEPENDENCIES} + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Enumerations.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Logging.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/OrthancException.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SystemToolbox.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Toolbox.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp + Plugin.cpp + ) + +set_target_properties( + AdvancedStorage PROPERTIES + VERSION ${PLUGIN_VERSION} + SOVERSION ${PLUGIN_VERSION} + ) + +install( + TARGETS AdvancedStorage + DESTINATION . + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -0,0 +1,700 @@ +/** + * 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/>. + **/ + +#define ORTHANC_PLUGIN_NAME "advanced-storage" + +#include "../../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" +#include "../../../../OrthancFramework/Sources/Toolbox.h" +#include "../../../../OrthancFramework/Sources/Logging.h" +#include "../Common/OrthancPluginCppWrapper.h" + +#include <boost/filesystem.hpp> +#include <boost/filesystem/fstream.hpp> +#include <boost/iostreams/device/file_descriptor.hpp> +#include <boost/iostreams/stream.hpp> + + +#include <json/value.h> +#include <json/writer.h> +#include <string.h> +#include <iostream> +#include <algorithm> +#include <map> +#include <list> +#include <time.h> + +namespace fs = boost::filesystem; + +fs::path rootPath_; +bool multipleStoragesEnabled_ = false; +std::map<std::string, fs::path> rootPaths_; +std::string currentStorageId_; +std::string namingScheme_; +bool fsyncOnWrite_ = true; +size_t maxPathLength_ = 256; +size_t legacyPathLength = 39; // ex "/00/f7/00f7fd8b-47bd8c3a-ff917804-d180cdbc-40cf9527" + +fs::path GetRootPath() +{ + if (multipleStoragesEnabled_) + { + return rootPaths_[currentStorageId_]; + } + + return rootPath_; +} + +fs::path GetRootPath(const std::string& storageId) +{ + if (multipleStoragesEnabled_) + { + if (rootPaths_.find(storageId) == rootPaths_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Advanced Storage - storage '" + storageId + "' is not defined in configuration")); + } + return rootPaths_[storageId]; + } + + return rootPath_; +} + + +fs::path GetLegacyRelativePath(const std::string& uuid) +{ + if (!Orthanc::Toolbox::IsUuid(uuid)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + fs::path path; + + path /= std::string(&uuid[0], &uuid[2]); + path /= std::string(&uuid[2], &uuid[4]); + path /= uuid; + +#if BOOST_HAS_FILESYSTEM_V3 == 1 + path.make_preferred(); +#endif + + return path; +} + +fs::path GetPath(const std::string& uuid, const std::string& customDataString) +{ + fs::path path; + + if (!customDataString.empty()) + { + Json::Value customData; + Orthanc::Toolbox::ReadJson(customData, customDataString); + + if (customData["Version"].asInt() == 1) + { + if (customData.isMember("StorageId")) + { + path = GetRootPath(customData["StorageId"].asString()); + } + else + { + path = GetRootPath(); + } + + if (customData.isMember("Path")) + { + path /= customData["Path"].asString(); + } + else + { // we are in "legacy mode" for the path part + path /= GetLegacyRelativePath(uuid); + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Advanced Storage - unknown version for custom data '" + boost::lexical_cast<std::string>(customData["Version"].asInt()) + "'")); + } + } + else // we are in "legacy mode" + { + path = GetRootPath(); + path /= GetLegacyRelativePath(uuid); + } + + path.make_preferred(); + return path; +} + +void GetCustomData(std::string& output, const fs::path& path) +{ + // if we use defaults, non need to store anything in the metadata, the plugin has the same behavior as the core of Orthanc + if (namingScheme_ == "OrthancDefault" && !multipleStoragesEnabled_) + { + return; + } + + Json::Value customDataJson; + customDataJson["Version"] = 1; + + if (namingScheme_ != "OrthancDefault") + { // no need to store the pathc since we are in the default mode + customDataJson["Path"] = path.string(); + } + + if (multipleStoragesEnabled_) + { + customDataJson["StorageId"] = currentStorageId_; + } + + return Orthanc::Toolbox::WriteFastJson(output, customDataJson); +} + +void AddSplitDateDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL) +{ + if (tags.isMember(tagName) && tags[tagName].asString().size() == 8) + { + std::string date = tags[tagName].asString(); + path /= date.substr(0, 4); + path /= date.substr(4, 2); + path /= date.substr(6, 2); + } + else if (defaultValue != NULL) + { + path /= defaultValue; + } +} + +void AddStringDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL) +{ + if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0) + { + path /= tags[tagName].asString(); + } + else if (defaultValue != NULL) + { + path /= defaultValue; + } +} + +void AddIntDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, size_t zeroPaddingWidth = 0, const char* defaultValue = NULL) +{ + if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0) + { + std::string tagValue = tags[tagName].asString(); + if (zeroPaddingWidth > 0 && tagValue.size() < zeroPaddingWidth) + { + std::string padding(zeroPaddingWidth - tagValue.size(), '0'); + path /= padding + tagValue; + } + else + { + path /= tagValue; + } + } + else if (defaultValue != NULL) + { + path /= defaultValue; + } +} + +std::string GetExtension(OrthancPluginContentType type, bool isCompressed) +{ + std::string extension; + + switch (type) + { + case OrthancPluginContentType_Dicom: + extension = ".dcm"; + break; + case OrthancPluginContentType_DicomUntilPixelData: + extension = ".dcm.head"; + break; + default: + extension = ".unk"; + } + if (isCompressed) + { + extension = extension + ".cmp"; // compression is zlib + size -> we can not use the .zip extension + } + + return extension; +} + +fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed) +{ + fs::path path; + + if (!tags.isNull()) + { + if (namingScheme_ == "Preset1-StudyDatePatientID") + { + if (!tags.isMember("StudyDate")) + { + LOG(WARNING) << "AdvancedStorage - No 'StudyDate' in attachment " << uuid << ". Attachment will be stored in NO_STUDY_DATE folder"; + } + + AddSplitDateDicomTagToPath(path, tags, "StudyDate", "NO_STUDY_DATE"); + AddStringDicomTagToPath(path, tags, "PatientID"); // no default value, tag is always present if the instance is accepted by Orthanc + + if (tags.isMember("PatientName") && tags["PatientName"].isString() && !tags["PatientName"].asString().empty()) + { + path += std::string(" - ") + tags["PatientName"].asString(); + } + + AddStringDicomTagToPath(path, tags, "StudyDescription"); + AddStringDicomTagToPath(path, tags, "SeriesInstanceUID"); + + path /= uuid; + path += GetExtension(type, isCompressed); + return path; + } + } + + return GetLegacyRelativePath(uuid); +} + + +OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData, + const char* uuid, + const Json::Value& tags, + const void* content, + int64_t size, + OrthancPluginContentType type, + bool isCompressed) +{ + fs::path relativePath = GetRelativePathFromTags(tags, uuid, type, isCompressed); + std::string customDataString; + GetCustomData(customDataString, relativePath); + + fs::path rootPath = GetRootPath(); + fs::path path = rootPath / relativePath; + + LOG(INFO) << "Advanced Storage - creating attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path.string() + ")"; + + // check that the final path is not 'above' the root path (this could happen if e.g., a PatientName is ../../../../toto) + // fs::canonical() can not be used for that since the file needs to exist + // so far, we'll just forbid path containing '..' since they might be suspicious + if (path.string().find("..") != std::string::npos) + { + fs::path legacyPath = rootPath / GetLegacyRelativePath(uuid); + LOG(WARNING) << "Advanced Storage - WAS02 - Path is suspicious since it contains '..': '" << path.string() << "' will be stored in '" << legacyPath << "'"; + path = legacyPath; + } + + // check path length !!!!!, if too long, go back to legacy path and issue a warning + if (path.string().size() > maxPathLength_) + { + fs::path legacyPath = rootPath / GetLegacyRelativePath(uuid); + LOG(WARNING) << "Advanced Storage - WAS01 - Path is too long: '" << path.string() << "' will be stored in '" << legacyPath << "'"; + path = legacyPath; + } + + if (fs::exists(path)) + { + // Extremely unlikely case if uuid is included in the path: This Uuid has already been created + // in the past. + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Advanced Storage - path already exists"); + + // TODO for the future: handle duplicates path (e.g: there's no uuid in the path and we are uploading the same file again) + } + + if (fs::exists(path.parent_path())) + { + if (!fs::is_directory(path.parent_path())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_DirectoryOverFile); + } + } + else + { + if (!fs::create_directories(path.parent_path())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_FileStorageCannotWrite); + } + } + + Orthanc::SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_); + + OrthancPluginCreateMemoryBuffer(OrthancPlugins::GetGlobalContext(), customData, customDataString.size()); + memcpy(customData->data, customDataString.data(), customDataString.size()); + + return OrthancPluginErrorCode_Success; + +} + +OrthancPluginErrorCode StorageCreateInstance(OrthancPluginMemoryBuffer* customData, + const char* uuid, + const OrthancPluginDicomInstance* instance, + const void* content, + int64_t size, + OrthancPluginContentType type, + bool isCompressed) +{ + try + { + OrthancPlugins::DicomInstance dicomInstance(instance); + Json::Value tags; + dicomInstance.GetSimplifiedJson(tags); + + return StorageCreate(customData, uuid, tags, content, size, type, isCompressed); + } + catch (Orthanc::OrthancException& e) + { + return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_StorageAreaPlugin; + } + + return OrthancPluginErrorCode_Success; +} + + +OrthancPluginErrorCode StorageCreateAttachment(OrthancPluginMemoryBuffer* customData, + const char* uuid, + const char* resourceId, + OrthancPluginResourceType resourceType, + const void* content, + int64_t size, + OrthancPluginContentType type, + bool isCompressed) +{ + try + { + LOG(INFO) << "Creating attachment \"" << uuid << "\""; + + //TODO_CUSTOM_DATA: get tags from the Rest API... + Json::Value tags; + + return StorageCreate(customData, uuid, tags, content, size, type, isCompressed); + } + catch (Orthanc::OrthancException& e) + { + return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_StorageAreaPlugin; + } + + return OrthancPluginErrorCode_Success; +} + +OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target, + const char* uuid, + const char* customData, + OrthancPluginContentType type) +{ + std::string path = GetPath(uuid, customData).string(); + + LOG(INFO) << "Advanced Storage - Reading whole attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path + ")"; + + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + LOG(ERROR) << "The path does not point to a regular file: " << path; + return OrthancPluginErrorCode_InexistentFile; + } + + try + { + fs::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + LOG(ERROR) << "The path does not point to a regular file: " << path; + return OrthancPluginErrorCode_InexistentFile; + } + + // get file size + f.seekg(0, std::ios::end); + std::streamsize fileSize = f.tellg(); + f.seekg(0, std::ios::beg); + + // The ReadWhole must allocate the buffer itself + if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, fileSize) != OrthancPluginErrorCode_Success) + { + LOG(ERROR) << "Unable to allocate memory to read file: " << path; + return OrthancPluginErrorCode_NotEnoughMemory; + } + + if (fileSize != 0) + { + f.read(reinterpret_cast<char*>(target->data), fileSize); + } + + f.close(); + } + catch (...) + { + LOG(ERROR) << "Unexpected error while reading: " << path; + return OrthancPluginErrorCode_StorageAreaPlugin; + } + + return OrthancPluginErrorCode_Success; +} + + +OrthancPluginErrorCode StorageReadRange (OrthancPluginMemoryBuffer64* target, + const char* uuid, + const char* customData, + OrthancPluginContentType type, + uint64_t rangeStart) +{ + std::string path = GetPath(uuid, customData).string(); + + LOG(INFO) << "Advanced Storage - Reading range of attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path + ")"; + + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + LOG(ERROR) << "The path does not point to a regular file: " << path; + return OrthancPluginErrorCode_InexistentFile; + } + + try + { + fs::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + LOG(ERROR) << "The path does not point to a regular file: " << path; + return OrthancPluginErrorCode_InexistentFile; + } + + f.seekg(rangeStart, std::ios::beg); + + // The ReadRange uses a target that has already been allocated by orthanc + f.read(reinterpret_cast<char*>(target->data), target->size); + + f.close(); + } + catch (...) + { + LOG(ERROR) << "Unexpected error while reading: " << path; + return OrthancPluginErrorCode_StorageAreaPlugin; + } + + return OrthancPluginErrorCode_Success; +} + + +OrthancPluginErrorCode StorageRemove (const char* uuid, + const char* customData, + OrthancPluginContentType type) +{ + fs::path path = GetPath(uuid, customData); + + LOG(INFO) << "Advanced Storage - Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path.string() + ")"; + + try + { + fs::remove(path); + } + catch (...) + { + // Ignore the error + } + + // Remove the empty parent directories, (ignoring the error code if these directories are not empty) + + try + { + fs::path parent = path.parent_path(); + + while (parent != GetRootPath()) + { + fs::remove(parent); + parent = parent.parent_path(); + } + } + catch (...) + { + // Ignore the error + } + + return OrthancPluginErrorCode_Success; +} + +extern "C" +{ + + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) + { + OrthancPlugins::SetGlobalContext(context); + Orthanc::Logging::InitializePluginContext(context); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(context) == 0) + { + OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + return -1; + } + + LOG(WARNING) << "AdvancedStorage plugin is initializing"; + OrthancPluginSetDescription2(context, ORTHANC_PLUGIN_NAME, "Provides alternative layout for your storage."); + + OrthancPlugins::OrthancConfiguration orthancConfiguration; + + OrthancPlugins::OrthancConfiguration advancedStorage; + orthancConfiguration.GetSection(advancedStorage, "AdvancedStorage"); + + bool enabled = advancedStorage.GetBooleanValue("Enable", false); + if (enabled) + { + /* + { + "AdvancedStorage": { + + // Enables/disables the plugin + "Enable": false, + + // Enables/disables support for multiple StorageDirectories + "MultipleStorages" : { + "Storages" : { + // The storgae ids below may never change since they are stored in DB + // The storage path may change in case you move your data from one place to the other + "1" : "/var/lib/orthanc/db", + "2" : "/mnt/disk2/orthanc" + }, + + // the storage on which new data is stored. + // There's currently no automatic changes of disks + "CurrentStorage" : "2", + }, + + // Defines the storage structure and file namings. Right now, + // only the "OrthancDefault" value shall be used in a production environment. + // All other values are currently experimental + // "OrthancDefault" = same structure and file naming as default orthanc, + // "Preset1-StudyDatePatientID" = split(StudyDate)/PatientID - PatientName/StudyDescription/SeriesInstanceUID/uuid.ext + "NamingScheme" : "OrthancDefault", + + // Defines the maximum length for path used in the storage. If a file is longer + // than this limit, it is stored with the default orthanc naming scheme + // (and a warning is issued). + // Note, on Windows, the maximum path length is 260 bytes by default but can be increased + // through a configuration. + "MaxPathLength" : 256 + } + } + */ + + fsyncOnWrite_ = orthancConfiguration.GetBooleanValue("SyncStorageArea", true); + + const Json::Value& pluginJson = advancedStorage.GetJson(); + + namingScheme_ = advancedStorage.GetStringValue("NamingScheme", "OrthancDefault"); + + // if we have enabled multiple storage after files have been saved without this plugin, we still need the default StorageDirectory + rootPath_ = fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage")); + LOG(WARNING) << "AdvancedStorage - Path to the default storage area: " << rootPath_.string(); + + maxPathLength_ = orthancConfiguration.GetIntegerValue("MaxPathLength", 256); + LOG(WARNING) << "AdvancedStorage - Maximum path length: " << maxPathLength_; + + if (!rootPath_.is_absolute()) + { + LOG(ERROR) << "AdvancedStorage - Path to the default storage area should be an absolute path '" << rootPath_ << "'"; + return -1; + } + + if (rootPath_.size() > (maxPathLength_ - legacyPathLength)) + { + LOG(ERROR) << "AdvancedStorage - Path to the default storage is too long"; + return -1; + } + + if (pluginJson.isMember("MultipleStorages")) + { + multipleStoragesEnabled_ = true; + const Json::Value& multipleStoragesJson = pluginJson["MultipleStorages"]; + + if (multipleStoragesJson.isMember("Storages") && multipleStoragesJson.isObject() && multipleStoragesJson.isMember("CurrentStorage") && multipleStoragesJson["CurrentStorage"].isString()) + { + const Json::Value& storagesJson = multipleStoragesJson["Storages"]; + Json::Value::Members storageIds = storagesJson.getMemberNames(); + + for (Json::Value::Members::const_iterator it = storageIds.begin(); it != storageIds.end(); ++it) + { + const Json::Value& storagePath = storagesJson[*it]; + if (!storagePath.isString()) + { + LOG(ERROR) << "AdvancedStorage - Storage path is not a string " << *it; + return -1; + } + + rootPaths_[*it] = storagePath.asString(); + + if (!rootPaths_[*it].is_absolute()) + { + LOG(ERROR) << "AdvancedStorage - Storage path shall be absolute path '" << storagePath.asString() << "'"; + return -1; + } + + if (storagePath.asString().size() > (maxPathLength_ - legacyPathLength)) + { + LOG(ERROR) << "AdvancedStorage - Storage path is too long '" << storagePath.asString() << "'"; + return -1; + } + } + + currentStorageId_ = multipleStoragesJson["CurrentStorage"].asString(); + + if (rootPaths_.find(currentStorageId_) == rootPaths_.end()) + { + LOG(ERROR) << "AdvancedStorage - CurrentStorage is not defined in Storages list: " << currentStorageId_; + return -1; + } + + LOG(WARNING) << "AdvancedStorage - multiple storages enabled. Current storage : " << rootPaths_[currentStorageId_].string(); + } + } + + OrthancPluginRegisterStorageArea3(context, StorageCreateInstance, StorageCreateAttachment, StorageReadWhole, StorageReadRange, StorageRemove); + } + else + { + LOG(WARNING) << "AdvancedStorage plugin is disabled by the configuration file"; + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + LOG(WARNING) << "AdvancedStorage plugin is finalizing"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return ORTHANC_PLUGIN_NAME; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return ADVANCED_STORAGE_VERSION; + } +}
--- a/OrthancServer/Resources/Configuration.json Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Resources/Configuration.json Tue Sep 24 11:39:52 2024 +0200 @@ -15,11 +15,15 @@ // Path to the directory that holds the heavyweight files (i.e. the // raw DICOM instances). Backslashes must be either escaped by // doubling them, or replaced by forward slashes "/". + // If a relative path is provided, it is relative to the configuration + // file path. It is advised to provide an absolute path. "StorageDirectory" : "OrthancStorage", // Path to the directory that holds the SQLite index (if unset, the // value of StorageDirectory is used). This index could be stored on // a RAM-drive or a SSD device for performance reasons. + // If a relative path is provided, it is relative to the configuration + // file path. It is advised to provide an absolute path. "IndexDirectory" : "OrthancStorage", // Path to the directory where Orthanc stores its large temporary
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/DowngradeFrom7to6.sql Tue Sep 24 11:39:52 2024 +0200 @@ -0,0 +1,45 @@ +-- 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 updates the version of the Orthanc database from 6 to 7. +-- + +-- Add a new column to AttachedFiles + +ALTER TABLE AttachedFiles DROP COLUMN metadata; + +-- update the triggers (back to v6) +DROP TRIGGER AttachedFileDeleted + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize, + old.uncompressedMD5, old.compressedMD5); +END; + + +-- Change the database version +-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration + +UPDATE GlobalProperties SET value="6" WHERE property=1; +
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Sep 24 11:39:52 2024 +0200 @@ -52,6 +52,7 @@ bool hasAtomicIncrementGlobalProperty_; bool hasUpdateAndGetStatistics_; bool hasMeasureLatency_; + bool hasAttachmentCustomDataSupport_; public: Capabilities() : @@ -60,7 +61,8 @@ hasLabelsSupport_(false), hasAtomicIncrementGlobalProperty_(false), hasUpdateAndGetStatistics_(false), - hasMeasureLatency_(false) + hasMeasureLatency_(false), + hasAttachmentCustomDataSupport_(false) { } @@ -94,6 +96,16 @@ return hasLabelsSupport_; } + void SetAttachmentCustomDataSupport(bool value) + { + hasAttachmentCustomDataSupport_ = value; + } + + bool HasAttachmentCustomDataSupport() const + { + return hasAttachmentCustomDataSupport_; + } + void SetAtomicIncrementGlobalProperty(bool value) { hasAtomicIncrementGlobalProperty_ = value;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql Tue Sep 24 11:39:52 2024 +0200 @@ -0,0 +1,66 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2022 Osimis S.A., Belgium +-- Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +-- +-- This program is free software: you can redistribute it and/or +-- modify it under the terms of the GNU General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. + + +-- +-- This SQLite script installs revision and customData without changing the Orthanc database version +-- + +-- Add new columns for revision +ALTER TABLE Metadata ADD COLUMN revision INTEGER; +ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER; + +-- Add new column for customData +ALTER TABLE AttachedFiles ADD COLUMN customData TEXT; + + +-- add another AttachedFileDeleted trigger +-- We want to keep backward compatibility and avoid changing the database version number (which would force +-- users to upgrade the DB). By keeping backward compatibility, we mean "allow a user to run a previous Orthanc +-- version after it has run this update script". +-- We must keep the signature of the initial trigger (it is impossible to have 2 triggers on the same event). +-- We tried adding a trigger on "BEFORE DELETE" but then it is being called when running the previous Orthanc +-- which makes it fail. +-- But, we need the customData in the C++ function that is called when a AttachedFiles is deleted. +-- The trick is then to save the customData in a DeletedFiles table. +-- The SignalFileDeleted C++ function will then get the customData from this table and delete the entry. +-- Drawback: if you downgrade Orthanc, the DeletedFiles table will remain and will be populated by the trigger +-- but not consumed by the C++ function -> we consider this is an acceptable drawback for a few people compared +-- to the burden of upgrading the DB. + +CREATE TABLE DeletedFiles( + uuid TEXT NOT NULL, -- 0 + customData TEXT -- 1 +); + +DROP TRIGGER AttachedFileDeleted; + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + INSERT INTO DeletedFiles VALUES(old.uuid, old.customData); + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize, + old.uncompressedMD5, old.compressedMD5 + ); +END; + +-- Record that this upgrade has been performed + +INSERT INTO GlobalProperties VALUES (7, 1); -- GlobalProperty_SQLiteHasCustomDataAndRevision
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Tue Sep 24 11:39:52 2024 +0200 @@ -52,6 +52,7 @@ id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, type INTEGER, value TEXT, + -- revision INTEGER, -- New in Orthanc 1.12.0 (added in InstallRevisionAndCustomData.sql) PRIMARY KEY(id, type) ); @@ -64,6 +65,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.0 (added in InstallRevisionAndCustomData.sql) + -- customData TEXT, -- New in Orthanc 1.12.0 (added in InstallRevisionAndCustomData.sql) PRIMARY KEY(id, fileType) ); @@ -126,7 +129,8 @@ SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, old.compressionType, old.compressedSize, -- These 2 arguments are new in Orthanc 0.7.3 (database v4) - old.uncompressedMD5, old.compressedMD5); + old.uncompressedMD5, old.compressedMD5 + ); END; CREATE TRIGGER ResourceDeleted
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -40,6 +40,8 @@ #include <stdio.h> #include <boost/lexical_cast.hpp> +static std::map<std::string, std::string> filesToDeleteCustomData; + namespace Orthanc { class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter @@ -324,8 +326,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()); @@ -334,10 +337,11 @@ s.BindInt(5, attachment.GetCompressionType()); s.BindString(6, attachment.GetUncompressedMD5()); s.BindString(7, attachment.GetCompressedMD5()); + s.BindInt(8, revision); + s.BindString(9, attachment.GetCustomData()); s.Run(); } - virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, const DatabaseConstraints& lookup, @@ -479,6 +483,28 @@ } } + void DeleteDeletedFile(const std::string& uuid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DeletedFiles WHERE uuid=?"); + s.BindString(0, uuid); + s.Run(); + } + + void GetDeletedFileCustomData(std::string& customData, const std::string& uuid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT customData FROM DeletedFiles WHERE uuid=?"); + s.BindString(0, uuid); + + if (s.Step()) + { + customData = s.ColumnString(0); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } virtual void GetAllMetadata(std::map<MetadataType, std::string>& target, int64_t id) ORTHANC_OVERRIDE @@ -798,7 +824,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); @@ -814,8 +840,9 @@ s.ColumnString(4), static_cast<CompressionType>(s.ColumnInt(2)), s.ColumnInt64(3), - s.ColumnString(5)); - revision = 0; // TODO - REVISIONS + s.ColumnString(5), + s.ColumnString(7)); + revision = s.ColumnInt(6); return true; } } @@ -850,7 +877,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); @@ -861,7 +888,7 @@ else { target = s.ColumnString(0); - revision = 0; // TODO - REVISIONS + revision = s.ColumnInt(1); return true; } } @@ -1033,11 +1060,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(); } @@ -1166,6 +1193,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)) @@ -1184,9 +1216,11 @@ uncompressedMD5, static_cast<CompressionType>(context.GetIntValue(3)), static_cast<uint64_t>(context.GetInt64Value(4)), - compressedMD5); + compressedMD5, + customData); sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info); + sqlite_.activeTransaction_->DeleteDeletedFile(id); } } }; @@ -1439,6 +1473,19 @@ } } + // New in Orthanc 1.12.5 + if (version_ >= 6) + { + if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasCustomDataAndRevision, true /* unused in SQLite */) + || tmp != "1") + { + LOG(INFO) << "Upgrading SQLite schema to support revision and customData"; + std::string query; + ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA); + db_.Execute(query); + } + } + transaction->Commit(0); } } @@ -1469,7 +1516,7 @@ { boost::mutex::scoped_lock lock(mutex_); - if (targetVersion != 6) + if (targetVersion != 7) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } @@ -1479,7 +1526,8 @@ if (version_ != 3 && version_ != 4 && version_ != 5 && - version_ != 6) + version_ != 6 && + version_ != 7) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } @@ -1520,6 +1568,7 @@ version_ = 6; } + }
--- a/OrthancServer/Sources/OrthancInitialization.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -442,7 +442,7 @@ { // Anonymous namespace to avoid clashes between compilation modules - class FilesystemStorageWithoutDicom : public IStorageArea + class FilesystemStorageWithoutDicom : public ICoreStorageArea { private: FilesystemStorage storage_;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -2610,6 +2610,7 @@ std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); + ResourceType resourceType = StringToResourceType(call.GetFullUri()[0].c_str()); FileContentType contentType = StringToContentType(name); if (IsUserContentType(contentType) || // It is forbidden to modify internal attachments... @@ -2634,7 +2635,7 @@ } int64_t newRevision; - context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(), + context.AddAttachment(newRevision, publicId, resourceType, StringToContentType(name), call.GetBodyData(), call.GetBodySize(), hasOldRevision, oldRevision, oldMD5); SetBufferContentETag(call.GetOutput(), newRevision, call.GetBodyData(), call.GetBodySize()); // New in Orthanc 1.9.2 @@ -2755,9 +2756,10 @@ std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); + ResourceType resourceType = StringToResourceType(call.GetFullUri()[0].c_str()); FileContentType contentType = StringToContentType(name); - OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression); + OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, resourceType, contentType, compression); call.GetOutput().AnswerBuffer("{}", MimeType_Json); }
--- a/OrthancServer/Sources/ServerContext.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -579,10 +579,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); } @@ -693,8 +694,11 @@ // 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_); + std::string dicomCustomData; + std::string dicomUuid = Toolbox::GenerateUuid(); + + FileInfo dicomInfo = accessor.WriteInstance(dicomCustomData, dicom, dicom.GetBufferData(), dicom.GetBufferSize(), + FileContentType_Dicom, compression, storeMD5_, dicomUuid); ServerIndex::Attachments attachments; attachments.push_back(dicomInfo); @@ -704,8 +708,11 @@ (!area_.HasReadRange() || compressionEnabled_)) { - dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, - FileContentType_DicomUntilPixelData, compression, storeMD5_); + std::string dicomHeaderCustomData; + std::string dicomHeaderUuid = Toolbox::GenerateUuid(); + + dicomUntilPixelData = accessor.WriteInstance(dicomHeaderCustomData, dicom, dicom.GetBufferData(), pixelDataOffset, + FileContentType_DicomUntilPixelData, compression, storeMD5_, dicomHeaderUuid); attachments.push_back(dicomUntilPixelData); } @@ -976,6 +983,7 @@ void ServerContext::ChangeAttachmentCompression(const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, CompressionType compression) { @@ -1002,8 +1010,25 @@ StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); accessor.Read(content, attachment); - FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(), - content.size(), attachmentType, compression, storeMD5_); + std::string newUuid = Toolbox::GenerateUuid(); + std::string newCustomData; + FileInfo modified; + + // if (attachmentType == FileContentType_Dicom || attachmentType == FileContentType_DicomUntilPixelData) + // { + // // DicomInstanceToStore instance; + // // TODO_CUSTOM_DATA: get the Instance such that we can call accessor.GetCustomData ... + // // modified = accessor.WriteInstance(newCustomData, instance, content.empty() ? NULL : content.c_str(), + // // content.size(), attachmentType, compression, storeMD5_, newUuid); + // } + // else + { + ResourceType resourceType = ResourceType_Instance; //TODO_CUSTOM_DATA: get it from above in the stack + modified = accessor.WriteAttachment(newCustomData, resourceId, resourceType, content.empty() ? NULL : content.c_str(), + content.size(), attachmentType, compression, storeMD5_, newUuid); + } + + try { @@ -1189,9 +1214,9 @@ compressionEnabled_) { int64_t newRevision; - AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData, + AddAttachment(newRevision, instancePublicId, ResourceType_Instance, FileContentType_DicomUntilPixelData, dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset, - false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); + false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); } } } @@ -1414,6 +1439,7 @@ bool ServerContext::AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -1427,7 +1453,13 @@ CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_); + + std::string uuid = Toolbox::GenerateUuid(); + std::string customData; + + assert(attachmentType != FileContentType_Dicom && attachmentType != FileContentType_DicomUntilPixelData); // this method can not be used to store instances + + FileInfo attachment = accessor.WriteAttachment(customData, resourceId, resourceType, data, size, attachmentType, compression, storeMD5_, uuid); try {
--- a/OrthancServer/Sources/ServerContext.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.h Tue Sep 24 11:39:52 2024 +0200 @@ -285,7 +285,8 @@ // This method must only be called from "ServerIndex"! void RemoveFile(const std::string& fileUuid, - FileContentType type); + FileContentType type, + const std::string& customData); // This DicomModification object is intended to be used as a // "rules engine" when de-identifying logs for C-Find, C-Get, and @@ -347,6 +348,7 @@ bool AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -368,6 +370,7 @@ FileContentType content); void ChangeAttachmentCompression(const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, CompressionType compression);
--- a/OrthancServer/Sources/ServerEnumerations.h Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Tue Sep 24 11:39:52 2024 +0200 @@ -126,6 +126,7 @@ GlobalProperty_AnonymizationSequence = 3, GlobalProperty_JobsRegistry = 5, GlobalProperty_GetTotalSizeIsFast = 6, // New in Orthanc 1.5.2 + GlobalProperty_SQLiteHasCustomDataAndRevision = 7, // New in Orthanc 1.12.0 GlobalProperty_Modalities = 20, // New in Orthanc 1.5.0 GlobalProperty_Peers = 21, // New in Orthanc 1.5.0
--- a/OrthancServer/Sources/ServerIndex.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Tue Sep 24 11:39:52 2024 +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/UnitTestsSources/ServerIndexTests.cpp Tue Sep 24 09:26:42 2024 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -294,11 +294,10 @@ ASSERT_EQ(0u, md.size()); transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", - CompressionType_ZlibWithSize, 21, "compressedMD5"), 42); - transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"), 43); - transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"), 44); + CompressionType_ZlibWithSize, 21, "compressedMD5", "customData"), 42); + transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5", "customData"), 43); + transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5", "customData"), 44); - // TODO - REVISIONS - "42" is revision number, that is not currently stored (*) transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42); transaction_->GetAllMetadata(md, a[4]); @@ -337,17 +336,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)); @@ -355,7 +354,7 @@ FileInfo att; ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(42, revision); ASSERT_EQ("my json file", att.GetUuid()); ASSERT_EQ(21u, att.GetCompressedSize()); ASSERT_EQ("md5", att.GetUncompressedMD5()); @@ -364,7 +363,7 @@ ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType()); ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(44, revision); ASSERT_EQ("world", att.GetUuid()); ASSERT_EQ(44u, att.GetCompressedSize()); ASSERT_EQ("md5", att.GetUncompressedMD5()); @@ -400,7 +399,7 @@ CheckTableRecordCount(0, "Resources"); CheckTableRecordCount(0, "AttachedFiles"); - CheckTableRecordCount(3, "GlobalProperties"); + CheckTableRecordCount(4, "GlobalProperties"); std::string tmp; ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true)); @@ -476,7 +475,7 @@ std::string p = "Patient " + boost::lexical_cast<std::string>(i); patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast<std::string>(i)), 42); + "md5-" + boost::lexical_cast<std::string>(i), "customData"), 42); ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } @@ -537,7 +536,7 @@ std::string p = "Patient " + boost::lexical_cast<std::string>(i); patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast<std::string>(i)), 42); + "md5-" + boost::lexical_cast<std::string>(i), "customData"), 42); ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } @@ -780,7 +779,7 @@ for (size_t i = 0; i < ids.size(); i++) { - FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5"); + FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5", "customData"); int64_t revision = -1; index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1, ""); ASSERT_EQ(0, revision);
--- a/TODO Tue Sep 24 09:26:42 2024 +0200 +++ b/TODO Tue Sep 24 11:39:52 2024 +0200 @@ -1,3 +1,36 @@ +TODO_CUSTOM_DATA branch +- add REVISIONS in AttachedFiles + Metadata in SQLite since we change the DB schema +- add revisions and customData in all Database plugins +- check if we can play with GlobalProperty_DatabasePatchLevel instead of upgrading the DB version ! +- upgrade DB automatically such that it does not need a specific launch with --update , add --downgrade + --no-auto-upgrade command lines +- refuse to instantiate a PluginStorage3 if a DBv4 is not instantiated ! +- handle all TODO_CUSTOM_DATA +- check /attachments/... routes for path returned +- AdvancedStoragePlugin + - show warning if a tag is missing when generating the path from tags (+ option to disable this warning) + - generate path from tags from resource (CreateAttachment) + - add an instanceId or parentSeriesId arg in CreateInstance ? + - implement a 'legacy' root path to group all files with missing tags or path too long + - avoid error AdvancedStorage - Path to the default storage area should be an absolute path '"OrthancStorage"' when using PG and no StorageDirectory has been defined + - document that, once you have used the AdvancedStoragePlugin and stored DICOM files, you can not downgrade Orthanc to a previous Orthanc + without loosing access to the DICOM files + - write integration test for advanced-storage: + - launch 1.11.2 + - upload 1 file + - launch 1.12.0 with advanced-storage plugin with a non default namingScheme + - upload 1 file + - access + delete initial file + +- write integration test for transitions from one DB to the other (for each DB plugin): + - launch 1.11.2, + - upload 2 files, + - launch 1.12.0, + - access + delete one file, + - upload one file, + - launch 1.11.2 again, + - access + delete last 2 files + + ======================= === Orthanc Roadmap === =======================