# HG changeset patch # User Sebastien Jodogne # Date 1354274324 -3600 # Node ID 8098448bd8273b769c2faf48afca49ddf73cccdc # Parent ae2367145b49b1a34574275fa251e6d20784ae10 export log diff -r ae2367145b49 -r 8098448bd827 Core/RestApi/RestApi.h --- a/Core/RestApi/RestApi.h Fri Nov 30 11:01:29 2012 +0100 +++ b/Core/RestApi/RestApi.h Fri Nov 30 12:18:44 2012 +0100 @@ -72,13 +72,13 @@ } std::string GetUriComponent(const std::string& name, - const std::string& defaultValue) + const std::string& defaultValue) const { return HttpHandler::GetArgument(*uriComponents_, name, defaultValue); } std::string GetHttpHeader(const std::string& name, - const std::string& defaultValue) + const std::string& defaultValue) const { return HttpHandler::GetArgument(*httpHeaders_, name, defaultValue); } @@ -95,7 +95,7 @@ public: std::string GetArgument(const std::string& name, - const std::string& defaultValue) + const std::string& defaultValue) const { return HttpHandler::GetArgument(*getArguments_, name, defaultValue); } diff -r ae2367145b49 -r 8098448bd827 NEWS --- a/NEWS Fri Nov 30 11:01:29 2012 +0100 +++ b/NEWS Fri Nov 30 12:18:44 2012 +0100 @@ -9,13 +9,15 @@ * The patient/study/series/instances are now indexed by SHA-1 digests of their DICOM Instance IDs (and not by UUIDs anymore): The same DICOM objects are thus always identified by the same Orthanc IDs +* Log of exported instances through DICOM C-Store SCU ("/exported" URI) Minor changes ------------- * Generate a sample configuration file from command line * "CompletedSeries" event in the changes API -* Thread to continuously flush DB to disk (robustness against reboots) +* Thread to continuously flush DB to disk (SQLite checkpoints for + robustness against reboots) Version 0.2.3 (2012/10/26) diff -r ae2367145b49 -r 8098448bd827 OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Fri Nov 30 11:01:29 2012 +0100 +++ b/OrthancServer/DatabaseWrapper.cpp Fri Nov 30 12:18:44 2012 +0100 @@ -554,20 +554,89 @@ } - void DatabaseWrapper::LogExportedInstance(const std::string& remoteModality, - DicomInstanceHasher& hasher, + void DatabaseWrapper::LogExportedResource(ResourceType resourceType, + const std::string& publicId, + const std::string& remoteModality, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid, const boost::posix_time::ptime& date) { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO ExportedInstances VALUES(NULL, ?, ?, ?, ?, ?, ?)"); - s.BindString(0, remoteModality); - s.BindString(1, hasher.HashInstance()); - s.BindString(2, hasher.GetPatientId()); - s.BindString(3, hasher.GetStudyUid()); - s.BindString(4, hasher.GetSeriesUid()); - s.BindString(5, hasher.GetInstanceUid()); - s.BindString(6, boost::posix_time::to_iso_string(date)); + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); + + s.BindInt(0, resourceType); + s.BindString(1, publicId); + s.BindString(2, remoteModality); + s.BindString(3, patientId); + s.BindString(4, studyInstanceUid); + s.BindString(5, seriesInstanceUid); + s.BindString(6, sopInstanceUid); + s.BindString(7, boost::posix_time::to_iso_string(date)); + s.Run(); } + + + void DatabaseWrapper::GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt(0, since); + s.BindInt(1, maxResults + 1); + + Json::Value changes = Json::arrayValue; + int64_t last = 0; + + while (changes.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt(0); + ResourceType resourceType = static_cast(s.ColumnInt(1)); + std::string publicId = s.ColumnString(2); + + Json::Value item = Json::objectValue; + item["Seq"] = static_cast(seq); + item["ResourceType"] = ToString(resourceType); + item["ID"] = publicId; + item["Path"] = GetBasePath(resourceType, publicId); + item["RemoteModality"] = s.ColumnString(3); + item["Date"] = s.ColumnString(8); + + // WARNING: Do not add "break" below and do not reorder the case items! + switch (resourceType) + { + case ResourceType_Instance: + item["SopInstanceUid"] = s.ColumnString(7); + + case ResourceType_Series: + item["SeriesInstanceUid"] = s.ColumnString(6); + + case ResourceType_Study: + item["StudyInstanceUid"] = s.ColumnString(5); + + case ResourceType_Patient: + item["PatientId"] = s.ColumnString(4); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + last = seq; + + changes.append(item); + } + + target = Json::objectValue; + target["Changes"] = changes; + target["PendingChanges"] = (changes.size() == maxResults && s.Step()); + target["LastSeq"] = static_cast(last); + } + + int64_t DatabaseWrapper::GetTableRecordCount(const std::string& table) diff -r ae2367145b49 -r 8098448bd827 OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Fri Nov 30 11:01:29 2012 +0100 +++ b/OrthancServer/DatabaseWrapper.h Fri Nov 30 12:18:44 2012 +0100 @@ -163,10 +163,20 @@ int64_t since, unsigned int maxResults); - void LogExportedInstance(const std::string& remoteModality, - DicomInstanceHasher& hasher, - const boost::posix_time::ptime& date = boost::posix_time::second_clock::local_time()); + void LogExportedResource(ResourceType resourceType, + const std::string& publicId, + const std::string& remoteModality, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid, + const boost::posix_time::ptime& date = + boost::posix_time::second_clock::local_time()); + void GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults); + int64_t GetTableRecordCount(const std::string& table); uint64_t GetTotalCompressedSize(); diff -r ae2367145b49 -r 8098448bd827 OrthancServer/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi.cpp Fri Nov 30 11:01:29 2012 +0100 +++ b/OrthancServer/OrthancRestApi.cpp Fri Nov 30 12:18:44 2012 +0100 @@ -242,13 +242,18 @@ { RETRIEVE_CONTEXT(call); + std::string remote = call.GetUriComponent("id", ""); DicomUserConnection connection; - ConnectToModality(connection, call.GetUriComponent("id", "")); + ConnectToModality(connection, remote); + + const std::string& resourceId = call.GetPostBody(); Json::Value found; - if (context.GetIndex().LookupResource(found, call.GetPostBody(), ResourceType_Series)) + if (context.GetIndex().LookupResource(found, resourceId, ResourceType_Series)) { // The UUID corresponds to a series + context.GetIndex().LogExportedResource(resourceId, remote); + for (Json::Value::ArrayIndex i = 0; i < found["Instances"].size(); i++) { std::string instanceId = found["Instances"][i].asString(); @@ -259,12 +264,13 @@ call.GetOutput().AnswerBuffer("{}", "application/json"); } - else if (context.GetIndex().LookupResource(found, call.GetPostBody(), ResourceType_Instance)) + else if (context.GetIndex().LookupResource(found, resourceId, ResourceType_Instance)) { // The UUID corresponds to an instance - std::string instanceId = call.GetPostBody(); + context.GetIndex().LogExportedResource(resourceId, remote); + std::string dicom; - context.ReadFile(dicom, instanceId, AttachedFileType_Dicom); + context.ReadFile(dicom, resourceId, AttachedFileType_Dicom); connection.Store(dicom); call.GetOutput().AnswerBuffer("{}", "application/json"); @@ -273,7 +279,7 @@ { // The POST body is not a known resource, assume that it // contains a raw DICOM instance - connection.Store(call.GetPostBody()); + connection.Store(resourceId); call.GetOutput().AnswerBuffer("{}", "application/json"); } } @@ -342,16 +348,12 @@ // Changes API -------------------------------------------------------------- - static void GetChanges(RestApi::GetCall& call) + static void GetSinceAndLimit(int64_t& since, + unsigned int& limit, + const RestApi::GetCall& call) { - RETRIEVE_CONTEXT(call); - static const unsigned int MAX_RESULTS = 100; - ServerIndex& index = context.GetIndex(); - //std::string filter = GetArgument(getArguments, "filter", ""); - int64_t since; - unsigned int limit; try { since = boost::lexical_cast(call.GetArgument("since", "0")); @@ -366,15 +368,40 @@ { limit = MAX_RESULTS; } + } + + static void GetChanges(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + + //std::string filter = GetArgument(getArguments, "filter", ""); + int64_t since; + unsigned int limit; + GetSinceAndLimit(since, limit, call); Json::Value result; - if (index.GetChanges(result, since, limit)) + if (context.GetIndex().GetChanges(result, since, limit)) { call.GetOutput().AnswerJson(result); } } + static void GetExports(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + + int64_t since; + unsigned int limit; + GetSinceAndLimit(since, limit, call); + + Json::Value result; + if (context.GetIndex().GetExportedResources(result, since, limit)) + { + call.GetOutput().AnswerJson(result); + } + } + // Get information about a single instance ---------------------------------- @@ -592,6 +619,7 @@ Register("/", ServeRoot); Register("/system", GetSystemInformation); Register("/changes", GetChanges); + Register("/exported", GetExports); Register("/instances", UploadDicomFile); Register("/instances", ListResources); diff -r ae2367145b49 -r 8098448bd827 OrthancServer/PrepareDatabase.sql --- a/OrthancServer/PrepareDatabase.sql Fri Nov 30 11:01:29 2012 +0100 +++ b/OrthancServer/PrepareDatabase.sql Fri Nov 30 12:18:44 2012 +0100 @@ -43,10 +43,11 @@ date TEXT ); -CREATE TABLE ExportedInstances( +CREATE TABLE ExportedResources( seq INTEGER PRIMARY KEY AUTOINCREMENT, + resourceType INTEGER, + publicId TEXT, remoteModality TEXT, - publicId TEXT, patientId TEXT, studyInstanceUid TEXT, seriesInstanceUid TEXT, diff -r ae2367145b49 -r 8098448bd827 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Fri Nov 30 11:01:29 2012 +0100 +++ b/OrthancServer/ServerIndex.cpp Fri Nov 30 12:18:44 2012 +0100 @@ -617,4 +617,85 @@ return true; } + + void ServerIndex::LogExportedResource(const std::string& publicId, + const std::string& remoteModality) + { + boost::mutex::scoped_lock lock(mutex_); + + int64_t id; + ResourceType type; + if (!db_->LookupResource(publicId, id, type)) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::string patientId; + std::string studyInstanceUid; + std::string seriesInstanceUid; + std::string sopInstanceUid; + + int64_t currentId = id; + ResourceType currentType = type; + + // Iteratively go up inside the patient/study/series/instance hierarchy + bool done = false; + while (!done) + { + DicomMap map; + db_->GetMainDicomTags(map, currentId); + + switch (currentType) + { + case ResourceType_Patient: + patientId = map.GetValue(DICOM_TAG_PATIENT_ID).AsString(); + done = true; + break; + + case ResourceType_Study: + studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(); + currentType = ResourceType_Patient; + break; + + case ResourceType_Series: + seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(); + currentType = ResourceType_Study; + break; + + case ResourceType_Instance: + sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString(); + currentType = ResourceType_Series; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + // If we have not reached the Patient level, find the parent of + // the current resource + if (!done) + { + assert(db_->LookupParent(currentId, currentId)); + } + } + + // No need for a SQLite::Transaction here, as we only insert 1 record + db_->LogExportedResource(type, + publicId, + remoteModality, + patientId, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid); + } + + + bool ServerIndex::GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults) + { + boost::mutex::scoped_lock lock(mutex_); + db_->GetExportedResources(target, since, maxResults); + return true; + } } diff -r ae2367145b49 -r 8098448bd827 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Fri Nov 30 11:01:29 2012 +0100 +++ b/OrthancServer/ServerIndex.h Fri Nov 30 12:18:44 2012 +0100 @@ -101,5 +101,12 @@ bool GetChanges(Json::Value& target, int64_t since, unsigned int maxResults); + + void LogExportedResource(const std::string& publicId, + const std::string& remoteModality); + + bool GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults); }; }