# HG changeset patch # User Alain Mazy # Date 1727193165 -7200 # Node ID a451777236fbd0e50718efd0eb8d9c32b1d7f8c5 # Parent 1f6642c04541e5c8bc1d42102a428dc72963fec6 advanced storage: fix + customizable path diff -r 1f6642c04541 -r a451777236fb OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Tue Sep 24 15:15:51 2024 +0200 +++ b/OrthancServer/CMakeLists.txt Tue Sep 24 17:52:45 2024 +0200 @@ -825,6 +825,14 @@ list(APPEND ADVANCED_STORAGE_RESOURCES ${AUTOGENERATED_DIR}/AdvancedStorage.rc) endif() + EmbedResources( + --target=AdvancedStorageDicomResources + --namespace=Orthanc.FrameworkResources + --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources + ${LIBICU_RESOURCES} + ${DCMTK_DICTIONARIES} + ) + set_source_files_properties( ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/Plugin.cpp PROPERTIES COMPILE_DEFINITIONS "ADVANCED_STORAGE_VERSION=\"${ORTHANC_VERSION}\"" @@ -832,13 +840,9 @@ 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 + ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/OrthancFrameworkDependencies.cpp + ${AUTOGENERATED_DIR}/AdvancedStorageDicomResources.cpp + ${ADVANCED_STORAGE_RESOURCES} ) DefineSourceBasenameForTarget(AdvancedStorage) diff -r 1f6642c04541 -r a451777236fb OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Tue Sep 24 15:15:51 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Tue Sep 24 17:52:45 2024 +0200 @@ -7,54 +7,95 @@ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium * * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Affero General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * + * 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 - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License + * 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 . **/ - #pragma once -#include "IndexBackend.h" - - -#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) // Macro introduced in Orthanc 1.3.1 -# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 0) +#if ORTHANC_ENABLE_PLUGINS == 1 -namespace OrthancDatabases -{ - /** - * @brief Bridge between C and C++ database engines. - * - * Class creating the bridge between the C low-level primitives for - * custom database engines, and the high-level IDatabaseBackend C++ - * interface, through ProtocolBuffers for Orthanc >= 1.12.0. - **/ - class DatabaseBackendAdapterV4 +#include "../../../OrthancFramework/Sources/SharedLibrary.h" +#include "../../Sources/Database/IDatabaseWrapper.h" +#include "../Include/orthanc/OrthancCPlugin.h" +#include "PluginsErrorDictionary.h" + +namespace Orthanc +{ + class OrthancPluginDatabaseV4 : public IDatabaseWrapper { private: - // This class cannot be instantiated - DatabaseBackendAdapterV4() - { - } + class Transaction; + + SharedLibrary& library_; + PluginsErrorDictionary& errorDictionary_; + _OrthancPluginRegisterDatabaseBackendV4 definition_; + std::string serverIdentifier_; + bool open_; + unsigned int databaseVersion_; + IDatabaseWrapper::Capabilities dbCapabilities_; + + void CheckSuccess(OrthancPluginErrorCode code) const; public: - static void Register(IndexBackend* backend, - size_t countConnections, - unsigned int maxDatabaseRetries); + OrthancPluginDatabaseV4(SharedLibrary& library, + PluginsErrorDictionary& errorDictionary, + const _OrthancPluginRegisterDatabaseBackendV4& database, + const std::string& serverIdentifier); + + virtual ~OrthancPluginDatabaseV4(); + + const _OrthancPluginRegisterDatabaseBackendV4& GetDefinition() const + { + return definition_; + } + + PluginsErrorDictionary& GetErrorDictionary() const + { + return errorDictionary_; + } + + const std::string& GetServerIdentifier() const + { + return serverIdentifier_; + } + + virtual void Open() ORTHANC_OVERRIDE; - static void Finalize(); + virtual void Close() ORTHANC_OVERRIDE; + + const SharedLibrary& GetSharedLibrary() const + { + return library_; + } + + virtual void FlushToDisk() ORTHANC_OVERRIDE; + + virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type, + IDatabaseListener& listener) + ORTHANC_OVERRIDE; + + virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; + + virtual void Upgrade(unsigned int targetVersion, + IStorageArea& storageArea) ORTHANC_OVERRIDE; + + virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; + + virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE; + + virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE; }; } -# endif #endif diff -r 1f6642c04541 -r a451777236fb OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp --- a/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp Tue Sep 24 15:15:51 2024 +0200 +++ b/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp Tue Sep 24 17:52:45 2024 +0200 @@ -26,6 +26,7 @@ #include "../../../../OrthancFramework/Sources/SystemToolbox.h" #include "../../../../OrthancFramework/Sources/Toolbox.h" #include "../../../../OrthancFramework/Sources/Logging.h" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h" #include "../Common/OrthancPluginCppWrapper.h" #include @@ -168,31 +169,60 @@ return Orthanc::Toolbox::WriteFastJson(output, customDataJson); } -void AddSplitDateDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL) +// 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; +// } +// } + +std::string GetSplitDateDicomTagToPath(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); + return date.substr(0, 4) + "/" + date.substr(4, 2) + "/" + date.substr(6, 2); } else if (defaultValue != NULL) { - path /= defaultValue; + return defaultValue; } + + return ""; } -void AddStringDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL) +std::string GetStringDicomTagForPath(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(); + return tags[tagName].asString(); } else if (defaultValue != NULL) { - path /= defaultValue; + return defaultValue; } + + return ""; } void AddIntDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, size_t zeroPaddingWidth = 0, const char* defaultValue = NULL) @@ -242,31 +272,112 @@ fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed) { fs::path path; + bool foundUuid = false; if (!tags.isNull()) { - if (namingScheme_ == "Preset1-StudyDatePatientID") + std::vector folderNames; + Orthanc::Toolbox::SplitString(folderNames, namingScheme_, '/'); + + for (std::vector::const_iterator it = folderNames.begin(); it != folderNames.end(); ++it) { - if (!tags.isMember("StudyDate")) + std::string folderName = *it; + + if (folderName.find("$StudyDate$") != std::string::npos) + { + boost::replace_all(folderName, "$StudyDate$", GetStringDicomTagForPath(tags, "StudyDate", "NO_STUDY_DATE")); + } + + if (folderName.find("$split(StudyDate)$")) + { + boost::replace_all(folderName, "$split(StudyDate)$", GetSplitDateDicomTagToPath(tags, "StudyDate", "NO_STUDY_DATE")); + } + + if (folderName.find("$PatientBirthDate$") != std::string::npos) + { + boost::replace_all(folderName, "$PatientBirthDate$", GetStringDicomTagForPath(tags, "PatientBirthDate", "NO_PATIENT_BIRTH_DATE")); + } + + if (folderName.find("$split(PatientBirthDate)$")) { - LOG(WARNING) << "AdvancedStorage - No 'StudyDate' in attachment " << uuid << ". Attachment will be stored in NO_STUDY_DATE folder"; + boost::replace_all(folderName, "$split(PatientBirthDate)$", GetSplitDateDicomTagToPath(tags, "PatientBirthDate", "NO_PATIENT_BIRTH_DATE")); + } + + if (folderName.find("$PatientID$") != std::string::npos) + { + boost::replace_all(folderName, "$PatientID$", GetStringDicomTagForPath(tags, "PatientID", "EMPTY_PATIENT_ID")); + } + + if (folderName.find("$StudyDescription$") != std::string::npos) + { + boost::replace_all(folderName, "$StudyDescription$", GetStringDicomTagForPath(tags, "StudyDescription", "NO_STUDY_DESCRIPTION")); + } + + if (folderName.find("$SeriesDescription$") != std::string::npos) + { + boost::replace_all(folderName, "$SeriesDescription$", GetStringDicomTagForPath(tags, "SeriesDescription", "NO_SERIES_DESCRIPTION")); + } + + if (folderName.find("$StudyInstanceUID$") != std::string::npos) + { + boost::replace_all(folderName, "$StudyInstanceUID$", GetStringDicomTagForPath(tags, "StudyDescription", "NO_STUDY_INSTANCE_UID")); } - 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()) + if (folderName.find("$SeriesInstanceUID$") != std::string::npos) + { + boost::replace_all(folderName, "$SeriesInstanceUID$", GetStringDicomTagForPath(tags, "SeriesInstanceUID", "NO_SERIES_INSTANCE_UID")); + } + + if (folderName.find("$SOPInstanceUID$") != std::string::npos) + { + boost::replace_all(folderName, "$SOPInstanceUID$", GetStringDicomTagForPath(tags, "SOPInstanceUID", "NO_SOP_INSTANCE_UID")); + } + + if (folderName.find("$OrthancPatientID$") != std::string::npos) { - path += std::string(" - ") + tags["PatientName"].asString(); + Orthanc::DicomInstanceHasher hasher(tags["PatientID"].asString(), tags["StudyInstanceUID"].asString(), tags["SeriesInstanceUID"].asString(), tags["SOPInstanceUID"].asString()); + boost::replace_all(folderName, "$OrthancPatientID$", hasher.HashPatient()); + } + + if (folderName.find("$OrthancStudyID$") != std::string::npos) + { + Orthanc::DicomInstanceHasher hasher(tags["PatientID"].asString(), tags["StudyInstanceUID"].asString(), tags["SeriesInstanceUID"].asString(), tags["SOPInstanceUID"].asString()); + boost::replace_all(folderName, "$OrthancStudyID$", hasher.HashPatient()); + } + + if (folderName.find("$OrthancSeriesID$") != std::string::npos) + { + Orthanc::DicomInstanceHasher hasher(tags["PatientID"].asString(), tags["StudyInstanceUID"].asString(), tags["SeriesInstanceUID"].asString(), tags["SOPInstanceUID"].asString()); + boost::replace_all(folderName, "$OrthancSeriesID$", hasher.HashPatient()); } - AddStringDicomTagToPath(path, tags, "StudyDescription"); - AddStringDicomTagToPath(path, tags, "SeriesInstanceUID"); + if (folderName.find("$OrthancInstanceID$") != std::string::npos) + { + Orthanc::DicomInstanceHasher hasher(tags["PatientID"].asString(), tags["StudyInstanceUID"].asString(), tags["SeriesInstanceUID"].asString(), tags["SOPInstanceUID"].asString()); + boost::replace_all(folderName, "$OrthancInstanceID$", hasher.HashPatient()); + } + + if (folderName.find("$UUID$") != std::string::npos) + { + boost::replace_all(folderName, "$UUID$", uuid); + foundUuid = true; + } - path /= uuid; - path += GetExtension(type, isCompressed); - return path; + if (folderName.find("$.ext$") != std::string::npos) + { + boost::replace_all(folderName, "$.ext$", GetExtension(type, isCompressed)); + } + + path /= folderName; } + + if (!foundUuid) + { + LOG(WARNING) << "Your naming scheme does not contain $UUID$. There is a high risk of files being overwritten. Using the default naming scheme."; + return GetLegacyRelativePath(uuid); + } + + return path; } return GetLegacyRelativePath(uuid); @@ -588,6 +699,7 @@ // 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 + // "Preset2-OrthancStudyID-OrthancSeriesID-UUID" = OrthancStudyID/OrthancSeriesID/uuid.ext "NamingScheme" : "OrthancDefault", // Defines the maximum length for path used in the storage. If a file is longer @@ -605,6 +717,11 @@ const Json::Value& pluginJson = advancedStorage.GetJson(); namingScheme_ = advancedStorage.GetStringValue("NamingScheme", "OrthancDefault"); + if (namingScheme_ != "OrthancDefault" && namingScheme_.find("$UUID") == std::string::npos) + { + LOG(ERROR) << "AdvancedStorage - Your naming scheme does not contain $UUID$. The risk of files being overwritten is to high."; + return -1; + } // 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")); @@ -615,7 +732,7 @@ if (!rootPath_.is_absolute()) { - LOG(ERROR) << "AdvancedStorage - Path to the default storage area should be an absolute path '" << rootPath_ << "'"; + LOG(ERROR) << "AdvancedStorage - Path to the default storage area should be an absolute path " << rootPath_ << " (\"StorageDirectory\" in main Orthanc configuration)"; return -1; }