# HG changeset patch # User Alain Mazy # Date 1765893635 -3600 # Node ID f96ce88ff7641c04c8a774e6c2e1789192ad3281 # Parent 370e6d73da2335df347fb7cc9f4fdfefaedacb2f added RegisterStorageArea3 diff -r 370e6d73da23 -r f96ce88ff764 CMakeLists.txt --- a/CMakeLists.txt Tue Dec 02 12:09:36 2025 +0100 +++ b/CMakeLists.txt Tue Dec 16 15:00:35 2025 +0100 @@ -317,6 +317,7 @@ Sources/ReceivedInstanceCallback.cpp Sources/RestCallbacks.cpp Sources/StorageArea.cpp + Sources/StorageArea3.cpp Sources/StorageCommitmentScpCallback.cpp # Third-party sources diff -r 370e6d73da23 -r f96ce88ff764 CodeAnalysis/CustomFunctions.json --- a/CodeAnalysis/CustomFunctions.json Tue Dec 02 12:09:36 2025 +0100 +++ b/CodeAnalysis/CustomFunctions.json Tue Dec 16 15:00:35 2025 +0100 @@ -661,6 +661,46 @@ "return_sdk_type" : "Tuple", "since_sdk" : [ 1, 12, 9 ], "sdk_functions" : [ "OrthancPluginSetStableStatus" ] + }, + + { + "comment" : "New in release 7.1", + "short_name" : "RegisterStorageArea3", + "implementation" : "RegisterStorageArea3", + "documentation" : { + "description" : [ "Register a custom storage area (v3)." ], + "args" : { + "create" : "The callback function to store a file on the custom storage area (v2).", + "read" : "The callback function to read a file from the custom storage area (v2).", + "remove" : "The callback function to remove a file from the custom storage area (v2)." + } + }, + "args" : [ + { + "sdk_name" : "create", + "sdk_type" : "Callable", + "callable_type" : "StorageCreateCallback2", + "callable_protocol_args" : "uuid: str, content_type: ContentType, compression_type: CompressionType, content: bytes, dicom_instance: DicomInstance", + "callable_protocol_return" : "Tuple" + }, + { + "sdk_name" : "read", + "sdk_type" : "Callable", + "callable_type" : "StorageReadCallback2", + "callable_protocol_args" : "uuid: str, content_type: ContentType, range_start: int, size: int, custom_data: bytes", + "callable_protocol_return" : "Tuple" + }, + { + "sdk_name" : "remove", + "sdk_type" : "Callable", + "callable_type" : "StorageRemoveCallback2", + "callable_protocol_args" : "uuid: str, content_type: ContentType, custom_data: bytes", + "callable_protocol_return" : "ErrorCode" + } + ], + "return_sdk_type" : "void", + "since_sdk" : [ 1, 9, 0 ], + "sdk_functions" : [ "OrthancPluginRegisterStorageArea3" ] } ] diff -r 370e6d73da23 -r f96ce88ff764 NEWS --- a/NEWS Tue Dec 02 12:09:36 2025 +0100 +++ b/NEWS Tue Dec 16 15:00:35 2025 +0100 @@ -1,11 +1,18 @@ Pending changes in the mainline =============================== +=> Maximum SDK version: 1.12.10 (default) <= +=> Minimum SDK version: 1.7.2 <= + + +* Added "RegisterStorageArea3" + Version 7.0 (2025-12-02) ======================== => Maximum SDK version: 1.12.10 (default) <= +=> Minimum SDK version: 1.7.2 <= * The "orthanc.pyi" stub is now excluded from the "install" step during the build * Wrapped new SCP callbacks: diff -r 370e6d73da23 -r f96ce88ff764 Sources/Plugin.cpp --- a/Sources/Plugin.cpp Tue Dec 02 12:09:36 2025 +0100 +++ b/Sources/Plugin.cpp Tue Dec 16 15:00:35 2025 +0100 @@ -38,6 +38,7 @@ #include "IncomingInstanceFilter.h" #include "ReceivedInstanceCallback.h" #include "StorageArea.h" +#include "StorageArea3.h" #include "StorageCommitmentScpCallback.h" #include "PythonModule.h" @@ -753,7 +754,10 @@ FinalizeOnStoredInstanceCallback(); FinalizeIncomingHttpRequestFilter(); FinalizeDicomScpCallbacks(); - + FinalizeStorageArea(); +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 0) + FinalizeStorageArea3(); +#endif displayMemoryUsageStopping_ = true; if (displayMemoryUsageThread_.joinable()) diff -r 370e6d73da23 -r f96ce88ff764 Sources/StorageArea3.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/StorageArea3.cpp Tue Dec 16 15:00:35 2025 +0100 @@ -0,0 +1,381 @@ +/** + * SPDX-FileCopyrightText: 2020-2023 Osimis S.A., 2024-2025 Orthanc Team SRL, 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Python plugin for Orthanc + * Copyright (C) 2020-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 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. + * + * 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 + * along with this program. If not, see . + **/ + + +#include "PythonHeaderWrapper.h" + +#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" +#include "PythonString.h" + +#include "StorageArea3.h" + +#include +#include + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 0) + +static PyObject* createCallback2_ = NULL; +static PyObject* readCallback2_ = NULL; +static PyObject* removeCallback2_ = NULL; + + +static OrthancPluginErrorCode RunCallback(PythonLock& lock, + PyObject* callback, + const PythonObject& args, + const std::string& name) +{ + PythonObject result(lock, PyObject_CallObject(callback, args.GetPyObject())); + + std::string traceback; + if (lock.HasErrorOccurred(traceback)) + { + ORTHANC_PLUGINS_LOG_ERROR("Error in the Python " + name + " callback, traceback:\n" + traceback); + return OrthancPluginErrorCode_Plugin; + } + else + { + return OrthancPluginErrorCode_Success; + } +} + + +// "callable_protocol_args" : "uuid: str, content_type: ContentType, compression_type: CompressionType, content: bytes, dicom_instance: DicomInstance", +// "callable_protocol_return" : "Tuple" // error code + custom data + +static OrthancPluginErrorCode StorageCreate2(OrthancPluginMemoryBuffer* customData, + const char* uuid, + const void* content, + uint64_t size, + OrthancPluginContentType type, + OrthancPluginCompressionType compressionType, + const OrthancPluginDicomInstance* dicomInstance) +{ + try + { + if (createCallback2_ == NULL) + { + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError); + } + + PythonLock lock; + + PythonObject args(lock, PyTuple_New(5)); + PyObject* pDicomInstance; + + { + PythonObject argsDicomInstance(lock, PyTuple_New(2)); + PyTuple_SetItem(argsDicomInstance.GetPyObject(), 0, PyLong_FromSsize_t((intptr_t) dicomInstance)); + PyTuple_SetItem(argsDicomInstance.GetPyObject(), 1, PyBool_FromLong(true /* borrowed, don't destruct */)); + pDicomInstance = PyObject_CallObject((PyObject*) GetOrthancPluginDicomInstanceType(), argsDicomInstance.GetPyObject()); + } + + + PythonString str(lock, uuid); + PyTuple_SetItem(args.GetPyObject(), 0, str.Release()); + PyTuple_SetItem(args.GetPyObject(), 1, PyLong_FromLong(type)); + PyTuple_SetItem(args.GetPyObject(), 2, PyLong_FromLong(compressionType)); + PyTuple_SetItem(args.GetPyObject(), 3, PyBytes_FromStringAndSize(reinterpret_cast(content), size)); + PyTuple_SetItem(args.GetPyObject(), 4, pDicomInstance); + + PythonObject result(lock, PyObject_CallObject(createCallback2_, args.GetPyObject())); + + std::string traceback; + if (lock.HasErrorOccurred(traceback)) + { + ORTHANC_PLUGINS_LOG_ERROR("Error in the Python StorageCreate2 callback, traceback:\n" + traceback); + return OrthancPluginErrorCode_Plugin; + } + else if (!PyTuple_Check(result.GetPyObject()) || PyTuple_Size(result.GetPyObject()) != 2) + { + ORTHANC_PLUGINS_LOG_ERROR("The Python StorageCreate2 callback has not returned a tuple as expected"); + return OrthancPluginErrorCode_Plugin; + } + else + { + PyObject* pyReturnCode = PyTuple_GET_ITEM(result.GetPyObject(), 0); + PyObject* pyTargetBuffer = PyTuple_GET_ITEM(result.GetPyObject(), 1); + + if (!PyLong_Check(pyReturnCode)) + { + ORTHANC_PLUGINS_LOG_ERROR("The Python StorageCreate2 callback has not returned an int as the first element of the return tuple"); + return OrthancPluginErrorCode_Plugin; + } + else if (!PyBytes_Check(pyTargetBuffer) && !Py_IsNone(pyTargetBuffer)) + { + ORTHANC_PLUGINS_LOG_ERROR("The Python StorageCreate2 callback has not returned a byte array as the second element of the return tuple"); + return OrthancPluginErrorCode_Plugin; + } + + OrthancPluginErrorCode returnCode = static_cast(PyLong_AsLong(pyReturnCode)); + + if (returnCode == OrthancPluginErrorCode_Success) + { + if (Py_IsNone(pyTargetBuffer)) // no custom-data, return directly + { + customData = NULL; + return returnCode; + } + + char* pythonBuffer = NULL; + Py_ssize_t pythonSize = 0; + if (PyBytes_AsStringAndSize(pyTargetBuffer, &pythonBuffer, &pythonSize) == 1) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot access the byte buffer returned by the Python StorageCreate2 callback"); + return OrthancPluginErrorCode_Plugin; + } + else if (pythonSize > 0) + { + if (pythonSize > std::numeric_limits::max()) + { + ORTHANC_PLUGINS_LOG_ERROR("StorageCreate2 python callback: The returned custom data array size (" + boost::lexical_cast(pythonSize) + " bytes) is too large"); + return OrthancPluginErrorCode_Plugin; + } + + // The StorageCreate2 must allocate its customData buffer; it will be freed by Orthanc + OrthancPluginErrorCode retAlloc = OrthancPluginCreateMemoryBuffer(OrthancPlugins::GetGlobalContext(), + customData, static_cast(pythonSize)); + + if (retAlloc != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_LOG_ERROR("StorageCreate2 python callback: Failed to allocate customData buffer: " + boost::lexical_cast(retAlloc) + ")"); + return OrthancPluginErrorCode_Plugin; + } + + memcpy(customData->data, reinterpret_cast(pythonBuffer), customData->size); + return OrthancPluginErrorCode_Success; + } + } + else + { + ORTHANC_PLUGINS_LOG_ERROR("The Python StorageCreate2 callback returned " + boost::lexical_cast(returnCode)); + return returnCode; + } + } + } + catch (OrthancPlugins::PluginException& e) + { + return e.GetErrorCode(); + } + + return OrthancPluginErrorCode_Plugin; +} + + +// "callable_protocol_args" : "uuid: str, content_type: ContentType, range_start: int, size: int, custom_data: bytes", +// "callable_protocol_return" : "Tuple" ErrorCode, target + +static OrthancPluginErrorCode StorageReadRange2(OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + uint64_t rangeStart, + const void* customData, + uint32_t customDataSize) +{ + try + { + if (readCallback2_ == NULL) + { + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError); + } + + PythonLock lock; + + PythonObject args(lock, PyTuple_New(5)); + + PythonString str(lock, uuid); + PyTuple_SetItem(args.GetPyObject(), 0, str.Release()); + PyTuple_SetItem(args.GetPyObject(), 1, PyLong_FromLong(type)); + PyTuple_SetItem(args.GetPyObject(), 2, PyLong_FromLong(rangeStart)); + PyTuple_SetItem(args.GetPyObject(), 3, PyLong_FromLong(target->size)); + PyTuple_SetItem(args.GetPyObject(), 4, PyBytes_FromStringAndSize(reinterpret_cast(customData), customDataSize)); + + PythonObject result(lock, PyObject_CallObject(readCallback2_, args.GetPyObject())); + + std::string traceback; + if (lock.HasErrorOccurred(traceback)) + { + ORTHANC_PLUGINS_LOG_ERROR("Error in the Python StorageReadRange2 callback, traceback:\n" + traceback); + return OrthancPluginErrorCode_Plugin; + } + else if (!PyTuple_Check(result.GetPyObject()) || PyTuple_Size(result.GetPyObject()) != 2) + { + ORTHANC_PLUGINS_LOG_ERROR("The Python StorageReadRange2 callback has not returned a tuple as expected"); + return OrthancPluginErrorCode_Plugin; + } + else + { + PyObject* pyReturnCode = PyTuple_GET_ITEM(result.GetPyObject(), 0); + PyObject* pyTargetBuffer = PyTuple_GET_ITEM(result.GetPyObject(), 1); + + if (!PyLong_Check(pyReturnCode)) + { + ORTHANC_PLUGINS_LOG_ERROR("The Python StorageReadRange2 callback has not returned an int as the first element of the return tuple"); + return OrthancPluginErrorCode_Plugin; + } + else if (!PyBytes_Check(pyTargetBuffer)) + { + ORTHANC_PLUGINS_LOG_ERROR("The Python StorageReadRange2 callback has not returned a byte array as the second element of the return tuple"); + return OrthancPluginErrorCode_Plugin; + } + + OrthancPluginErrorCode returnCode = static_cast(PyLong_AsLong(pyReturnCode)); + + if (returnCode == OrthancPluginErrorCode_Success) + { + char* pythonBuffer = NULL; + Py_ssize_t pythonSize = 0; + if (PyBytes_AsStringAndSize(pyTargetBuffer, &pythonBuffer, &pythonSize) == 1) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot access the byte buffer returned by the Python StorageReadRange2 callback"); + return OrthancPluginErrorCode_Plugin; + } + else + { + // The StorageReadRange2 uses a target that has already been allocated by orthanc + if (static_cast(pythonSize) == target->size) + { + memcpy(target->data, reinterpret_cast(pythonBuffer), target->size); + return OrthancPluginErrorCode_Success; + } + else + { + ORTHANC_PLUGINS_LOG_ERROR("The returned bytes array size (" + boost::lexical_cast(pythonSize) + " bytes) is not equal to the requested size ( " + boost::lexical_cast(target->size) + " bytes) in the Python StorageReadRange2 callback"); + return OrthancPluginErrorCode_Plugin; + } + } + } + else + { + ORTHANC_PLUGINS_LOG_ERROR("The Python StorageReadRange2 callback returned " + boost::lexical_cast(returnCode)); + return returnCode; + } + } + } + catch (OrthancPlugins::PluginException& e) + { + return e.GetErrorCode(); + } +} + +// "callable_protocol_args" : "uuid: str, content_type: ContentType, custom_data: bytes", +// "callable_protocol_return" : "ErrorCode" + +static OrthancPluginErrorCode StorageRemove2(const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint32_t customDataSize) +{ + try + { + if (removeCallback2_ == NULL) + { + throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError); + } + + PythonLock lock; + + PythonObject args(lock, PyTuple_New(3)); + + PythonString str(lock, uuid); + PyTuple_SetItem(args.GetPyObject(), 0, str.Release()); + PyTuple_SetItem(args.GetPyObject(), 1, PyLong_FromLong(type)); + PyTuple_SetItem(args.GetPyObject(), 2, PyBytes_FromStringAndSize(reinterpret_cast(customData), customDataSize)); + + return RunCallback(lock, removeCallback2_, args, "StorageRemove2"); + } + catch (OrthancPlugins::PluginException& e) + { + return e.GetErrorCode(); + } +} + + +PyObject* RegisterStorageArea3(PyObject* module, PyObject* args) +{ + // The GIL is locked at this point (no need to create "PythonLock") + + PyObject* a = NULL; + PyObject* b = NULL; + PyObject* c = NULL; + + if (!PyArg_ParseTuple(args, "OOO", &a, &b, &c) || + a == NULL || + b == NULL || + c == NULL) + { + PyErr_SetString(PyExc_ValueError, "Expected three callback functions to register a custom storage area"); + return NULL; + } + else if (createCallback2_ != NULL || + readCallback2_ != NULL || + removeCallback2_ != NULL) + { + PyErr_SetString(PyExc_RuntimeError, "Cannot register twice a custom storage area"); + return NULL; + } + else + { + ORTHANC_PLUGINS_LOG_INFO("Registering a custom storage area in Python"); + + OrthancPluginRegisterStorageArea3(OrthancPlugins::GetGlobalContext(), + StorageCreate2, StorageReadRange2, StorageRemove2); + + createCallback2_ = a; + Py_XINCREF(createCallback2_); + + readCallback2_ = b; + Py_XINCREF(readCallback2_); + + removeCallback2_ = c; + Py_XINCREF(removeCallback2_); + + Py_INCREF(Py_None); + return Py_None; + } +} + + +void FinalizeStorageArea3() +{ + PythonLock lock; + + if (createCallback2_ != NULL) + { + Py_XDECREF(createCallback2_); + } + + if (readCallback2_ != NULL) + { + Py_XDECREF(readCallback2_); + } + + if (removeCallback2_ != NULL) + { + Py_XDECREF(removeCallback2_); + } +} + +#endif \ No newline at end of file diff -r 370e6d73da23 -r f96ce88ff764 Sources/StorageArea3.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/StorageArea3.h Tue Dec 16 15:00:35 2025 +0100 @@ -0,0 +1,35 @@ +/** + * SPDX-FileCopyrightText: 2020-2023 Osimis S.A., 2024-2025 Orthanc Team SRL, 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Python plugin for Orthanc + * Copyright (C) 2020-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 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. + * + * 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 + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 0) + +void FinalizeStorageArea3(); + +#endif