Mercurial > hg > orthanc
diff OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp @ 5080:d7274e43ea7c attach-custom-data
allow plugins to store a customData in the Attachments table to e.g. store custom paths without requiring an external DB
author | Alain Mazy <am@osimis.io> |
---|---|
date | Thu, 08 Sep 2022 17:42:08 +0200 |
parents | |
children | c673997507ea |
line wrap: on
line diff
--- /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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" +#include "../../../../OrthancFramework/Sources/Toolbox.h" +#include "../../../../OrthancFramework/Sources/Logging.h" +#include "../Common/OrthancPluginCppWrapper.h" + +#include <boost/filesystem.hpp> +#include <boost/filesystem/fstream.hpp> +#include <boost/iostreams/device/file_descriptor.hpp> +#include <boost/iostreams/stream.hpp> + + +#include <json/value.h> +#include <json/writer.h> +#include <string.h> +#include <iostream> +#include <algorithm> +#include <map> +#include <list> +#include <time.h> + +namespace fs = boost::filesystem; + +fs::path 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<OrthancPluginErrorCode>(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_StorageAreaPlugin; + } + + return OrthancPluginErrorCode_Success; +} + + +OrthancPluginErrorCode StorageCreateAttachment(OrthancPluginMemoryBuffer* customData, + const char* uuid, + const char* resourceId, + OrthancPluginResourceType resourceType, + const void* content, + int64_t size, + OrthancPluginContentType type, + bool isCompressed) +{ + try + { + 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<OrthancPluginErrorCode>(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<char*>(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<char*>(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<int>(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; + } +}