Skip to content
Commits on Source (5)
repo: 3959d33612ccaadc0d4d707227fbed09ac35e5fe
node: 169e0920634088ba788ac5816ec96c2b1739e1c8
branch: Orthanc-1.5.2
node: 6f5e38ec1f120b8c0ac9c7bfaeafd50ef819d863
branch: Orthanc-1.5.3
latesttag: dcmtk-3.6.1
latesttagdistance: 660
changessincelatesttag: 768
latesttagdistance: 677
changessincelatesttag: 786
......@@ -150,7 +150,35 @@ namespace Orthanc
DcmDataset* DicomFindAnswers::ExtractDcmDataset(size_t index) const
{
return new DcmDataset(*GetAnswer(index).GetDcmtkObject().getDataset());
// As "DicomFindAnswers" stores its content using class
// "ParsedDicomFile" (that internally uses "DcmFileFormat" from
// DCMTK), the dataset can contain tags that are reserved if
// storing the media on the disk, notably tag
// "MediaStorageSOPClassUID" (0002,0002). In this function, we
// remove all those tags whose group is below 0x0008. The
// resulting data set is clean for emission in the C-FIND SCP.
// http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3
// https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ
DcmDataset& source = *GetAnswer(index).GetDcmtkObject().getDataset();
std::auto_ptr<DcmDataset> target(new DcmDataset);
for (unsigned long i = 0; i < source.card(); i++)
{
const DcmElement* element = source.getElement(i);
assert(element != NULL);
if (element != NULL &&
element->getTag().getGroup() >= 0x0008 &&
element->getTag().getElement() != 0x0000)
{
target->insert(dynamic_cast<DcmElement*>(element->clone()));
}
}
return target.release();
}
......
......@@ -2055,7 +2055,7 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
if (output.type() != Json::objectValue)
{
throw OrthancException(ErrorCode_LuaBadOutput,
"Lua: IncomingFindRequestFilter must return a table");
"Lua: The script must return a table");
}
Json::Value::Members members = output.getMemberNames();
......@@ -2065,7 +2065,7 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64Converter, Float64, DcmFloatingPointDoubl
if (output[members[i]].type() != Json::stringValue)
{
throw OrthancException(ErrorCode_LuaBadOutput,
"Lua: IncomingFindRequestFilter must return a table "
"Lua: The script must return a table "
"mapping names of DICOM tags to strings");
}
......
......@@ -61,6 +61,13 @@ namespace Orthanc
Initialize(path);
}
FilesystemHttpSender(const std::string& path,
MimeType contentType)
{
SetContentType(contentType);
Initialize(path);
}
FilesystemHttpSender(const FilesystemStorage& storage,
const std::string& uuid)
{
......
......@@ -407,12 +407,6 @@ namespace Orthanc
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
if (keepAlive_)
{
throw OrthancException(ErrorCode_NotImplemented,
"Multipart answers are not implemented together with keep-alive connections");
}
if (state_ != State_WritingHeader)
{
throw OrthancException(ErrorCode_BadSequenceOfCalls);
......@@ -428,6 +422,20 @@ namespace Orthanc
std::string header = "HTTP/1.1 200 OK\r\n";
if (keepAlive_)
{
#if ORTHANC_ENABLE_MONGOOSE == 1
throw OrthancException(ErrorCode_NotImplemented,
"Multipart answers are not implemented together "
"with keep-alive connections if using Mongoose");
#else
// Turn off Keep-Alive for multipart answers
// https://github.com/civetweb/civetweb/issues/727
stream_.DisableKeepAlive();
header += "Connection: close\r\n";
#endif
}
// Possibly add the cookies
for (std::list<std::string>::const_iterator
it = headers_.begin(); it != headers_.end(); ++it)
......
......@@ -108,6 +108,16 @@ namespace Orthanc
{
// Ignore this
}
virtual void DisableKeepAlive()
{
#if ORTHANC_ENABLE_MONGOOSE == 1
throw OrthancException(ErrorCode_NotImplemented,
"Only available if using CivetWeb");
#elif ORTHANC_ENABLE_CIVETWEB == 1
mg_disable_keep_alive(connection_);
#endif
}
};
......
......@@ -50,5 +50,9 @@ namespace Orthanc
virtual void OnHttpStatusReceived(HttpStatus status) = 0;
virtual void Send(bool isHeader, const void* buffer, size_t length) = 0;
// Disable HTTP keep alive for this single HTTP connection. Must
// be called before sending the "HTTP/1.1 200 OK" header.
virtual void DisableKeepAlive() = 0;
};
}
......@@ -54,6 +54,10 @@ namespace Orthanc
virtual void Send(bool isHeader, const void* buffer, size_t length);
virtual void DisableKeepAlive()
{
}
void GetOutput(std::string& output);
};
}
......@@ -755,9 +755,11 @@ namespace Orthanc
{
if (!GetStateInternal(state, id))
{
// Job has finished and has been lost (should not happen)
state = JobState_Failure;
break;
// Job has finished and has been lost (typically happens if
// "JobsHistorySize" is 0)
throw OrthancException(ErrorCode_InexistentItem,
"Cannot retrieve the status of the job, "
"make sure that \"JobsHistorySize\" is not 0");
}
else if (state == JobState_Failure)
{
......
......@@ -2,6 +2,24 @@ Pending changes in the mainline
===============================
Version 1.5.3 (2019-01-25)
==========================
General
-------
* New configuration option: "SaveJobs" to specify whether jobs are stored in the database
Maintenance
-----------
* Don't return tags whose group is below 0x0008 in C-FIND SCP answers
* Fix compatibility with DICOMweb plugin (allow multipart answers over HTTP Keep-Alive)
* Fix issue #73 (/modalities/{modalityId}/store raises 500 errors instead of 404)
* Fix issue #90 (C-Find shall match missing tags to null/empty string)
* Fix issue #119 (/patients/.../archive returns a 500 when JobsHistorySize is 0)
* Fix issue #128 (Asynchronous C-MOVE: invalid number of remaining sub-operations)
Version 1.5.2 (2019-01-18)
==========================
......@@ -26,7 +44,7 @@ Plugins
Maintenance
-----------
* Don't consider tags whose group is below 0x0008 in C-FIND SCP
* Ignore tags whose group is below 0x0008 in C-FIND SCP requests
* Compatibility with DCMTK 3.6.4
* Fix issue #21 (DICOM files missing after uploading with Firefox)
* Fix issue #32 (HTTP keep-alive is now enabled by default)
......
......@@ -50,6 +50,14 @@
</div>
</div>
<div data-role="content">
<div data-role="content" id="content" style="padding:0px">
<p align="center">
<a href="http://www.orthanc-server.com/" target="_blank" alt="Orthanc homepage">
<img src="orthanc-logo.png" alt="Orthanc" style="max-width:100%" />
</a>
</p>
</div>
<form data-ajax="false" id="lookup-form">
<div data-role="fieldcontain">
<label for="lookup-patient-id">Patient ID:</label>
......
var pendingUploads = [];
var currentUpload = 0;
var totalUpload = 0;
var alreadyInitialized = false; // trying to debug Orthanc issue #1
$(document).ready(function() {
if (alreadyInitialized) {
console.log("Orthanc issue #1: the fileupload has been initialized twice !");
} else {
alreadyInitialized = true;
}
// Initialize the jQuery File Upload widget:
$('#fileupload').fileupload({
//dataType: 'json',
......@@ -27,7 +34,7 @@ $(document).ready(function() {
$('#progress .label').text('Failure');
})
.bind('fileuploaddrop', function (e, data) {
console.log("dropped " + data.files.length + " files");
console.log("dropped " + data.files.length + " files: ", data);
appendFilesToUploadList(data.files);
})
.bind('fileuploadsend', function (e, data) {
......
......@@ -639,7 +639,7 @@ namespace Orthanc
std::string value = element.GetValue().GetContent();
if (value.size() == 0)
{
// An empty string corresponds to a "*" wildcard constraint, so we ignore it
// An empty string corresponds to an universal constraint, so we ignore it
continue;
}
......
......@@ -125,6 +125,7 @@ namespace Orthanc
ServerContext& context_;
std::auto_ptr<DicomModalityStoreJob> job_;
size_t position_;
size_t countInstances_;
public:
AsynchronousMove(ServerContext& context,
......@@ -156,6 +157,8 @@ namespace Orthanc
std::list<std::string> tmp;
context_.GetIndex().GetChildInstances(tmp, publicId);
countInstances_ = tmp.size();
job_->Reserve(tmp.size());
for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
......@@ -166,20 +169,23 @@ namespace Orthanc
virtual unsigned int GetSubOperationCount() const
{
return 1;
return countInstances_;
}
virtual Status DoNext()
{
if (position_ == 0)
if (position_ >= countInstances_)
{
context_.GetJobsEngine().GetRegistry().Submit(job_.release(), 0 /* priority */);
return Status_Success;
return Status_Failure;
}
else
if (position_ == 0)
{
return Status_Failure;
context_.GetJobsEngine().GetRegistry().Submit(job_.release(), 0 /* priority */);
}
position_ ++;
return Status_Success;
}
};
}
......
......@@ -144,8 +144,7 @@ namespace Orthanc
(publicContent, job.release(), priority))
{
// The archive is now created: Prepare the sending of the ZIP file
FilesystemHttpSender sender(tmp->GetPath());
sender.SetContentType(MimeType_Gzip);
FilesystemHttpSender sender(tmp->GetPath(), MimeType_Zip);
sender.SetContentFilename(filename);
// Send the ZIP
......
......@@ -813,7 +813,7 @@ namespace Orthanc
* DICOM C-Store SCU
***************************************************************************/
static bool GetInstancesToExport(Json::Value& otherArguments,
static void GetInstancesToExport(Json::Value& otherArguments,
SetOfInstancesJob& job,
const std::string& remote,
RestApiPostCall& call)
......@@ -834,7 +834,7 @@ namespace Orthanc
else if (!call.ParseJsonRequest(request))
{
// Bad JSON request
return false;
throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON value");
}
if (request.isString())
......@@ -843,6 +843,11 @@ namespace Orthanc
request = Json::arrayValue;
request.append(item);
}
else if (!request.isArray() &&
!request.isObject())
{
throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object, or a JSON array of strings");
}
const Json::Value* resources;
if (request.isArray())
......@@ -854,13 +859,15 @@ namespace Orthanc
if (request.type() != Json::objectValue ||
!request.isMember(KEY_RESOURCES))
{
return false;
throw OrthancException(ErrorCode_BadFileFormat,
"Missing field in JSON: \"" + std::string(KEY_RESOURCES) + "\"");
}
resources = &request[KEY_RESOURCES];
if (!resources->isArray())
{
return false;
throw OrthancException(ErrorCode_BadFileFormat,
"JSON field \"" + std::string(KEY_RESOURCES) + "\" must contain an array");
}
// Copy the remaining arguments
......@@ -882,24 +889,24 @@ namespace Orthanc
{
if (!(*resources) [i].isString())
{
return false;
throw OrthancException(ErrorCode_BadFileFormat,
"Resources to be exported must be specified as a JSON array of strings");
}
std::string stripped = Toolbox::StripSpaces((*resources) [i].asString());
if (!Toolbox::IsSHA1(stripped))
{
return false;
throw OrthancException(ErrorCode_BadFileFormat,
"This string is not a valid Orthanc identifier: " + stripped);
}
context.AddChildInstances(job, stripped);
if (logExportedResources)
{
context.GetIndex().LogExportedResource(stripped, remote);
}
context.AddChildInstances(job, stripped);
}
return true;
}
......@@ -912,8 +919,8 @@ namespace Orthanc
Json::Value request;
std::auto_ptr<DicomModalityStoreJob> job(new DicomModalityStoreJob(context));
if (GetInstancesToExport(request, *job, remote, call))
{
GetInstancesToExport(request, *job, remote, call);
std::string localAet = Toolbox::GetJsonStringField
(request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle());
std::string moveOriginatorAET = Toolbox::GetJsonStringField
......@@ -932,7 +939,6 @@ namespace Orthanc
OrthancRestApi::GetApi(call).SubmitCommandsJob
(call, job.release(), true /* synchronous by default */, request);
}
}
/***************************************************************************
......@@ -1059,8 +1065,8 @@ namespace Orthanc
Json::Value request;
std::auto_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context));
if (GetInstancesToExport(request, *job, remote, call))
{
GetInstancesToExport(request, *job, remote, call);
OrthancConfiguration::ReaderLock lock;
WebServiceParameters peer;
......@@ -1076,7 +1082,6 @@ namespace Orthanc
"No peer with symbolic name: " + remote);
}
}
}
// DICOM bridge -------------------------------------------------------------
......
......@@ -1415,9 +1415,16 @@ namespace Orthanc
"Tag \"" + members[i] + "\" should be associated with a string");
}
const std::string value = request[KEY_QUERY][members[i]].asString();
if (!value.empty())
{
// An empty string corresponds to an universal constraint,
// so we ignore it. This mimics the behavior of class
// "OrthancFindRequestHandler"
query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]),
request[KEY_QUERY][members[i]].asString(),
caseSensitive, true);
value, caseSensitive, true);
}
}
FindVisitor visitor;
......
......@@ -49,6 +49,9 @@ namespace Orthanc
const std::string& dicomQuery,
bool caseSensitive,
bool mandatoryTag);
void AddConstraint(DicomTagConstraint* constraint); // Takes ownership
public:
DatabaseLookup()
{
......@@ -68,8 +71,6 @@ namespace Orthanc
const DicomTagConstraint& GetConstraint(size_t index) const;
void AddConstraint(DicomTagConstraint* constraint); // Takes ownership
bool IsMatch(const DicomMap& value) const;
bool IsMatch(DcmItem& item,
......
......@@ -195,6 +195,8 @@ namespace Orthanc
void ServerContext::SaveJobsEngine()
{
if (saveJobs_)
{
VLOG(1) << "Serializing the content of the jobs engine";
......@@ -213,6 +215,7 @@ namespace Orthanc
LOG(ERROR) << "Cannot serialize the jobs engine: " << e.What();
}
}
}
ServerContext::ServerContext(IDatabaseWrapper& database,
......@@ -245,6 +248,7 @@ namespace Orthanc
new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1)));
defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2));
saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true);
}
jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
......