# HG changeset patch # User Alain Mazy # Date 1662651728 -7200 # Node ID d7274e43ea7c8ffbc54f8e70916a0d192bc1dfe4 # Parent 4366b4c414411b6320b35c3793f69489f54e83c0 allow plugins to store a customData in the Attachments table to e.g. store custom paths without requiring an external DB diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Thu Sep 08 17:42:08 2022 +0200 @@ -32,8 +32,9 @@ # * 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 -set(ORTHANC_DATABASE_VERSION 6) +# * Orthanc 0.9.5 -> Orthanc 1.11.X = version 6 +# * Orthanc 1.12.0 -> mainline = version 7 +set(ORTHANC_DATABASE_VERSION 7) # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/Sources/FileStorage/FileInfo.cpp --- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -41,7 +41,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), @@ -49,7 +50,8 @@ uncompressedMD5_(md5), compressionType_(CompressionType_None), compressedSize_(size), - compressedMD5_(md5) + compressedMD5_(md5), + customData_(customData) { } @@ -60,7 +62,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), @@ -68,7 +71,8 @@ uncompressedMD5_(uncompressedMD5), compressionType_(compressionType), compressedSize_(compressedSize), - compressedMD5_(compressedMD5) + compressedMD5_(compressedMD5), + customData_(customData) { } @@ -168,4 +172,16 @@ throw OrthancException(ErrorCode_BadSequenceOfCalls); } } + + const std::string& FileInfo::GetCustomData() const + { + if (valid_) + { + return customData_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } } diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/Sources/FileStorage/FileInfo.h --- a/OrthancFramework/Sources/FileStorage/FileInfo.h Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.h Thu Sep 08 17:42:08 2022 +0200 @@ -41,6 +41,7 @@ CompressionType compressionType_; uint64_t compressedSize_; std::string compressedMD5_; + std::string customData_; public: FileInfo(); @@ -51,7 +52,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. @@ -62,7 +64,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; @@ -79,5 +82,7 @@ const std::string& GetCompressedMD5() const; const std::string& GetUncompressedMD5() const; + + const std::string& GetCustomData() const; }; } diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp --- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -242,6 +242,7 @@ { namespace fs = boost::filesystem; typedef std::set List; + std::string customDataNotUsed; List result; ListAllFiles(result); diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/Sources/FileStorage/FilesystemStorage.h --- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Thu Sep 08 17:42:08 2022 +0200 @@ -42,7 +42,7 @@ namespace Orthanc { - class ORTHANC_PUBLIC FilesystemStorage : public IStorageArea + class ORTHANC_PUBLIC FilesystemStorage : public ICoreStorageArea { // TODO REMOVE THIS friend class FilesystemHttpSender; diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/Sources/FileStorage/IStorageArea.h --- a/OrthancFramework/Sources/FileStorage/IStorageArea.h Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h Thu Sep 08 17:42:08 2022 +0200 @@ -32,6 +32,8 @@ namespace Orthanc { + class DicomInstanceToStore; + class IStorageArea : public boost::noncopyable { public: @@ -39,8 +41,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; @@ -52,9 +138,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; + }; } diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/Sources/FileStorage/MemoryStorageArea.h --- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Thu Sep 08 17:42:08 2022 +0200 @@ -32,7 +32,7 @@ namespace Orthanc { - class MemoryStorageArea : public IStorageArea + class MemoryStorageArea : public ICoreStorageArea { private: typedef std::map Content; @@ -43,6 +43,7 @@ public: virtual ~MemoryStorageArea(); + protected: virtual void Create(const std::string& uuid, const void* content, size_t size, diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/Sources/FileStorage/StorageAccessor.cpp --- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -79,14 +79,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) @@ -100,14 +101,14 @@ { MetricsTimer timer(*this, METRICS_CREATE); - area_.Create(uuid, data, size, type); + area_.CreateInstance(customData, instance, uuid, data, size, type, false); if (cache_ != NULL) { cache_->Add(uuid, type, data, size); } - return FileInfo(uuid, type, size, md5); + return FileInfo(uuid, type, size, md5, customData); } case CompressionType_ZlibWithSize: @@ -129,11 +130,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); } } @@ -143,7 +144,7 @@ } return FileInfo(uuid, type, size, md5, - CompressionType_ZlibWithSize, compressed.size(), compressedMD5); + CompressionType_ZlibWithSize, compressed.size(), compressedMD5, customData); } default: @@ -151,13 +152,78 @@ } } - 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); + + area_.CreateAttachment(customData, resourceId, resourceType, uuid, data, size, type, false); + + if (cache_ != NULL) + { + cache_->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); + + 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 (cache_ != NULL) + { + cache_->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); + } } @@ -172,7 +238,7 @@ case CompressionType_None: { MetricsTimer timer(*this, METRICS_READ); - std::unique_ptr buffer(area_.Read(info.GetUuid(), info.GetContentType())); + std::unique_ptr buffer(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData())); buffer->MoveToString(content); break; @@ -186,7 +252,7 @@ { MetricsTimer timer(*this, METRICS_READ); - compressed.reset(area_.Read(info.GetUuid(), info.GetContentType())); + compressed.reset(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData())); } zlib.Uncompress(content, compressed->GetData(), compressed->GetSize()); @@ -217,14 +283,15 @@ if (cache_ == NULL || !cache_->Fetch(content, info.GetUuid(), info.GetContentType())) { MetricsTimer timer(*this, METRICS_READ); - std::unique_ptr buffer(area_.Read(info.GetUuid(), info.GetContentType())); + std::unique_ptr buffer(area_.Read(info.GetUuid(), info.GetContentType(), info.GetCustomData())); buffer->MoveToString(content); } } void StorageAccessor::Remove(const std::string& fileUuid, - FileContentType type) + FileContentType type, + const std::string& customData) { if (cache_ != NULL) { @@ -233,26 +300,27 @@ { MetricsTimer timer(*this, METRICS_REMOVE); - 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()); } void StorageAccessor::ReadStartRange(std::string& target, const std::string& fileUuid, FileContentType contentType, - uint64_t end /* exclusive */) + uint64_t end /* exclusive */, + const std::string& customData) { if (cache_ == NULL || !cache_->FetchStartRange(target, fileUuid, contentType, end)) { MetricsTimer timer(*this, METRICS_READ); - std::unique_ptr buffer(area_.ReadRange(fileUuid, contentType, 0, end)); + std::unique_ptr buffer(area_.ReadRange(fileUuid, contentType, 0, end, customData)); assert(buffer->GetSize() == end); buffer->MoveToString(target); @@ -341,4 +409,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); + // } + } diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/Sources/FileStorage/StorageAccessor.h --- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Thu Sep 08 17:42:08 2022 +0200 @@ -85,16 +85,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); @@ -105,10 +136,12 @@ void ReadStartRange(std::string& target, const std::string& fileUuid, FileContentType fullFileContentType, - uint64_t end /* exclusive */); + uint64_t end /* exclusive */, + const std::string& customData); void Remove(const std::string& fileUuid, - FileContentType type); + FileContentType type, + const std::string& customData); void Remove(const FileInfo& info); @@ -129,5 +162,15 @@ 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); }; } diff -r 4366b4c41441 -r d7274e43ea7c OrthancFramework/UnitTestsSources/FileStorageTests.cpp --- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -130,7 +130,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); @@ -152,7 +153,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); @@ -163,6 +165,7 @@ ASSERT_EQ(FileContentType_Dicom, info.GetContentType()); ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5()); ASSERT_NE(info.GetUncompressedMD5(), info.GetCompressedMD5()); + ASSERT_EQ(uuid, info.GetUuid()); } @@ -176,9 +179,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); diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/CMakeLists.txt Thu Sep 08 17:42:08 2022 +0200 @@ -61,6 +61,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(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins") SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") @@ -228,6 +229,7 @@ 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 + UPGRADE_DATABASE_6_TO_7 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade6To7.sql INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql @@ -318,6 +320,7 @@ -DMODALITY_WORKLISTS_VERSION="${ORTHANC_VERSION}" -DSERVE_FOLDERS_VERSION="${ORTHANC_VERSION}" -DHOUSEKEEPER_VERSION="${ORTHANC_VERSION}" + -DADVANCED_STORAGE_VERSION="${ORTHANC_VERSION}" ) @@ -430,7 +433,7 @@ ##################################################################### if (ENABLE_PLUGINS AND - (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER)) + (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER OR BUILD_ADVANCED_STORAGE)) add_library(ThirdPartyPlugins STATIC ${BOOST_SOURCES} ${JSONCPP_SOURCES} @@ -730,6 +733,77 @@ ##################################################################### +## Build the "AdvancedStorage" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_ADVANCED_STORAGE) + + set(AdvancedStorageFlags) + + if (CMAKE_TOOLCHAIN_FILE) + # Take absolute path to the toolchain + get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR}) + list(APPEND AdvancedStorageFlags -DCMAKE_TOOLCHAIN_FILE=${TMP}) + endif() + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + list(APPEND AdvancedStorageFlags + -DLSB_CC=${CMAKE_LSB_CC} + -DLSB_CXX=${CMAKE_LSB_CXX} + ) + endif() + + externalproject_add(AdvancedStorage + SOURCE_DIR "${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage" + + # We explicitly provide a build directory, in order to avoid paths + # that are too long on our Visual Studio 2008 CIS + BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/AdvancedStorage-build" + + # this helps triggering build when changing the external project + BUILD_ALWAYS 1 + + CMAKE_ARGS + -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR} + -DPLUGIN_VERSION=${ORTHANC_VERSION} + -DSTATIC_BUILD=${STATIC_BUILD} + -DALLOW_DOWNLOADS=${ALLOW_DOWNLOADS} + -DUSE_SYSTEM_BOOST=${USE_SYSTEM_BOOST} + -DUSE_LEGACY_JSONCPP=${USE_LEGACY_JSONCPP} + -DUSE_LEGACY_BOOST=${USE_LEGACY_BOOST} + ${AdvancedStorageFlags} + + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} + -DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} + ) + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + if (MSVC) + set(Prefix "") + else() + set(Prefix "lib") # MinGW + endif() + + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}AdvancedStorage.dll + DESTINATION "lib") + else() + list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix) + list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 Suffix) + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}AdvancedStorage${Suffix} + ${CMAKE_CURRENT_BINARY_DIR}/${Prefix}AdvancedStorage${Suffix}.${ORTHANC_VERSION} + DESTINATION "share/orthanc/plugins") + endif() +endif() + + +##################################################################### ## Build the companion tool to recover files compressed using Orthanc ##################################################################### diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -82,13 +82,15 @@ static FileInfo Convert(const OrthancPluginAttachment& attachment) { + std::string customData; return FileInfo(attachment.uuid, static_cast(attachment.contentType), attachment.uncompressedSize, attachment.uncompressedHash, static_cast(attachment.compressionType), attachment.compressedSize, - attachment.compressedHash); + attachment.compressedHash, + customData); } diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -61,13 +61,15 @@ static FileInfo Convert(const OrthancPluginAttachment& attachment) { + std::string customData; return FileInfo(attachment.uuid, static_cast(attachment.contentType), attachment.uncompressedSize, attachment.uncompressedHash, static_cast(attachment.compressionType), attachment.compressedSize, - attachment.compressedHash); + attachment.compressedHash, + customData); } diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Plugins/Engine/OrthancPlugins.cpp --- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -58,6 +58,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" @@ -413,6 +414,99 @@ } }; + 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 instance_; + + public: + DicomInstanceFromBuffer(const void* buffer, + size_t size) + { + buffer_.assign(reinterpret_cast(buffer), size); + + instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_)); + instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return *instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance + { + private: + std::unique_ptr parsed_; + std::unique_ptr instance_; + + public: + explicit DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) : + parsed_(transcoded.ReleaseAsParsedDicomFile()) + { + if (parsed_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_)); + instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return *instance_; + }; + }; static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, const void* data, @@ -547,8 +641,8 @@ } }; - - class StorageAreaBase : public IStorageArea + // "legacy" storage plugins don't store customData -> derive from ICoreStorageArea + class PluginStorageAreaBase : public ICoreStorageArea { private: OrthancPluginStorageCreate create_; @@ -602,9 +696,9 @@ } public: - StorageAreaBase(OrthancPluginStorageCreate create, - OrthancPluginStorageRemove remove, - PluginsErrorDictionary& errorDictionary) : + PluginStorageAreaBase(OrthancPluginStorageCreate create, + OrthancPluginStorageRemove remove, + PluginsErrorDictionary& errorDictionary) : create_(create), remove_(remove), errorDictionary_(errorDictionary) @@ -646,7 +740,7 @@ }; - class PluginStorageArea : public StorageAreaBase + class PluginStorageArea : public PluginStorageAreaBase { private: OrthancPluginStorageRead read_; @@ -663,7 +757,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) { @@ -712,7 +806,7 @@ // New in Orthanc 1.9.0 - class PluginStorageArea2 : public StorageAreaBase + class PluginStorageArea2 : public PluginStorageAreaBase { private: OrthancPluginStorageReadWhole readWhole_; @@ -721,7 +815,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) { @@ -806,19 +900,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 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(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(&wrapped), + content, size, Plugins::Convert(type), + isCompressed); + + if (error != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast(error)); + } + + if (customDataBuffer.size > 0) + { + customData.assign(reinterpret_cast(customDataBuffer.data), + static_cast(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(error)); + } + + if (customDataBuffer.size > 0) + { + customData.assign(reinterpret_cast(customDataBuffer.data), + static_cast(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(error)); + } + } + + virtual IMemoryBuffer* Read(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + std::unique_ptr 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(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(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(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() @@ -852,6 +1179,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_; @@ -867,6 +1208,9 @@ case Version2: return new PluginStorageArea2(callbacks2_, errorDictionary_); + case Version3: + return new PluginStorageArea3(callbacks3_, errorDictionary_); + default: throw OrthancException(ErrorCode_InternalError); } @@ -2458,101 +2802,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 instance_; - - public: - DicomInstanceFromBuffer(const void* buffer, - size_t size) - { - buffer_.assign(reinterpret_cast(buffer), size); - - instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_)); - instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); - } - - virtual bool CanBeFreed() const ORTHANC_OVERRIDE - { - return true; - } - - virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE - { - return *instance_; - }; - }; - - - class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance - { - private: - std::unique_ptr parsed_; - std::unique_ptr instance_; - - public: - explicit DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) : - parsed_(transcoded.ReleaseAsParsedDicomFile()) - { - if (parsed_.get() == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - - instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_)); - instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); - } - - 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) @@ -4830,7 +5079,8 @@ { const _OrthancPluginStorageAreaCreate& p = *reinterpret_cast(parameters); - IStorageArea& storage = *reinterpret_cast(p.storageArea); + PluginStorageAreaBase& storage = *reinterpret_cast(p.storageArea); + std::string customDataNotUsed; storage.Create(p.uuid, p.content, static_cast(p.size), Plugins::Convert(p.type)); return true; } @@ -4840,7 +5090,8 @@ const _OrthancPluginStorageAreaRead& p = *reinterpret_cast(parameters); IStorageArea& storage = *reinterpret_cast(p.storageArea); - std::unique_ptr content(storage.Read(p.uuid, Plugins::Convert(p.type))); + std::string customDataNotUsed; + std::unique_ptr content(storage.Read(p.uuid, Plugins::Convert(p.type), customDataNotUsed)); CopyToMemoryBuffer(*p.target, content->GetData(), content->GetSize()); return true; } @@ -4850,7 +5101,8 @@ const _OrthancPluginStorageAreaRemove& p = *reinterpret_cast(parameters); IStorageArea& storage = *reinterpret_cast(p.storageArea); - storage.Remove(p.uuid, Plugins::Convert(p.type)); + std::string customDataNotUsed; + storage.Remove(p.uuid, Plugins::Convert(p.type), customDataNotUsed); return true; } @@ -5411,23 +5663,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(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(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(parameters); + pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); + } else { throw OrthancException(ErrorCode_InternalError); diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Plugins/Engine/OrthancPlugins.h --- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Thu Sep 08 17:42:08 2022 +0200 @@ -87,11 +87,14 @@ class HttpClientChunkedAnswer; class HttpServerChunkedReader; class IDicomInstance; - class DicomInstanceFromCallback; class DicomInstanceFromBuffer; class DicomInstanceFromTranscoded; class WebDavCollection; - + +public: + class DicomInstanceFromCallback; + +private: void RegisterRestCallback(const void* parameters, bool lock); diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Thu Sep 08 17:42:08 2022 +0200 @@ -469,6 +469,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, @@ -1259,7 +1260,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 @@ -1334,6 +1335,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. * @@ -9034,6 +9150,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); + } + #ifdef __cplusplus } #endif diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Plugins/Samples/AdvancedStorage/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdvancedStorage/CMakeLists.txt Thu Sep 08 17:42:08 2022 +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 . + + +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 . + ) diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -0,0 +1,504 @@ +/** + * 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 . + **/ + + +#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 +#include +#include +#include + + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = boost::filesystem; + +fs::path absoluteRootPath_; +bool fsyncOnWrite_ = true; + + +fs::path GetLegacyRelativePath(const std::string& uuid) +{ + + if (!Orthanc::Toolbox::IsUuid(uuid)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + fs::path path = absoluteRootPath_; + + 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 GetAbsolutePath(const std::string& uuid, const std::string& customData) +{ + fs::path path = absoluteRootPath_; + + if (!customData.empty()) + { + if (customData.substr(0, 2) == "1.") // version 1 + { + path /= customData.substr(2); + } + else + { + throw "TODO: unknown version"; + } + + } + else + { + path /= GetLegacyRelativePath(uuid); + } + + path.make_preferred(); + return path; +} + +std::string GetCustomData(const fs::path& path) +{ + return std::string("1.") + path.string(); // prefix the relative path with a version +} + +void AddDateDicomTagToPath(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 AddSringDicomTagToPath(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; + } +} + +fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed) +{ + fs::path path; + + if (type == OrthancPluginContentType_Dicom || type == OrthancPluginContentType_DicomUntilPixelData) + { + // TODO: allow customization ... note: right now, we always need the uuid in the path !! + + AddDateDicomTagToPath(path, tags, "StudyDate", "NO_STUDY_DATE"); + AddSringDicomTagToPath(path, tags, "PatientID"); // no default value, tag is always present if the instance is accepted by Orthanc + AddSringDicomTagToPath(path, tags, "StudyInstanceUID"); + AddSringDicomTagToPath(path, tags, "SeriesInstanceUID"); + //AddIntDicomTagToPath(path, tags, "InstanceNumber", 8, uuid); + path /= uuid; + } + else + { + path = GetLegacyRelativePath(uuid); + } + + 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 + } + + path += extension; + + return path; +} + + +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(relativePath); + + fs::path absolutePath = absoluteRootPath_ / relativePath; + + if (fs::exists(absolutePath)) + { + // 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); + + // TODO for the future: handle duplicates path (e.g: there's no uuid in the path and we are uploading the same file again) + // OrthancPlugins::LogWarning(std::string("Overwriting file \"") + path.string() + "\" (" + uuid + ")"); + } + + if (fs::exists(absolutePath.parent_path())) + { + if (!fs::is_directory(absolutePath.parent_path())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_DirectoryOverFile); + } + } + else + { + if (!fs::create_directories(absolutePath.parent_path())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_FileStorageCannotWrite); + } + } + + Orthanc::SystemToolbox::WriteFile(content, size, absolutePath.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::LogInfo(std::string("Creating instance attachment \"") + uuid + "\""); + + 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(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 + { + OrthancPlugins::LogInfo(std::string("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(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_StorageAreaPlugin; + } + + return OrthancPluginErrorCode_Success; +} + +OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target, + const char* uuid, + const char* customData, + OrthancPluginContentType type) +{ + OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\""); + + std::string path = GetAbsolutePath(uuid, customData).string(); + + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + OrthancPlugins::LogError(std::string("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()) + { + OrthancPlugins::LogError(std::string("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) + { + OrthancPlugins::LogError(std::string("Unable to allocate memory to read file: ") + path); + return OrthancPluginErrorCode_NotEnoughMemory; + } + + if (fileSize != 0) + { + f.read(reinterpret_cast(target->data), fileSize); + } + + f.close(); + } + catch (...) + { + OrthancPlugins::LogError(std::string("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) +{ + OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\""); + + std::string path = GetAbsolutePath(uuid, customData).string(); + + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + OrthancPlugins::LogError(std::string("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()) + { + OrthancPlugins::LogError(std::string("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(target->data), target->size); + + f.close(); + } + catch (...) + { + OrthancPlugins::LogError(std::string("Unexpected error while reading: ") + path); + return OrthancPluginErrorCode_StorageAreaPlugin; + } + + return OrthancPluginErrorCode_Success; +} + + +OrthancPluginErrorCode StorageRemove (const char* uuid, + const char* customData, + OrthancPluginContentType type) +{ + // LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast(type); + + fs::path p = GetAbsolutePath(uuid, customData); + + try + { + fs::remove(p); + } + catch (...) + { + // Ignore the error + } + + // Remove the empty parent directories, (ignoring the error code if these directories are not empty) + + try + { + fs::path parent = p.parent_path(); + + while (parent != absoluteRootPath_) + { + 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; + } + + OrthancPlugins::LogWarning("AdvancedStorage plugin is initializing"); + OrthancPluginSetDescription(context, "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, + } + } + */ + + absoluteRootPath_ = fs::absolute(fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage"))); + LOG(WARNING) << "AdvancedStorage - Path to the storage area: " << absoluteRootPath_.string(); + + OrthancPluginRegisterStorageArea3(context, StorageCreateInstance, StorageCreateAttachment, StorageReadWhole, StorageReadRange, StorageRemove); + } + else + { + OrthancPlugins::LogWarning("AdvancedStorage plugin is disabled by the configuration file"); + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + OrthancPlugins::LogWarning("AdvancedStorage plugin is finalizing"); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "advanced-storage"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return ORTHANC_PLUGIN_VERSION; + } +} diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Resources/Configuration.json --- a/OrthancServer/Resources/Configuration.json Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Resources/Configuration.json Thu Sep 08 17:42:08 2022 +0200 @@ -10,11 +10,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 diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Sources/Database/DowngradeFrom7to6.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/DowngradeFrom7to6.sql Thu Sep 08 17:42:08 2022 +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 . + + +-- +-- 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; + diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Sources/Database/PrepareDatabase.sql --- a/OrthancServer/Sources/Database/PrepareDatabase.sql Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Thu Sep 08 17:42:08 2022 +0200 @@ -63,6 +63,7 @@ compressionType INTEGER, uncompressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) compressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) + customData TEXT, -- New in Orthanc 1.12.0 (database v7) PRIMARY KEY(id, fileType) ); @@ -114,7 +115,9 @@ 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, + -- customData is new in Orthanc 1.12.0 (database v7) + old.customData); END; CREATE TRIGGER ResourceDeleted @@ -143,4 +146,4 @@ -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration -INSERT INTO GlobalProperties VALUES (1, "6"); +INSERT INTO GlobalProperties VALUES (1, "7"); diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -324,7 +324,9 @@ int64_t revision) ORTHANC_OVERRIDE { // TODO - REVISIONS - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5, customData) " + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)"); s.BindInt64(0, id); s.BindInt(1, attachment.GetContentType()); s.BindString(2, attachment.GetUuid()); @@ -333,10 +335,10 @@ s.BindInt(5, attachment.GetCompressionType()); s.BindString(6, attachment.GetUncompressedMD5()); s.BindString(7, attachment.GetCompressedMD5()); + s.BindString(8, attachment.GetCustomData()); s.Run(); } - virtual void ApplyLookupResources(std::list& resourcesId, std::list* instancesId, const std::vector& lookup, @@ -801,7 +803,7 @@ { SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT uuid, uncompressedSize, compressionType, compressedSize, " - "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); + "uncompressedMD5, compressedMD5, customData FROM AttachedFiles WHERE id=? AND fileType=?"); s.BindInt64(0, id); s.BindInt(1, contentType); @@ -817,7 +819,8 @@ s.ColumnString(4), static_cast(s.ColumnInt(2)), s.ColumnInt64(3), - s.ColumnString(5)); + s.ColumnString(5), + s.ColumnString(6)); revision = 0; // TODO - REVISIONS return true; } @@ -1098,14 +1101,14 @@ virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE { - return 7; + return 8; } virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE { if (sqlite_.activeTransaction_ != NULL) { - std::string uncompressedMD5, compressedMD5; + std::string uncompressedMD5, compressedMD5, customData; if (!context.IsNullValue(5)) { @@ -1117,13 +1120,19 @@ compressedMD5 = context.GetStringValue(6); } + if (!context.IsNullValue(7)) + { + customData = context.GetStringValue(7); + } + FileInfo info(context.GetStringValue(0), static_cast(context.GetIntValue(1)), static_cast(context.GetInt64Value(2)), uncompressedMD5, static_cast(context.GetIntValue(3)), static_cast(context.GetInt64Value(4)), - compressedMD5); + compressedMD5, + customData); sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info); } @@ -1351,7 +1360,7 @@ } // New in Orthanc 1.5.1 - if (version_ == 6) + if (version_ >= 6) { if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true /* unused in SQLite */) || tmp != "1") @@ -1393,7 +1402,7 @@ { boost::mutex::scoped_lock lock(mutex_); - if (targetVersion != 6) + if (targetVersion != 7) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } @@ -1403,7 +1412,8 @@ if (version_ != 3 && version_ != 4 && version_ != 5 && - version_ != 6) + version_ != 6 && + version_ != 7) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); } @@ -1444,6 +1454,14 @@ version_ = 6; } + + if (version_ == 6) + { + LOG(WARNING) << "Upgrading database version from 6 to 7"; + ExecuteUpgradeScript(db_, ServerResources::UPGRADE_DATABASE_6_TO_7); + version_ = 7; + } + } diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Sources/Database/Upgrade6To7.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Upgrade6To7.sql Thu Sep 08 17:42:08 2022 +0200 @@ -0,0 +1,46 @@ +-- 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 . + + +-- +-- This SQLite script updates the version of the Orthanc database from 6 to 7. +-- + +-- Add a new column to AttachedFiles + +ALTER TABLE AttachedFiles ADD COLUMN customData TEXT; + +-- update the triggers +DROP TRIGGER AttachedFileDeleted; + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize, + -- These 2 arguments are new in Orthanc 0.7.3 (database v4) + old.uncompressedMD5, old.compressedMD5, + -- Next argument new in Orthanc 1.12.0 (database v7) + old.customData); +END; + +-- Change the database version +-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration + +UPDATE GlobalProperties SET value="7" WHERE property=1; diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Sources/OrthancInitialization.cpp --- a/OrthancServer/Sources/OrthancInitialization.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -420,7 +420,7 @@ { // Anonymous namespace to avoid clashes between compilation modules - class FilesystemStorageWithoutDicom : public IStorageArea + class FilesystemStorageWithoutDicom : public ICoreStorageArea { private: FilesystemStorage storage_; diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -2393,6 +2393,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 @@ -2416,7 +2417,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 @@ -2536,9 +2537,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); } diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -486,10 +486,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); } @@ -599,8 +600,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); @@ -610,8 +614,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); } @@ -858,6 +865,7 @@ void ServerContext::ChangeAttachmentCompression(const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, CompressionType compression) { @@ -884,8 +892,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 { @@ -1004,7 +1029,7 @@ { StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry()); - accessor.ReadStartRange(dicom, attachment.GetUuid(), FileContentType_Dicom, pixelDataOffset); + accessor.ReadStartRange(dicom, attachment.GetUuid(), FileContentType_Dicom, pixelDataOffset, attachment.GetCustomData()); } assert(dicom.size() == pixelDataOffset); @@ -1070,9 +1095,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 */); } } } @@ -1134,7 +1159,7 @@ StorageAccessor accessor(area_, &storageCache_, GetMetricsRegistry()); - accessor.ReadStartRange(dicom, attachment.GetUuid(), attachment.GetContentType(), pixelDataOffset); + accessor.ReadStartRange(dicom, attachment.GetUuid(), attachment.GetContentType(), pixelDataOffset, attachment.GetCustomData()); assert(dicom.size() == pixelDataOffset); return true; // Success @@ -1262,6 +1287,7 @@ bool ServerContext::AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -1275,7 +1301,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 { diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Sources/ServerContext.h Thu Sep 08 17:42:08 2022 +0200 @@ -268,7 +268,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 @@ -325,6 +326,7 @@ bool AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -346,6 +348,7 @@ FileContentType content); void ChangeAttachmentCompression(const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, CompressionType compression); diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/Sources/ServerIndex.cpp --- a/OrthancServer/Sources/ServerIndex.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -46,12 +46,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()) { } @@ -61,6 +63,11 @@ return uuid_; } + const std::string& GetCustomData() const + { + return customData_; + } + FileContentType GetContentType() const { return type_; @@ -94,7 +101,7 @@ { try { - context_.RemoveFile(it->GetUuid(), it->GetContentType()); + context_.RemoveFile(it->GetUuid(), it->GetContentType(), it->GetCustomData()); } catch (OrthancException& e) { diff -r 4366b4c41441 -r d7274e43ea7c OrthancServer/UnitTestsSources/ServerIndexTests.cpp --- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Wed Aug 31 10:36:38 2022 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Thu Sep 08 17:42:08 2022 +0200 @@ -291,9 +291,9 @@ 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); @@ -401,7 +401,7 @@ std::string tmp; ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true)); - ASSERT_EQ("6", tmp); + ASSERT_EQ("7", tmp); ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep, true)); ASSERT_EQ("World", tmp); ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast, true)); @@ -473,7 +473,7 @@ std::string p = "Patient " + boost::lexical_cast(i); patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast(i)), 42); + "md5-" + boost::lexical_cast(i), "customData"), 42); ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } @@ -534,7 +534,7 @@ std::string p = "Patient " + boost::lexical_cast(i); patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast(i)), 42); + "md5-" + boost::lexical_cast(i), "customData"), 42); ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } @@ -774,7 +774,7 @@ for (size_t i = 0; i < ids.size(); i++) { - FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5"); + FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5", "customData"); int64_t revision = -1; index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1, ""); ASSERT_EQ(0, revision); diff -r 4366b4c41441 -r d7274e43ea7c TODO --- a/TODO Wed Aug 31 10:36:38 2022 +0200 +++ b/TODO Thu Sep 08 17:42:08 2022 +0200 @@ -1,3 +1,16 @@ +TODO_CUSTOM_DATA branch +- add REVISIONS in SQLite since we change the DB schema +- upgrade DB automatically such that it does not need a specific launch with --update , add --downgrade + --no-auto-upgrade command lines +- expand the DB plugin SDK to handle customDATA +- implement OrthancPluginDataBaseV4 +- handle all TODO_CUSTOM_DATA +- check /attachments/... routes for path returned +- AdvancedStoragePlugin + - support multiple roots (multiple disks) + - 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) + + ======================= === Orthanc Roadmap === =======================