/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2021 Osimis S.A., 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 "StorageBackend.h"
#if HAS_ORTHANC_EXCEPTION != 1
# error HAS_ORTHANC_EXCEPTION must be set to 1
#endif
#include "../../Framework/Common/BinaryStringValue.h"
#include "../../Framework/Common/ResultFileValue.h"
#include // For std::unique_ptr<>
#include
#include
#include
#include
#include
#define ORTHANC_PLUGINS_DATABASE_CATCH \
catch (::Orthanc::OrthancException& e) \
{ \
return static_cast(e.GetErrorCode()); \
} \
catch (::std::runtime_error& e) \
{ \
std::string s = "Exception in storage area back-end: " + std::string(e.what()); \
OrthancPluginLogError(context_, s.c_str()); \
return OrthancPluginErrorCode_DatabasePlugin; \
} \
catch (...) \
{ \
OrthancPluginLogError(context_, "Native exception"); \
return OrthancPluginErrorCode_DatabasePlugin; \
}
namespace OrthancDatabases
{
class StorageBackend::ReadWholeOperation : public StorageBackend::IDatabaseOperation
{
private:
IFileContentVisitor& visitor_;
const char* uuid_;
OrthancPluginContentType type_;
public:
ReadWholeOperation(IFileContentVisitor& visitor,
const char* uuid,
OrthancPluginContentType type) :
visitor_(visitor),
uuid_(uuid),
type_(type)
{
}
virtual void Execute(StorageBackend::IAccessor& accessor) ORTHANC_OVERRIDE
{
accessor.ReadWhole(visitor_, uuid_, type_);
}
};
StorageBackend::StorageBackend(IDatabaseFactory* factory,
unsigned int maxRetries) :
manager_(factory),
maxRetries_(maxRetries)
{
}
void StorageBackend::AccessorBase::Create(const std::string& uuid,
const void* content,
size_t size,
OrthancPluginContentType type)
{
DatabaseManager::Transaction transaction(manager_, TransactionType_ReadWrite);
{
DatabaseManager::CachedStatement statement(
STATEMENT_FROM_HERE, manager_,
"INSERT INTO StorageArea VALUES (${uuid}, ${content}, ${type})");
statement.SetParameterType("uuid", ValueType_Utf8String);
statement.SetParameterType("content", ValueType_InputFile);
statement.SetParameterType("type", ValueType_Integer64);
Dictionary args;
args.SetUtf8Value("uuid", uuid);
args.SetFileValue("content", content, size);
args.SetIntegerValue("type", type);
statement.Execute(args);
}
transaction.Commit();
}
void StorageBackend::AccessorBase::ReadWhole(StorageBackend::IFileContentVisitor& visitor,
const std::string& uuid,
OrthancPluginContentType type)
{
DatabaseManager::Transaction transaction(manager_, TransactionType_ReadOnly);
{
DatabaseManager::CachedStatement statement(
STATEMENT_FROM_HERE, manager_,
"SELECT content FROM StorageArea WHERE uuid=${uuid} AND type=${type}");
statement.SetParameterType("uuid", ValueType_Utf8String);
statement.SetParameterType("type", ValueType_Integer64);
Dictionary args;
args.SetUtf8Value("uuid", uuid);
args.SetIntegerValue("type", type);
statement.Execute(args);
if (statement.IsDone())
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
}
else if (statement.GetResultFieldsCount() != 1)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
}
else
{
const IValue& value = statement.GetResultField(0);
switch (value.GetType())
{
case ValueType_ResultFile:
{
std::string content;
dynamic_cast(value).ReadWhole(content);
visitor.Assign(content);
break;
}
case ValueType_BinaryString:
visitor.Assign(dynamic_cast(value).GetContent());
break;
default:
throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
}
}
}
transaction.Commit();
if (!visitor.IsSuccess())
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Could not read attachment from the storage area");
}
}
void StorageBackend::AccessorBase::ReadRange(IFileContentVisitor& visitor,
const std::string& uuid,
OrthancPluginContentType type,
uint64_t start,
size_t length)
{
/**
* This is a generic implementation, that will only work if
* "ResultFileValue" is implemented by the database backend. For
* instance, this will *not* work with MySQL, as the latter uses
* BLOB columns to store files.
**/
DatabaseManager::Transaction transaction(manager_, TransactionType_ReadOnly);
{
DatabaseManager::CachedStatement statement(
STATEMENT_FROM_HERE, manager_,
"SELECT content FROM StorageArea WHERE uuid=${uuid} AND type=${type}");
statement.SetParameterType("uuid", ValueType_Utf8String);
statement.SetParameterType("type", ValueType_Integer64);
Dictionary args;
args.SetUtf8Value("uuid", uuid);
args.SetIntegerValue("type", type);
statement.Execute(args);
if (statement.IsDone())
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
}
else if (statement.GetResultFieldsCount() != 1)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
}
else
{
const IValue& value = statement.GetResultField(0);
if (value.GetType() == ValueType_ResultFile)
{
std::string content;
dynamic_cast(value).ReadRange(content, start, length);
visitor.Assign(content);
}
else
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
}
}
}
transaction.Commit();
if (!visitor.IsSuccess())
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Could not read attachment from the storage area");
}
}
void StorageBackend::AccessorBase::Remove(const std::string& uuid,
OrthancPluginContentType type)
{
DatabaseManager::Transaction transaction(manager_, TransactionType_ReadWrite);
{
DatabaseManager::CachedStatement statement(
STATEMENT_FROM_HERE, manager_,
"DELETE FROM StorageArea WHERE uuid=${uuid} AND type=${type}");
statement.SetParameterType("uuid", ValueType_Utf8String);
statement.SetParameterType("type", ValueType_Integer64);
Dictionary args;
args.SetUtf8Value("uuid", uuid);
args.SetIntegerValue("type", type);
statement.Execute(args);
}
transaction.Commit();
}
static OrthancPluginContext* context_ = NULL;
static std::unique_ptr backend_;
static OrthancPluginErrorCode StorageCreate(const char* uuid,
const void* content,
int64_t size,
OrthancPluginContentType type)
{
class Operation : public StorageBackend::IDatabaseOperation
{
private:
const char* uuid_;
const void* content_;
int64_t size_;
OrthancPluginContentType type_;
public:
Operation(const char* uuid,
const void* content,
int64_t size,
OrthancPluginContentType type) :
uuid_(uuid),
content_(content),
size_(size),
type_(type)
{
}
virtual void Execute(StorageBackend::IAccessor& accessor) ORTHANC_OVERRIDE
{
accessor.Create(uuid_, content_, size_, type_);
}
};
try
{
if (backend_.get() == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
}
else
{
Operation operation(uuid, content, size, type);
backend_->Execute(operation);
return OrthancPluginErrorCode_Success;
}
}
ORTHANC_PLUGINS_DATABASE_CATCH;
}
#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 0)
static OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target,
const char* uuid,
OrthancPluginContentType type)
{
class Visitor : public StorageBackend::IFileContentVisitor
{
private:
OrthancPluginMemoryBuffer64* target_;
bool success_;
public:
Visitor(OrthancPluginMemoryBuffer64* target) :
target_(target),
success_(false)
{
if (target == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
}
}
virtual bool IsSuccess() const ORTHANC_OVERRIDE
{
return success_;
}
virtual void Assign(const std::string& content) ORTHANC_OVERRIDE
{
if (success_)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
}
else
{
assert(context_ != NULL);
if (OrthancPluginCreateMemoryBuffer64(context_, target_, static_cast(content.size())) !=
OrthancPluginErrorCode_Success)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory);
}
if (!content.empty())
{
memcpy(target_->data, content.c_str(), content.size());
}
success_ = true;
}
}
};
try
{
if (backend_.get() == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
}
else
{
Visitor visitor(target);
{
StorageBackend::ReadWholeOperation operation(visitor, uuid, type);
backend_->Execute(operation);
}
return OrthancPluginErrorCode_Success;
}
}
ORTHANC_PLUGINS_DATABASE_CATCH;
}
static OrthancPluginErrorCode StorageReadRange(OrthancPluginMemoryBuffer64* target,
const char* uuid,
OrthancPluginContentType type,
uint64_t start)
{
class Visitor : public StorageBackend::IFileContentVisitor
{
private:
OrthancPluginMemoryBuffer64* target_; // This buffer is already allocated by the Orthanc core
bool success_;
public:
Visitor(OrthancPluginMemoryBuffer64* target) :
target_(target),
success_(false)
{
if (target == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
}
}
virtual bool IsSuccess() const ORTHANC_OVERRIDE
{
return success_;
}
virtual void Assign(const std::string& content) ORTHANC_OVERRIDE
{
if (success_)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
}
else
{
if (content.size() != target_->size)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
}
if (!content.empty())
{
memcpy(target_->data, content.c_str(), content.size());
}
success_ = true;
}
}
};
class Operation : public StorageBackend::IDatabaseOperation
{
private:
Visitor& visitor_;
const char* uuid_;
OrthancPluginContentType type_;
uint64_t start_;
size_t length_;
public:
Operation(Visitor& visitor,
const char* uuid,
OrthancPluginContentType type,
uint64_t start,
size_t length) :
visitor_(visitor),
uuid_(uuid),
type_(type),
start_(start),
length_(length)
{
}
virtual void Execute(StorageBackend::IAccessor& accessor) ORTHANC_OVERRIDE
{
accessor.ReadRange(visitor_, uuid_, type_, start_, length_);
}
};
try
{
if (backend_.get() == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
}
else
{
Visitor visitor(target);
{
Operation operation(visitor, uuid, type, start, target->size);
backend_->Execute(operation);
}
return OrthancPluginErrorCode_Success;
}
}
ORTHANC_PLUGINS_DATABASE_CATCH;
}
# endif
#endif
static OrthancPluginErrorCode StorageRead(void** data,
int64_t* size,
const char* uuid,
OrthancPluginContentType type)
{
class Visitor : public StorageBackend::IFileContentVisitor
{
private:
void** data_;
int64_t* size_;
bool success_;
public:
Visitor(void** data,
int64_t* size) :
data_(data),
size_(size),
success_(false)
{
}
~Visitor()
{
if (data_ != NULL /* this condition is invalidated by "Release()" */ &&
*data_ != NULL)
{
free(*data_);
}
}
void Release()
{
data_ = NULL;
}
virtual bool IsSuccess() const ORTHANC_OVERRIDE
{
return success_;
}
virtual void Assign(const std::string& content) ORTHANC_OVERRIDE
{
if (success_)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
}
else if (data_ == NULL)
{
// "Release()" has been called
throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
}
else
{
if (content.empty())
{
*data_ = NULL;
*size_ = 0;
}
else
{
*size_ = static_cast(content.size());
if (static_cast(*size_) != content.size())
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory,
"File cannot be stored in a 63bit buffer");
}
*data_ = malloc(*size_);
if (*data_ == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory);
}
memcpy(*data_, content.c_str(), *size_);
}
success_ = true;
}
}
};
try
{
if (backend_.get() == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
}
else if (data == NULL ||
size == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
}
else
{
Visitor visitor(data, size);
{
StorageBackend::ReadWholeOperation operation(visitor, uuid, type);
backend_->Execute(operation);
}
visitor.Release();
return OrthancPluginErrorCode_Success;
}
}
ORTHANC_PLUGINS_DATABASE_CATCH;
}
static OrthancPluginErrorCode StorageRemove(const char* uuid,
OrthancPluginContentType type)
{
class Operation : public StorageBackend::IDatabaseOperation
{
private:
const char* uuid_;
OrthancPluginContentType type_;
public:
Operation(const char* uuid,
OrthancPluginContentType type) :
uuid_(uuid),
type_(type)
{
}
virtual void Execute(StorageBackend::IAccessor& accessor) ORTHANC_OVERRIDE
{
accessor.Remove(uuid_, type_);
}
};
try
{
if (backend_.get() == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
}
else
{
Operation operation(uuid, type);
backend_->Execute(operation);
return OrthancPluginErrorCode_Success;
}
}
ORTHANC_PLUGINS_DATABASE_CATCH;
}
void StorageBackend::Register(OrthancPluginContext* context,
StorageBackend* backend)
{
if (context == NULL ||
backend == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
}
else if (context_ != NULL ||
backend_.get() != NULL)
{
// This function can only be invoked once in the plugin
throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
}
else
{
context_ = context;
backend_.reset(backend);
bool hasLoadedV2 = false;
#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) // Macro introduced in Orthanc 1.3.1
# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 0)
if (OrthancPluginCheckVersionAdvanced(context, 1, 9, 0) == 1)
{
OrthancPluginStorageReadRange readRange = NULL;
if (backend_->HasReadRange())
{
readRange = StorageReadRange;
}
OrthancPluginRegisterStorageArea2(context_, StorageCreate, StorageReadWhole, readRange, StorageRemove);
hasLoadedV2 = true;
}
# endif
#endif
if (!hasLoadedV2)
{
LOG(WARNING) << "Performance warning: Your version of the Orthanc core or SDK doesn't support reading of file ranges";
OrthancPluginRegisterStorageArea(context_, StorageCreate, StorageRead, StorageRemove);
}
LOG(WARNING) << "The storage area plugin will retry up to " << backend_->GetMaxRetries()
<< " time(s) in the case of a collision";
}
}
void StorageBackend::Finalize()
{
backend_.reset(NULL);
context_ = NULL;
}
class StorageBackend::StringVisitor : public StorageBackend::IFileContentVisitor
{
private:
std::string& target_;
bool success_;
public:
explicit StringVisitor(std::string& target) :
target_(target),
success_(false)
{
}
virtual bool IsSuccess() const ORTHANC_OVERRIDE
{
return success_;
}
virtual void Assign(const std::string& content) ORTHANC_OVERRIDE
{
if (success_)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
}
else
{
target_.assign(content);
success_ = true;
}
}
};
void StorageBackend::ReadWholeToString(std::string& target,
IAccessor& accessor,
const std::string& uuid,
OrthancPluginContentType type)
{
StringVisitor visitor(target);
accessor.ReadWhole(visitor, uuid, type);
if (!visitor.IsSuccess())
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
}
}
void StorageBackend::ReadRangeToString(std::string& target,
IAccessor& accessor,
const std::string& uuid,
OrthancPluginContentType type,
uint64_t start,
size_t length)
{
StringVisitor visitor(target);
accessor.ReadRange(visitor, uuid, type, start, length);
if (!visitor.IsSuccess())
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
}
}
void StorageBackend::Execute(IDatabaseOperation& operation)
{
std::unique_ptr accessor(CreateAccessor());
if (accessor.get() == NULL)
{
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
}
#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 2)
unsigned int attempt = 0;
#endif
for (;;)
{
try
{
operation.Execute(*accessor);
return; // Success
}
catch (Orthanc::OrthancException& e)
{
#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 2)
if (e.GetErrorCode() == Orthanc::ErrorCode_DatabaseCannotSerialize)
{
if (attempt >= maxRetries_)
{
throw;
}
else
{
attempt++;
// The "rand()" adds some jitter to de-synchronize writers
boost::this_thread::sleep(boost::posix_time::milliseconds(100 * attempt + 5 * (rand() % 10)));
}
}
else
{
throw;
}
#else
throw;
#endif
}
}
}
}