Mercurial > hg > orthanc
view OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp @ 5824:79ac3924eff8 attach-custom-data
doc
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Wed, 02 Oct 2024 11:41:01 +0200 |
parents | a856763e5736 |
children |
line wrap: on
line source
/** * Orthanc - A Lightweight, RESTful DICOM Store * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium * Copyright (C) 2017-2022 Osimis S.A., Belgium * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. **/ #define ORTHANC_PLUGIN_NAME "advanced-storage" #include "../../../../OrthancFramework/Sources/Compatibility.h" #include "../../../../OrthancFramework/Sources/OrthancException.h" #include "../../../../OrthancFramework/Sources/SystemToolbox.h" #include "../../../../OrthancFramework/Sources/Toolbox.h" #include "../../../../OrthancFramework/Sources/Logging.h" #include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h" #include "../Common/OrthancPluginCppWrapper.h" #include <boost/filesystem.hpp> #include <boost/filesystem/fstream.hpp> #include <boost/iostreams/device/file_descriptor.hpp> #include <boost/iostreams/stream.hpp> #include <json/value.h> #include <json/writer.h> #include <string.h> #include <iostream> #include <algorithm> #include <map> #include <list> #include <time.h> namespace fs = boost::filesystem; fs::path rootPath_; bool multipleStoragesEnabled_ = false; std::map<std::string, fs::path> rootPaths_; std::string currentStorageId_; std::string namingScheme_; std::string otherAttachmentsPrefix_; bool fsyncOnWrite_ = true; size_t maxPathLength_ = 256; size_t legacyPathLength = 39; // ex "/00/f7/00f7fd8b-47bd8c3a-ff917804-d180cdbc-40cf9527" fs::path GetRootPath() { if (multipleStoragesEnabled_) { return rootPaths_[currentStorageId_]; } return rootPath_; } fs::path GetRootPath(const std::string& storageId) { if (multipleStoragesEnabled_) { if (rootPaths_.find(storageId) == rootPaths_.end()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Advanced Storage - storage '" + storageId + "' is not defined in configuration")); } return rootPaths_[storageId]; } return rootPath_; } fs::path GetLegacyRelativePath(const std::string& uuid) { if (!Orthanc::Toolbox::IsUuid(uuid)) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } fs::path path; if (!otherAttachmentsPrefix_.empty()) { path /= otherAttachmentsPrefix_; } path /= std::string(&uuid[0], &uuid[2]); path /= std::string(&uuid[2], &uuid[4]); path /= uuid; #if BOOST_HAS_FILESYSTEM_V3 == 1 path.make_preferred(); #endif return path; } fs::path GetPath(const std::string& uuid, const std::string& customDataString) { fs::path path; if (!customDataString.empty()) { Json::Value customData; Orthanc::Toolbox::ReadJson(customData, customDataString); if (customData["v"].asInt() == 1) // Version { if (customData.isMember("s")) // Storage ID { path = GetRootPath(customData["s"].asString()); } else { path = GetRootPath(); } if (customData.isMember("p")) // Path { path /= customData["p"].asString(); } else { // we are in "legacy mode" for the path part path /= GetLegacyRelativePath(uuid); } } else { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Advanced Storage - unknown version for custom data '" + boost::lexical_cast<std::string>(customData["Version"].asInt()) + "'")); } } else // we are in "legacy mode" { path = rootPath_; path /= GetLegacyRelativePath(uuid); } path.make_preferred(); return path; } void GetCustomData(std::string& output, const fs::path& path) { // if we use defaults, non need to store anything in the metadata, the plugin has the same behavior as the core of Orthanc if (namingScheme_ == "OrthancDefault" && !multipleStoragesEnabled_) { return; } Json::Value customDataJson; // Keep short field names to reduce SQL data usage customDataJson["v"] = 1; // Version // no need to store the path since if we are in the default mode if (namingScheme_ != "OrthancDefault") { customDataJson["p"] = path.string(); // Path } if (multipleStoragesEnabled_) { customDataJson["s"] = currentStorageId_; // Storage id } return Orthanc::Toolbox::WriteFastJson(output, customDataJson); } 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(); return date.substr(0, 4) + "/" + date.substr(4, 2) + "/" + date.substr(6, 2); } else if (defaultValue != NULL) { return defaultValue; } return ""; } std::string GetStringDicomTagForPath(const Json::Value& tags, const std::string& tagName, const char* defaultValue = NULL) { if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0) { return tags[tagName].asString(); } else if (defaultValue != NULL) { return defaultValue; } return ""; } std::string GetIntDicomTagForPath(const Json::Value& tags, const std::string& tagName, const char* defaultValue = NULL, size_t padding = 0) { if (tags.isMember(tagName)) { std::string value; if (tags[tagName].isInt()) { value = boost::lexical_cast<std::string>(tags[tagName].asInt()); } else if (tags[tagName].isString()) { value = tags[tagName].asString(); } if (padding > 0 && padding > value.size()) { value = std::string(padding - value.size(), '0') + value; } return value; } else if (defaultValue != NULL) { return defaultValue; } return ""; } void ReplaceTagKeyword(std::string& folderName, const std::string& keyword, const Json::Value& tags, const char* defaultValue, const char* tagKey = NULL) { if (folderName.find(keyword) != std::string::npos) { std::string key = keyword.substr(1, keyword.size() -2); if (tagKey != NULL) { key = tagKey; } boost::replace_all(folderName, keyword, GetStringDicomTagForPath(tags, key, defaultValue)); } } void ReplaceIntTagKeyword(std::string& folderName, const std::string& keyword, const Json::Value& tags, const char* defaultValue, size_t padding, const char* tagKey = NULL) { if (folderName.find(keyword) != std::string::npos) { std::string key = keyword.substr(1, keyword.size() -2); if (tagKey != NULL) { key = tagKey; } boost::replace_all(folderName, keyword, GetIntDicomTagForPath(tags, key, defaultValue, padding)); } } void ReplaceOrthancID(std::string& folderName, const std::string& keyword, const std::string& id, size_t from, size_t length) { if (length == 0) { boost::replace_all(folderName, keyword, id); } else { boost::replace_all(folderName, keyword, id.substr(from, length)); } } void AddIntDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, size_t zeroPaddingWidth = 0, const char* defaultValue = NULL) { if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0) { std::string tagValue = tags[tagName].asString(); if (zeroPaddingWidth > 0 && tagValue.size() < zeroPaddingWidth) { std::string padding(zeroPaddingWidth - tagValue.size(), '0'); path /= padding + tagValue; } else { path /= tagValue; } } else if (defaultValue != NULL) { path /= defaultValue; } } std::string GetExtension(OrthancPluginContentType type, bool isCompressed) { std::string extension; switch (type) { case OrthancPluginContentType_Dicom: extension = ".dcm"; break; case OrthancPluginContentType_DicomUntilPixelData: extension = ".dcm.head"; break; default: extension = ".unk"; } if (isCompressed) { extension = extension + ".cmp"; // compression is zlib + size -> we can not use the .zip extension } return extension; } fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed) { fs::path path; if (!tags.isNull()) { std::vector<std::string> folderNames; Orthanc::Toolbox::SplitString(folderNames, namingScheme_, '/'); for (std::vector<std::string>::const_iterator it = folderNames.begin(); it != folderNames.end(); ++it) { std::string folderName = *it; if (folderName.find("{split(StudyDate)}") != std::string::npos) { boost::replace_all(folderName, "{split(StudyDate)}", GetSplitDateDicomTagToPath(tags, "StudyDate", "NO_STUDY_DATE")); } if (folderName.find("{split(PatientBirthDate)}") != std::string::npos) { boost::replace_all(folderName, "{split(PatientBirthDate)}", GetSplitDateDicomTagToPath(tags, "PatientBirthDate", "NO_PATIENT_BIRTH_DATE")); } ReplaceTagKeyword(folderName, "{PatientID}", tags, "NO_PATIENT_ID"); ReplaceTagKeyword(folderName, "{PatientBirthDate}", tags, "NO_PATIENT_BIRTH_DATE"); ReplaceTagKeyword(folderName, "{PatientName}", tags, "NO_PATIENT_NAME"); ReplaceTagKeyword(folderName, "{PatientSex}", tags, "NO_PATIENT_SEX"); ReplaceTagKeyword(folderName, "{StudyInstanceUID}", tags, "NO_STUDY_INSTANCE_UID"); ReplaceTagKeyword(folderName, "{StudyDate}", tags, "NO_STUDY_DATE"); ReplaceTagKeyword(folderName, "{StudyID}", tags, "NO_STUDY_ID"); ReplaceTagKeyword(folderName, "{StudyDescription}", tags, "NO_STUDY_DESCRIPTION"); ReplaceTagKeyword(folderName, "{AccessionNumber}", tags, "NO_ACCESSION_NUMBER"); ReplaceTagKeyword(folderName, "{SeriesInstanceUID}", tags, "NO_SERIES_INSTANCE_UID"); ReplaceTagKeyword(folderName, "{SeriesDate}", tags, "NO_SERIES_DATE"); ReplaceTagKeyword(folderName, "{SeriesDescription}", tags, "NO_SERIES_DESCRIPTION"); ReplaceTagKeyword(folderName, "{SOPInstanceUID}", tags, "NO_SOP_INSTANCE_UID"); ReplaceIntTagKeyword(folderName, "{SeriesNumber}", tags, "NO_SERIES_NUMBER", 0); ReplaceIntTagKeyword(folderName, "{InstanceNumber}", tags, "NO_INSTANCE_NUMBER", 0); ReplaceIntTagKeyword(folderName, "{pad4(SeriesNumber)}", tags, "NO_SERIES_NUMBER", 4, "SeriesNumber"); ReplaceIntTagKeyword(folderName, "{pad4(InstanceNumber)}", tags, "NO_INSTANCE_NUMBER", 4, "InstanceNumber"); ReplaceIntTagKeyword(folderName, "{pad6(SeriesNumber)}", tags, "NO_SERIES_NUMBER", 6, "SeriesNumber"); ReplaceIntTagKeyword(folderName, "{pad6(InstanceNumber)}", tags, "NO_INSTANCE_NUMBER", 6, "InstanceNumber"); ReplaceIntTagKeyword(folderName, "{pad8(SeriesNumber)}", tags, "NO_SERIES_NUMBER", 8, "SeriesNumber"); ReplaceIntTagKeyword(folderName, "{pad8(InstanceNumber)}", tags, "NO_INSTANCE_NUMBER", 8, "InstanceNumber"); Orthanc::DicomInstanceHasher hasher(tags["PatientID"].asString(), tags["StudyInstanceUID"].asString(), tags["SeriesInstanceUID"].asString(), tags["SOPInstanceUID"].asString()); std::string orthancPatientId = hasher.HashPatient(); std::string orthancStudyId = hasher.HashStudy(); std::string orthancSeriesId = hasher.HashSeries(); std::string orthancInstanceId = hasher.HashInstance(); ReplaceOrthancID(folderName, "{OrthancPatientID}", orthancPatientId, 0, 0); ReplaceOrthancID(folderName, "{OrthancStudyID}", orthancStudyId, 0, 0); ReplaceOrthancID(folderName, "{OrthancSeriesID}", orthancSeriesId, 0, 0); ReplaceOrthancID(folderName, "{OrthancInstanceID}", orthancInstanceId, 0, 0); ReplaceOrthancID(folderName, "{01(OrthancPatientID)}", orthancPatientId, 0, 2); ReplaceOrthancID(folderName, "{01(OrthancStudyID)}", orthancStudyId, 0, 2); ReplaceOrthancID(folderName, "{01(OrthancSeriesID)}", orthancSeriesId, 0, 2); ReplaceOrthancID(folderName, "{01(OrthancInstanceID)}", orthancInstanceId, 0, 2); ReplaceOrthancID(folderName, "{23(OrthancPatientID)}", orthancPatientId, 2, 2); ReplaceOrthancID(folderName, "{23(OrthancStudyID)}", orthancStudyId, 2, 2); ReplaceOrthancID(folderName, "{23(OrthancSeriesID)}", orthancSeriesId, 2, 2); ReplaceOrthancID(folderName, "{23(OrthancInstanceID)}", orthancInstanceId, 2, 2); if (folderName.find("{UUID}") != std::string::npos) { boost::replace_all(folderName, "{UUID}", uuid); } if (folderName.find("{.ext}") != std::string::npos) { boost::replace_all(folderName, "{.ext}", GetExtension(type, isCompressed)); } path /= folderName; } return path; } return GetLegacyRelativePath(uuid); } OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData, const char* uuid, const Json::Value& tags, const void* content, int64_t size, OrthancPluginContentType type, bool isCompressed) { fs::path relativePath = GetRelativePathFromTags(tags, uuid, type, isCompressed); fs::path rootPath = GetRootPath(); fs::path path = rootPath / relativePath; // check that the final path is not 'above' the root path (this could happen if e.g., a PatientName is ../../../../toto) // fs::canonical() can not be used for that since the file needs to exist // so far, we'll just forbid path containing '..' since they might be suspicious if (path.string().find("..") != std::string::npos) { relativePath = GetLegacyRelativePath(uuid); fs::path legacyPath = rootPath / relativePath; LOG(WARNING) << "Advanced Storage - WAS02 - Path is suspicious since it contains '..': '" << path.string() << "' will be stored in '" << legacyPath << "'"; path = legacyPath; } // check path length !!!!!, if too long, go back to legacy path and issue a warning if (path.string().size() > maxPathLength_) { relativePath = GetLegacyRelativePath(uuid); fs::path legacyPath = rootPath / relativePath; LOG(WARNING) << "Advanced Storage - WAS01 - Path is too long: '" << path.string() << "' will be stored in '" << legacyPath << "'"; path = legacyPath; } if (fs::exists(path)) { // Extremely unlikely case if uuid is included in the path: This Uuid has already been created // in the past. throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Advanced Storage - path already exists"); // TODO for the future: handle duplicates path (e.g: there's no uuid in the path and we are uploading the same file again) } std::string customDataString; GetCustomData(customDataString, relativePath); LOG(INFO) << "Advanced Storage - creating attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path.string() + ")"; if (fs::exists(path.parent_path())) { if (!fs::is_directory(path.parent_path())) { throw Orthanc::OrthancException(Orthanc::ErrorCode_DirectoryOverFile); } } else { if (!fs::create_directories(path.parent_path())) { throw Orthanc::OrthancException(Orthanc::ErrorCode_FileStorageCannotWrite); } } Orthanc::SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_); OrthancPluginCreateMemoryBuffer(OrthancPlugins::GetGlobalContext(), customData, customDataString.size()); memcpy(customData->data, customDataString.data(), customDataString.size()); return OrthancPluginErrorCode_Success; } OrthancPluginErrorCode StorageCreateInstance(OrthancPluginMemoryBuffer* customData, const char* uuid, const OrthancPluginDicomInstance* instance, const void* content, int64_t size, OrthancPluginContentType type, bool isCompressed) { try { OrthancPlugins::DicomInstance dicomInstance(instance); Json::Value tags; dicomInstance.GetSimplifiedJson(tags); return StorageCreate(customData, uuid, tags, content, size, type, isCompressed); } catch (Orthanc::OrthancException& e) { return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); } catch (...) { return OrthancPluginErrorCode_StorageAreaPlugin; } return OrthancPluginErrorCode_Success; } OrthancPluginErrorCode StorageCreateAttachment(OrthancPluginMemoryBuffer* customData, const char* uuid, const char* resourceId, OrthancPluginResourceType resourceType, const void* content, int64_t size, OrthancPluginContentType type, bool isCompressed) { try { LOG(INFO) << "Creating attachment \"" << uuid << "\""; //TODO_CUSTOM_DATA: get tags from the Rest API... Json::Value tags; return StorageCreate(customData, uuid, tags, content, size, type, isCompressed); } catch (Orthanc::OrthancException& e) { return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); } catch (...) { return OrthancPluginErrorCode_StorageAreaPlugin; } return OrthancPluginErrorCode_Success; } OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target, const char* uuid, const char* customData, OrthancPluginContentType type) { std::string path = GetPath(uuid, customData).string(); LOG(INFO) << "Advanced Storage - Reading whole attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path + ")"; if (!Orthanc::SystemToolbox::IsRegularFile(path)) { LOG(ERROR) << "The path does not point to a regular file: " << path; return OrthancPluginErrorCode_InexistentFile; } try { fs::ifstream f; f.open(path, std::ifstream::in | std::ifstream::binary); if (!f.good()) { LOG(ERROR) << "The path does not point to a regular file: " << path; return OrthancPluginErrorCode_InexistentFile; } // get file size f.seekg(0, std::ios::end); std::streamsize fileSize = f.tellg(); f.seekg(0, std::ios::beg); // The ReadWhole must allocate the buffer itself if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, fileSize) != OrthancPluginErrorCode_Success) { LOG(ERROR) << "Unable to allocate memory to read file: " << path; return OrthancPluginErrorCode_NotEnoughMemory; } if (fileSize != 0) { f.read(reinterpret_cast<char*>(target->data), fileSize); } f.close(); } catch (...) { LOG(ERROR) << "Unexpected error while reading: " << path; return OrthancPluginErrorCode_StorageAreaPlugin; } return OrthancPluginErrorCode_Success; } OrthancPluginErrorCode StorageReadRange (OrthancPluginMemoryBuffer64* target, const char* uuid, const char* customData, OrthancPluginContentType type, uint64_t rangeStart) { std::string path = GetPath(uuid, customData).string(); LOG(INFO) << "Advanced Storage - Reading range of attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path + ")"; if (!Orthanc::SystemToolbox::IsRegularFile(path)) { LOG(ERROR) << "The path does not point to a regular file: " << path; return OrthancPluginErrorCode_InexistentFile; } try { fs::ifstream f; f.open(path, std::ifstream::in | std::ifstream::binary); if (!f.good()) { LOG(ERROR) << "The path does not point to a regular file: " << path; return OrthancPluginErrorCode_InexistentFile; } f.seekg(rangeStart, std::ios::beg); // The ReadRange uses a target that has already been allocated by orthanc f.read(reinterpret_cast<char*>(target->data), target->size); f.close(); } catch (...) { LOG(ERROR) << "Unexpected error while reading: " << path; return OrthancPluginErrorCode_StorageAreaPlugin; } return OrthancPluginErrorCode_Success; } OrthancPluginErrorCode StorageRemove (const char* uuid, const char* customData, OrthancPluginContentType type) { fs::path path = GetPath(uuid, customData); LOG(INFO) << "Advanced Storage - Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path.string() + ")"; try { fs::remove(path); } catch (...) { // Ignore the error } // Remove the empty parent directories, (ignoring the error code if these directories are not empty) try { fs::path parent = path.parent_path(); while (parent != GetRootPath()) { fs::remove(parent); parent = parent.parent_path(); } } catch (...) { // Ignore the error } return OrthancPluginErrorCode_Success; } extern "C" { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) { OrthancPlugins::SetGlobalContext(context); Orthanc::Logging::InitializePluginContext(context); /* Check the version of the Orthanc core */ if (OrthancPluginCheckVersion(context) == 0) { OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); return -1; } LOG(WARNING) << "AdvancedStorage plugin is initializing"; OrthancPluginSetDescription2(context, ORTHANC_PLUGIN_NAME, "Provides alternative layout for your storage."); OrthancPlugins::OrthancConfiguration orthancConfiguration; OrthancPlugins::OrthancConfiguration advancedStorage; orthancConfiguration.GetSection(advancedStorage, "AdvancedStorage"); bool enabled = advancedStorage.GetBooleanValue("Enable", false); if (enabled) { /* { "AdvancedStorage": { // Enables/disables the plugin "Enable": false, // Enables/disables support for multiple StorageDirectories (disabled by default) // Note: when saving a file, the plugin stores only the storage-id in the SQL DB "MultipleStorages" : { "Storages" : { // Therefore, storage path may change in case you move your data from one place to another. // The storgae ids may never change since they are stored in DB; you can only add new ones. "1" : "/var/lib/orthanc/db", "2" : "/mnt/disk2/orthanc" }, // The storage on which new data is stored. // There's currently no automatic changes of disks "CurrentStorage" : "2", }, // Defines the storage structure and file namings. // These keywords can be used to generate the path: // Attachment info: // {UUID} : A unique file identifier // {01(UUID)} : The first 2 characters of the file UUID // {23(UUID)} : The 3rd and 4th characters of the file UUID // {.ext} : The file extension // DICOM TAGS: // {PatientID}, {PatientName}, {PatientBirthDate} // {StudyInstanceUID}, {SeriesInstanceUID}, {SOPInstanceUID} // {StudyDescription}, {SeriesDescription} // {StudyDate}, {AccessionNumber}, {InstanceNumber}, {SeriesNumber} // Transformed DICOM TAGS: // {split(StudyDate)} : 3 subfolders: YYYY/MM/DD // {split(PatientBirthDate)} : 3 subfolders: YYYY/MM/DD // {pad4(InstanceNumber)} : the instance number padded with zeroes to have 4 characters // {pad4(SeriesNumber)} : the instance number padded with zeroes to have 4 characters // {pad6(InstanceNumber)} : the instance number padded with zeroes to have 6 characters // {pad6(SeriesNumber)} : the instance number padded with zeroes to have 6 characters // {pad8(InstanceNumber)} : the instance number padded with zeroes to have 8 characters // {pad8(SeriesNumber)} : the instance number padded with zeroes to have 8 characters // Orthanc IDs: // {OrthancPatientID}, {OrthancStudyID}, {OrthancSeriesID}, {OrthancInstanceID} // Transformed Orthanc IDs: // {01(OrthancPatientID)}, {01(OrthancStudyID)}, ... : the first 2 characters of the Orthanc ID // {23(OrthancPatientID)}, {23(OrthancStudyID)}, ... : the 3rd and 4th characters of the Orthanc ID // Examples: // "OrthancDefault" is a special value to use the same structure as the Orthanc core. // This option consumes less space in the SQL DB since the path must not be saved in DB. // "{01(UUID)}/{23(UUID)}/{UUID}{.ext}" is equivalent with the structure of the Orthanc core with and added file extension // "{split(StudyDate)}/{StudyInstanceUID} - {PatientID}/{SeriesInstanceUID}/{pad6(InstanceNumber)} - {UUID}{.ext}" // "{PatientID} - {PatientName}/{StudyDate} - {StudyInstanceUID} - {StudyDescription}/{SeriesInstanceUID}/{UUID}{.ext}" // Notes: // - To prevent files from being overwritten, it is very important that their path is unique ! // Therefore, your NamingScheme must always include: // - either the file {UUID} (this is mandatory in this Beta version !!!!!) // - MAYBE IN A LATER BETA VERSION: at least a patient identifier {PatientID} or {OrthancPatientID}, // a study identifier {StudyInstanceUID} or {OrthancStudyID}, // a series identifier {SeriesInstanceUID} or {OrthancSeriesID}, // an instance identifier {SOPInstanceUID} or {OrthancInstanceID} // - The NamingScheme defines a RELATIVE path to either the "StorageDirectory" of Orthanc or one of // the "MultipleStorages" of this plugin. // - The relative path generated from the NamingScheme is stored in the SQL DB. Therefore, you may change the // NamingScheme at any time and you'll still be able to access previously saved files. "NamingScheme" : "OrthancDefault", // Defines the maximum length for path used in the storage. If a file is longer // than this limit, it is stored with the default orthanc naming scheme // (and a warning is issued). // Note, on Windows, the maximum path length is 260 bytes by default but can be increased // through a configuration. "MaxPathLength" : 256, // When saving non DICOM attachments, Orthanc does not have access to the DICOM tags // and can therefore not compute a path using the NamingScheme. // Therefore, all non DICOM attachements are grouped in a subfolder using the // legacy structure. With this option, you can define a root folder for these // non DICOM attachments // e.g: "OtherAttachmentsPrefix": "_attachments" // Notes: // - When using a prefix, the path is saved in the SQL DB. Therefore, you may change the OtherAttachmentsPrefix // at any time and you'll still be able to access previously saved files. "OtherAttachmentsPrefix": "", } } */ fsyncOnWrite_ = orthancConfiguration.GetBooleanValue("SyncStorageArea", true); const Json::Value& pluginJson = advancedStorage.GetJson(); namingScheme_ = advancedStorage.GetStringValue("NamingScheme", "OrthancDefault"); if (namingScheme_ != "OrthancDefault") { // when using a custom scheme, to avoid collisions, you must include, at least the attachment UUID // or each of the DICOM IDs or orthanc IDs if (namingScheme_.find("{UUID}") == std::string::npos) { LOG(ERROR) << "AdvancedStorage - To avoid files from being overwritten, your naming scheme shall alway contain the {UUID} (at least in this beta version !!!)."; return -1; if (namingScheme_.find("PatientID") == std::string::npos && namingScheme_.find("OrthancPatientID") == std::string::npos) { LOG(ERROR) << "AdvancedStorage - To avoid files from being overwritten, your naming scheme shall alway contain either the {UUID} or 4 DICOM identifiers ({PatientID}, {StudyInstanceUID}, {SeriesInstanceUID}, {SOPInstanceUID}) or 4 Orthanc identifiers ({PatientOrthancID}, {StudyOrthancID}, {SeriesOrthancID}, {SOPInstanceUID})."; return -1; } if (namingScheme_.find("StudyInstanceUID") == std::string::npos && namingScheme_.find("OrthancStudyID") == std::string::npos) { LOG(ERROR) << "AdvancedStorage - To avoid files from being overwritten, your naming scheme shall alway contain either the {UUID} or 4 DICOM identifiers ({PatientID}, {StudyInstanceUID}, {SeriesInstanceUID}, {SOPInstanceUID}) or 4 Orthanc identifiers ({PatientOrthancID}, {StudyOrthancID}, {SeriesOrthancID}, {SOPInstanceUID})."; return -1; } if (namingScheme_.find("SeriesInstanceUID") == std::string::npos && namingScheme_.find("OrthancSeriesID") == std::string::npos) { LOG(ERROR) << "AdvancedStorage - To avoid files from being overwritten, your naming scheme shall alway contain either the {UUID} or 4 DICOM identifiers ({PatientID}, {StudyInstanceUID}, {SeriesInstanceUID}, {SOPInstanceUID}) or 4 Orthanc identifiers ({PatientOrthancID}, {StudyOrthancID}, {SeriesOrthancID}, {SOPInstanceUID})."; return -1; } if (namingScheme_.find("SOPInstanceUID") == std::string::npos && namingScheme_.find("OrthancInstanceID") == std::string::npos) { LOG(ERROR) << "AdvancedStorage - To avoid files from being overwritten, your naming scheme shall alway contain either the {UUID} or 4 DICOM identifiers ({PatientID}, {StudyInstanceUID}, {SeriesInstanceUID}, {SOPInstanceUID}) or 4 Orthanc identifiers ({PatientOrthancID}, {StudyOrthancID}, {SeriesOrthancID}, {SOPInstanceUID})."; return -1; } } } otherAttachmentsPrefix_ = advancedStorage.GetStringValue("OtherAttachmentsPrefix", ""); LOG(WARNING) << "AdvancedStorage - Path to the other attachments root: " << otherAttachmentsPrefix_; // if we have enabled multiple storage after files have been saved without this plugin, we still need the default StorageDirectory rootPath_ = fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage")); LOG(WARNING) << "AdvancedStorage - Path to the default storage area: " << rootPath_.string(); maxPathLength_ = advancedStorage.GetIntegerValue("MaxPathLength", 256); LOG(WARNING) << "AdvancedStorage - Maximum path length: " << maxPathLength_; if (!rootPath_.is_absolute()) { LOG(ERROR) << "AdvancedStorage - Path to the default storage area should be an absolute path " << rootPath_ << " (\"StorageDirectory\" in main Orthanc configuration)"; return -1; } if (rootPath_.size() > (maxPathLength_ - legacyPathLength)) { LOG(ERROR) << "AdvancedStorage - Path to the default storage is too long"; return -1; } if (pluginJson.isMember("MultipleStorages")) { multipleStoragesEnabled_ = true; const Json::Value& multipleStoragesJson = pluginJson["MultipleStorages"]; if (multipleStoragesJson.isMember("Storages") && multipleStoragesJson.isObject() && multipleStoragesJson.isMember("CurrentStorage") && multipleStoragesJson["CurrentStorage"].isString()) { const Json::Value& storagesJson = multipleStoragesJson["Storages"]; Json::Value::Members storageIds = storagesJson.getMemberNames(); for (Json::Value::Members::const_iterator it = storageIds.begin(); it != storageIds.end(); ++it) { const Json::Value& storagePath = storagesJson[*it]; if (!storagePath.isString()) { LOG(ERROR) << "AdvancedStorage - Storage path is not a string " << *it; return -1; } rootPaths_[*it] = storagePath.asString(); if (!rootPaths_[*it].is_absolute()) { LOG(ERROR) << "AdvancedStorage - Storage path shall be absolute path '" << storagePath.asString() << "'"; return -1; } if (storagePath.asString().size() > (maxPathLength_ - legacyPathLength)) { LOG(ERROR) << "AdvancedStorage - Storage path is too long '" << storagePath.asString() << "'"; return -1; } } currentStorageId_ = multipleStoragesJson["CurrentStorage"].asString(); if (rootPaths_.find(currentStorageId_) == rootPaths_.end()) { LOG(ERROR) << "AdvancedStorage - CurrentStorage is not defined in Storages list: " << currentStorageId_; return -1; } LOG(WARNING) << "AdvancedStorage - multiple storages enabled. Current storage : " << rootPaths_[currentStorageId_].string(); } } OrthancPluginRegisterStorageArea3(context, StorageCreateInstance, StorageCreateAttachment, StorageReadWhole, StorageReadRange, StorageRemove); } else { LOG(WARNING) << "AdvancedStorage plugin is disabled by the configuration file"; } return 0; } ORTHANC_PLUGINS_API void OrthancPluginFinalize() { LOG(WARNING) << "AdvancedStorage plugin is finalizing"; } ORTHANC_PLUGINS_API const char* OrthancPluginGetName() { return ORTHANC_PLUGIN_NAME; } ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() { return ADVANCED_STORAGE_VERSION; } }