# HG changeset patch # User Sebastien Jodogne # Date 1355061797 -3600 # Node ID 58f96993372082d677dc322c3737b651405adb11 # Parent 8af8754a7a8e44df7e18f4e8daab7d3ec514c607# Parent 3b3525dee6619f7332fc2cfa017fb6c3f2d5b3d4 merge with Orthanc-0.3.1 diff -r 3b3525dee661 -r 58f969933720 CMakeLists.txt --- a/CMakeLists.txt Sun Dec 09 15:01:00 2012 +0100 +++ b/CMakeLists.txt Sun Dec 09 15:03:17 2012 +0100 @@ -4,7 +4,7 @@ # Version of the build, should always be "mainline" except in release branches add_definitions( - -DORTHANC_VERSION="0.3.1" + -DORTHANC_VERSION="mainline" ) # Parameters of the build diff -r 3b3525dee661 -r 58f969933720 Core/Enumerations.h --- a/Core/Enumerations.h Sun Dec 09 15:01:00 2012 +0100 +++ b/Core/Enumerations.h Sun Dec 09 15:03:17 2012 +0100 @@ -55,7 +55,8 @@ ErrorCode_BadFileFormat, ErrorCode_Timeout, ErrorCode_UnknownResource, - ErrorCode_IncompatibleDatabaseVersion + ErrorCode_IncompatibleDatabaseVersion, + ErrorCode_FullStorage }; enum PixelFormat diff -r 3b3525dee661 -r 58f969933720 Core/OrthancException.cpp --- a/Core/OrthancException.cpp Sun Dec 09 15:01:00 2012 +0100 +++ b/Core/OrthancException.cpp Sun Dec 09 15:03:17 2012 +0100 @@ -93,6 +93,9 @@ case ErrorCode_IncompatibleDatabaseVersion: return "Incompatible version of the database"; + case ErrorCode_FullStorage: + return "The file storage is full"; + case ErrorCode_Custom: default: return "???"; diff -r 3b3525dee661 -r 58f969933720 Core/SQLite/FunctionContext.cpp --- a/Core/SQLite/FunctionContext.cpp Sun Dec 09 15:01:00 2012 +0100 +++ b/Core/SQLite/FunctionContext.cpp Sun Dec 09 15:03:17 2012 +0100 @@ -72,6 +72,12 @@ return sqlite3_value_int(argv_[index]); } + int64_t FunctionContext::GetInt64Value(unsigned int index) const + { + CheckIndex(index); + return sqlite3_value_int64(argv_[index]); + } + double FunctionContext::GetDoubleValue(unsigned int index) const { CheckIndex(index); diff -r 3b3525dee661 -r 58f969933720 Core/SQLite/FunctionContext.h --- a/Core/SQLite/FunctionContext.h Sun Dec 09 15:01:00 2012 +0100 +++ b/Core/SQLite/FunctionContext.h Sun Dec 09 15:03:17 2012 +0100 @@ -69,6 +69,8 @@ int GetIntValue(unsigned int index) const; + int64_t GetInt64Value(unsigned int index) const; + double GetDoubleValue(unsigned int index) const; std::string GetStringValue(unsigned int index) const; diff -r 3b3525dee661 -r 58f969933720 NEWS --- a/NEWS Sun Dec 09 15:01:00 2012 +0100 +++ b/NEWS Sun Dec 09 15:03:17 2012 +0100 @@ -1,6 +1,8 @@ Pending changes in the mainline =============================== +* Recycling of disk space +* Protection of patients against recycling (also in Orthanc Explorer) Version 0.3.1 (2012/12/05) diff -r 3b3525dee661 -r 58f969933720 OrthancExplorer/explorer.css --- a/OrthancExplorer/explorer.css Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancExplorer/explorer.css Sun Dec 09 15:03:17 2012 +0100 @@ -37,3 +37,7 @@ text-decoration: none; color: white !important; } + +.switch-container .ui-slider-switch { + width: 100%; +} \ No newline at end of file diff -r 3b3525dee661 -r 58f969933720 OrthancExplorer/explorer.html --- a/OrthancExplorer/explorer.html Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancExplorer/explorer.html Sun Dec 09 15:03:17 2012 +0100 @@ -82,7 +82,13 @@

- Go to patient finder +

+ +
+ Delete this patient Download ZIP

diff -r 3b3525dee661 -r 58f969933720 OrthancExplorer/explorer.js --- a/OrthancExplorer/explorer.js Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancExplorer/explorer.js Sun Dec 09 15:03:17 2012 +0100 @@ -378,6 +378,18 @@ } target.listview('refresh'); + + // Check whether this patient is protected + $.ajax({ + url: '../patients/' + $.mobile.pageData.uuid + '/protected', + type: 'GET', + dataType: 'text', + async: false, + success: function (s) { + var v = (s == '1') ? 'on' : 'off'; + $('#protection').val(v).slider('refresh'); + } + }); }); }); } @@ -786,3 +798,13 @@ window.location.href = '../series/' + $.mobile.pageData.uuid + '/archive'; }); +$('#protection').live('change', function(e) { + var isProtected = e.target.value == "on"; + $.ajax({ + url: '../patients/' + $.mobile.pageData.uuid + '/protected', + type: 'PUT', + dataType: 'text', + data: isProtected ? '1' : '0', + async: false + }); +}); diff -r 3b3525dee661 -r 58f969933720 OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/DatabaseWrapper.cpp Sun Dec 09 15:03:17 2012 +0100 @@ -33,6 +33,7 @@ #include "DatabaseWrapper.h" #include "../Core/DicomFormat/DicomArray.h" +#include "../Core/Uuid.h" #include "EmbeddedResources.h" #include @@ -61,12 +62,18 @@ virtual unsigned int GetCardinality() const { - return 1; + return 5; } virtual void Compute(SQLite::FunctionContext& context) { - listener_.SignalFileDeleted(context.GetStringValue(0)); + FileInfo info(context.GetStringValue(0), + static_cast(context.GetIntValue(1)), + static_cast(context.GetInt64Value(2)), + static_cast(context.GetIntValue(3)), + static_cast(context.GetInt64Value(4))); + + listener_.SignalFileDeleted(info); } }; @@ -743,9 +750,9 @@ LOG(INFO) << "Version of the Orthanc database: " << version; unsigned int v = boost::lexical_cast(version); - // This version of Orthanc is only compatible with version 2 of - // the DB schema (since Orthanc 0.3.1) - ok = (v == 2); + // This version of Orthanc is only compatible with version 3 of + // the DB schema (since Orthanc 0.3.2) + ok = (v == 3); } catch (boost::bad_lexical_cast&) { @@ -777,4 +784,70 @@ return c; } + + bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1"); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else + { + internalId = s.ColumnInt(0); + return true; + } + } + + bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder " + "WHERE patientId != ? ORDER BY seq ASC LIMIT 1"); + s.BindInt(0, patientIdToAvoid); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else + { + internalId = s.ColumnInt(0); + return true; + } + } + + bool DatabaseWrapper::IsProtectedPatient(int64_t internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); + s.BindInt(0, internalId); + return !s.Step(); + } + + void DatabaseWrapper::SetProtectedPatient(int64_t internalId, + bool isProtected) + { + if (isProtected) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); + s.BindInt(0, internalId); + s.Run(); + } + else if (IsProtectedPatient(internalId)) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); + s.BindInt(0, internalId); + s.Run(); + } + else + { + // Nothing to do: The patient is already unprotected + } + } } diff -r 3b3525dee661 -r 58f969933720 OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/DatabaseWrapper.h Sun Dec 09 15:03:17 2012 +0100 @@ -179,6 +179,16 @@ void GetAllPublicIds(Json::Value& target, ResourceType resourceType); + bool SelectPatientToRecycle(int64_t& internalId); + + bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid); + + bool IsProtectedPatient(int64_t internalId); + + void SetProtectedPatient(int64_t internalId, + bool isProtected); + DatabaseWrapper(const std::string& path, IServerIndexListener& listener); diff -r 3b3525dee661 -r 58f969933720 OrthancServer/IServerIndexListener.h --- a/OrthancServer/IServerIndexListener.h Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/IServerIndexListener.h Sun Dec 09 15:03:17 2012 +0100 @@ -47,7 +47,6 @@ virtual void SignalRemainingAncestor(ResourceType parentType, const std::string& publicId) = 0; - virtual void SignalFileDeleted(const std::string& fileUuid) = 0; - + virtual void SignalFileDeleted(const FileInfo& info) = 0; }; } diff -r 3b3525dee661 -r 58f969933720 OrthancServer/OrthancInitialization.h --- a/OrthancServer/OrthancInitialization.h Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/OrthancInitialization.h Sun Dec 09 15:03:17 2012 +0100 @@ -35,6 +35,7 @@ #include #include #include +#include #include "../Core/HttpServer/MongooseServer.h" namespace Orthanc diff -r 3b3525dee661 -r 58f969933720 OrthancServer/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi.cpp Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/OrthancRestApi.cpp Sun Dec 09 15:03:17 2012 +0100 @@ -607,6 +607,40 @@ } + // Get information about a single patient ----------------------------------- + + static void IsProtectedPatient(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + std::string publicId = call.GetUriComponent("id", ""); + bool isProtected = context.GetIndex().IsProtectedPatient(publicId); + call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", "text/plain"); + } + + + static void SetPatientProtection(RestApi::PutCall& call) + { + RETRIEVE_CONTEXT(call); + std::string publicId = call.GetUriComponent("id", ""); + std::string s = Toolbox::StripSpaces(call.GetPutBody()); + + if (s == "0") + { + context.GetIndex().SetProtectedPatient(publicId, false); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + else if (s == "1") + { + context.GetIndex().SetProtectedPatient(publicId, true); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + else + { + // Bad request + } + } + + // Get information about a single instance ---------------------------------- static void GetInstanceFile(RestApi::GetCall& call) @@ -845,6 +879,8 @@ Register("/studies/{id}/archive", GetArchive); Register("/series/{id}/archive", GetArchive); + Register("/patients/{id}/protected", IsProtectedPatient); + Register("/patients/{id}/protected", SetPatientProtection); Register("/instances/{id}/file", GetInstanceFile); Register("/instances/{id}/tags", GetInstanceTags); Register("/instances/{id}/simplified-tags", GetInstanceTags); diff -r 3b3525dee661 -r 58f969933720 OrthancServer/PrepareDatabase.sql --- a/OrthancServer/PrepareDatabase.sql Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/PrepareDatabase.sql Sun Dec 09 15:03:17 2012 +0100 @@ -55,9 +55,15 @@ date TEXT ); +CREATE TABLE PatientRecyclingOrder( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE + ); + CREATE INDEX ChildrenIndex ON Resources(parentId); CREATE INDEX PublicIndex ON Resources(publicId); CREATE INDEX ResourceTypeIndex ON Resources(resourceType); +CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); @@ -68,7 +74,8 @@ CREATE TRIGGER AttachedFileDeleted AFTER DELETE ON AttachedFiles BEGIN - SELECT SignalFileDeleted(old.uuid); + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize); END; CREATE TRIGGER ResourceDeleted @@ -86,6 +93,14 @@ DELETE FROM Resources WHERE internalId = old.parentId; END; +CREATE TRIGGER PatientAdded +AFTER INSERT ON Resources +FOR EACH ROW WHEN new.resourceType = 1 -- "1" corresponds to "ResourceType_Patient" in C++ +BEGIN + INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId); +END; + + -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration -INSERT INTO GlobalProperties VALUES (1, "2"); +INSERT INTO GlobalProperties VALUES (1, "3"); diff -r 3b3525dee661 -r 58f969933720 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/ServerContext.cpp Sun Dec 09 15:03:17 2012 +0100 @@ -53,6 +53,9 @@ index_(*this, path.string()), accessor_(storage_) { + // TODO RECYCLING SETUP HERE + //index_.SetMaximumPatientCount(4); + //index_.SetMaximumStorageSize(10); } void ServerContext::SetCompressionEnabled(bool enabled) diff -r 3b3525dee661 -r 58f969933720 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/ServerIndex.cpp Sun Dec 09 15:03:17 2012 +0100 @@ -59,6 +59,7 @@ bool hasRemainingLevel_; ResourceType remainingType_; std::string remainingPublicId_; + std::list pendingFilesToRemove_; public: ServerIndexListener(ServerContext& context) : @@ -73,6 +74,17 @@ void Reset() { hasRemainingLevel_ = false; + pendingFilesToRemove_.clear(); + } + + void CommitFilesToRemove() + { + for (std::list::iterator + it = pendingFilesToRemove_.begin(); + it != pendingFilesToRemove_.end(); it++) + { + context_.RemoveFile(*it); + } } virtual void SignalRemainingAncestor(ResourceType parentType, @@ -96,10 +108,10 @@ } } - virtual void SignalFileDeleted(const std::string& fileUuid) + virtual void SignalFileDeleted(const FileInfo& info) { - assert(Toolbox::IsUuid(fileUuid)); - context_.RemoveFile(fileUuid); + assert(Toolbox::IsUuid(info.GetUuid())); + pendingFilesToRemove_.push_back(info.GetUuid()); } bool HasRemainingLevel() const @@ -127,7 +139,6 @@ ResourceType expectedType) { boost::mutex::scoped_lock lock(mutex_); - listener_->Reset(); std::auto_ptr t(db_->StartTransaction()); @@ -160,6 +171,10 @@ t->Commit(); + // We can remove the files once the SQLite transaction has been + // successfully committed + listener_->CommitFilesToRemove(); + return true; } @@ -180,7 +195,9 @@ ServerIndex::ServerIndex(ServerContext& context, - const std::string& dbPath) : mutex_() + const std::string& dbPath) : + maximumStorageSize_(0), + maximumPatients_(0) { listener_.reset(new Internals::ServerIndexListener(context)); @@ -203,6 +220,10 @@ db_.reset(new DatabaseWrapper(p.string() + "/index", *listener_)); } + // Initial recycling if the parameters have changed since the last + // execution of Orthanc + StandaloneRecycling(); + unsigned int sleep; try { @@ -232,6 +253,7 @@ const std::string& remoteAet) { boost::mutex::scoped_lock lock(mutex_); + listener_->Reset(); DicomInstanceHasher hasher(dicomSummary); @@ -251,6 +273,16 @@ return StoreStatus_AlreadyStored; } + // Ensure there is enough room in the storage for the new instance + uint64_t instanceSize = 0; + for (Attachments::const_iterator it = attachments.begin(); + it != attachments.end(); it++) + { + instanceSize += it->GetCompressedSize(); + } + + Recycle(instanceSize, hasher.HashPatient()); + // Create the instance instance = db_->CreateResource(hasher.HashInstance(), ResourceType_Instance); @@ -339,11 +371,17 @@ t->Commit(); + // We can remove the files once the SQLite transaction has been + // successfully committed. Some files might have to be deleted + // because of recycling. + listener_->CommitFilesToRemove(); + return StoreStatus_Success; } catch (OrthancException& e) { - LOG(ERROR) << "EXCEPTION2 [" << e.What() << "]" << " " << db_->GetErrorMessage(); + LOG(ERROR) << "EXCEPTION [" << e.What() << "]" + << " (SQLite status: " << db_->GetErrorMessage() << ")"; } return StoreStatus_Failure; @@ -477,20 +515,20 @@ switch (type) { - case ResourceType_Study: - result["ParentPatient"] = parent; - break; + case ResourceType_Study: + result["ParentPatient"] = parent; + break; - case ResourceType_Series: - result["ParentStudy"] = parent; - break; + case ResourceType_Series: + result["ParentStudy"] = parent; + break; - case ResourceType_Instance: - result["ParentSeries"] = parent; - break; + case ResourceType_Instance: + result["ParentSeries"] = parent; + break; - default: - throw OrthancException(ErrorCode_InternalError); + default: + throw OrthancException(ErrorCode_InternalError); } } @@ -510,72 +548,72 @@ switch (type) { - case ResourceType_Patient: - result["Studies"] = c; - break; + case ResourceType_Patient: + result["Studies"] = c; + break; - case ResourceType_Study: - result["Series"] = c; - break; + case ResourceType_Study: + result["Series"] = c; + break; - case ResourceType_Series: - result["Instances"] = c; - break; + case ResourceType_Series: + result["Instances"] = c; + break; - default: - throw OrthancException(ErrorCode_InternalError); + default: + throw OrthancException(ErrorCode_InternalError); } } // Set the resource type switch (type) { - case ResourceType_Patient: - result["Type"] = "Patient"; - break; + case ResourceType_Patient: + result["Type"] = "Patient"; + break; - case ResourceType_Study: - result["Type"] = "Study"; - break; - - case ResourceType_Series: - { - result["Type"] = "Series"; - result["Status"] = ToString(GetSeriesStatus(id)); + case ResourceType_Study: + result["Type"] = "Study"; + break; - int i; - if (db_->GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) - result["ExpectedNumberOfInstances"] = i; - else - result["ExpectedNumberOfInstances"] = Json::nullValue; - - break; - } + case ResourceType_Series: + { + result["Type"] = "Series"; + result["Status"] = ToString(GetSeriesStatus(id)); - case ResourceType_Instance: - { - result["Type"] = "Instance"; + int i; + if (db_->GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) + result["ExpectedNumberOfInstances"] = i; + else + result["ExpectedNumberOfInstances"] = Json::nullValue; - FileInfo attachment; - if (!db_->LookupAttachment(attachment, id, FileContentType_Dicom)) - { - throw OrthancException(ErrorCode_InternalError); + break; } - result["FileSize"] = static_cast(attachment.GetUncompressedSize()); - result["FileUuid"] = attachment.GetUuid(); + case ResourceType_Instance: + { + result["Type"] = "Instance"; + + FileInfo attachment; + if (!db_->LookupAttachment(attachment, id, FileContentType_Dicom)) + { + throw OrthancException(ErrorCode_InternalError); + } - int i; - if (db_->GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) - result["IndexInSeries"] = i; - else - result["IndexInSeries"] = Json::nullValue; + result["FileSize"] = static_cast(attachment.GetUncompressedSize()); + result["FileUuid"] = attachment.GetUuid(); - break; - } + int i; + if (db_->GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) + result["IndexInSeries"] = i; + else + result["IndexInSeries"] = Json::nullValue; - default: - throw OrthancException(ErrorCode_InternalError); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); } // Record the remaining information @@ -666,28 +704,28 @@ switch (currentType) { - case ResourceType_Patient: - patientId = map.GetValue(DICOM_TAG_PATIENT_ID).AsString(); - done = true; - break; + 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_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_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; + case ResourceType_Instance: + sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString(); + currentType = ResourceType_Series; + break; - default: - throw OrthancException(ErrorCode_InternalError); + default: + throw OrthancException(ErrorCode_InternalError); } // If we have not reached the Patient level, find the parent of @@ -724,4 +762,161 @@ db_->GetLastExportedResource(target); return true; } + + + bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize) + { + if (maximumStorageSize_ != 0) + { + uint64_t currentSize = db_->GetTotalCompressedSize(); + if (currentSize + instanceSize > maximumStorageSize_) + { + return true; + } + } + + if (maximumPatients_ != 0) + { + uint64_t patientCount = db_->GetResourceCount(ResourceType_Patient); + if (patientCount > maximumPatients_) + { + return true; + } + } + + return false; + } + + + void ServerIndex::Recycle(uint64_t instanceSize, + const std::string& newPatientId) + { + if (!IsRecyclingNeeded(instanceSize)) + { + return; + } + + // Check whether other DICOM instances from this patient are + // already stored + int64_t patientToAvoid; + ResourceType type; + bool hasPatientToAvoid = db_->LookupResource(newPatientId, patientToAvoid, type); + + if (hasPatientToAvoid && type != ResourceType_Patient) + { + throw OrthancException(ErrorCode_InternalError); + } + + // Iteratively select patient to remove until there is enough + // space in the DICOM store + int64_t patientToRecycle; + while (true) + { + // If other instances of this patient are already in the store, + // we must avoid to recycle them + bool ok = hasPatientToAvoid ? + db_->SelectPatientToRecycle(patientToRecycle, patientToAvoid) : + db_->SelectPatientToRecycle(patientToRecycle); + + if (!ok) + { + throw OrthancException(ErrorCode_FullStorage); + } + + LOG(INFO) << "Recycling one patient"; + db_->DeleteResource(patientToRecycle); + + if (!IsRecyclingNeeded(instanceSize)) + { + // OK, we're done + break; + } + } + } + + void ServerIndex::SetMaximumPatientCount(unsigned int count) + { + boost::mutex::scoped_lock lock(mutex_); + maximumPatients_ = count; + + if (count == 0) + { + LOG(WARNING) << "No limit on the number of stored patients"; + } + else + { + LOG(WARNING) << "At most " << count << " patients will be stored"; + } + + StandaloneRecycling(); + } + + void ServerIndex::SetMaximumStorageSize(uint64_t size) + { + boost::mutex::scoped_lock lock(mutex_); + maximumStorageSize_ = size; + + if (size == 0) + { + LOG(WARNING) << "No limit on the size of the storage area"; + } + else + { + LOG(WARNING) << "At most " << (size / (1024 * 1024)) << "MB will be used for the storage area"; + } + + StandaloneRecycling(); + } + + void ServerIndex::StandaloneRecycling() + { + // WARNING: No mutex here, do not include this as a public method + std::auto_ptr t(db_->StartTransaction()); + t->Begin(); + Recycle(0, ""); + t->Commit(); + listener_->CommitFilesToRemove(); + } + + + bool ServerIndex::IsProtectedPatient(const std::string& publicId) + { + boost::mutex::scoped_lock lock(mutex_); + + // Lookup for the requested resource + int64_t id; + ResourceType type; + if (!db_->LookupResource(publicId, id, type) || + type != ResourceType_Patient) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + return db_->IsProtectedPatient(id); + } + + + void ServerIndex::SetProtectedPatient(const std::string& publicId, + bool isProtected) + { + boost::mutex::scoped_lock lock(mutex_); + + // Lookup for the requested resource + int64_t id; + ResourceType type; + if (!db_->LookupResource(publicId, id, type) || + type != ResourceType_Patient) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // No need for a SQLite::Transaction here, as we only make 1 write to the DB + db_->SetProtectedPatient(id, isProtected); + + if (isProtected) + LOG(INFO) << "Patient " << publicId << " has been protected"; + else + LOG(INFO) << "Patient " << publicId << " has been unprotected"; + } + } diff -r 3b3525dee661 -r 58f969933720 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/ServerIndex.h Sun Dec 09 15:03:17 2012 +0100 @@ -62,11 +62,21 @@ std::auto_ptr listener_; std::auto_ptr db_; + uint64_t maximumStorageSize_; + unsigned int maximumPatients_; + void MainDicomTagsToJson(Json::Value& result, int64_t resourceId); SeriesStatus GetSeriesStatus(int id); + bool IsRecyclingNeeded(uint64_t instanceSize); + + void Recycle(uint64_t instanceSize, + const std::string& newPatientId); + + void StandaloneRecycling(); + public: typedef std::list Attachments; @@ -75,6 +85,22 @@ ~ServerIndex(); + uint64_t GetMaximumStorageSize() const + { + return maximumStorageSize_; + } + + uint64_t GetMaximumPatientCount() const + { + return maximumPatients_; + } + + // "size == 0" means no limit on the storage size + void SetMaximumStorageSize(uint64_t size); + + // "count == 0" means no limit on the number of patients + void SetMaximumPatientCount(unsigned int count); + StoreStatus Store(const DicomMap& dicomSummary, const Attachments& attachments, const std::string& remoteAet); @@ -111,5 +137,9 @@ bool GetLastExportedResource(Json::Value& target); + bool IsProtectedPatient(const std::string& publicId); + + void SetProtectedPatient(const std::string& publicId, + bool isProtected); }; } diff -r 3b3525dee661 -r 58f969933720 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Sun Dec 09 15:01:00 2012 +0100 +++ b/OrthancServer/main.cpp Sun Dec 09 15:03:17 2012 +0100 @@ -214,6 +214,25 @@ ServerContext context(storageDirectory); context.SetCompressionEnabled(GetGlobalBoolParameter("StorageCompression", false)); + try + { + context.GetIndex().SetMaximumPatientCount(GetGlobalIntegerParameter("MaximumPatientCount", 0)); + } + catch (...) + { + context.GetIndex().SetMaximumPatientCount(0); + } + + try + { + uint64_t size = GetGlobalIntegerParameter("MaximumStorageSize", 0); + context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024); + } + catch (...) + { + context.GetIndex().SetMaximumStorageSize(0); + } + MyDicomStoreFactory storeScp(context); { diff -r 3b3525dee661 -r 58f969933720 Resources/Configuration.json --- a/Resources/Configuration.json Sun Dec 09 15:01:00 2012 +0100 +++ b/Resources/Configuration.json Sun Dec 09 15:03:17 2012 +0100 @@ -13,6 +13,14 @@ // Enable the transparent compression of the DICOM instances "StorageCompression" : false, + // Maximum size of the storage in MB (a value of "0" indicates no + // limit on the storage size) + "MaximumStorageSize" : 0, + + // Maximum number of patients that can be stored at a given time + // in the storage (a value of "0" indicates no limit on the number + // of patients) + "MaximumPatientCount" : 0, /** diff -r 3b3525dee661 -r 58f969933720 Resources/Samples/RestApi/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/RestApi/CMakeLists.txt Sun Dec 09 15:03:17 2012 +0100 @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 2.8) + +project(RestApiSample) + +include(ExternalProject) + +ExternalProject_Add( + ORTHANC_CORE + PREFIX ${CMAKE_BINARY_DIR}/Orthanc/ + DOWNLOAD_COMMAND hg clone https://code.google.com/p/orthanc/ -r Orthanc-0.3.1 + UPDATE_COMMAND "" + SOURCE_DIR ${CMAKE_BINARY_DIR}/Orthanc/src/orthanc/ + + # Optional step, to reuse the third-party downloads + PATCH_COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/../../../ThirdPartyDownloads ThirdPartyDownloads + + CMAKE_COMMAND ${CMAKE_COMMAND} + CMAKE_ARGS -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DUSE_DYNAMIC_GOOGLE_LOG=OFF -DUSE_DYNAMIC_SQLITE=OFF -DONLY_CORE_LIBRARY=ON -DENABLE_SSL=OFF + BUILD_COMMAND $(MAKE) + INSTALL_COMMAND "" + BUILD_IN_SOURCE 0 +) + +ExternalProject_Get_Property(ORTHANC_CORE source_dir) +include_directories(${source_dir}) + +ExternalProject_Get_Property(ORTHANC_CORE binary_dir) +link_directories(${binary_dir}) +include_directories(${binary_dir}/jsoncpp-src-0.5.0/include) +include_directories(${binary_dir}/glog-0.3.2/src) + +add_executable(RestApiSample + Sample.cpp + ) + +add_dependencies(RestApiSample ORTHANC_CORE) + +target_link_libraries(RestApiSample + # From Orthanc + CoreLibrary + GoogleLog + #OpenSSL + + # System-wide libraries + pthread + ) + diff -r 3b3525dee661 -r 58f969933720 Resources/Samples/RestApi/Sample.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/RestApi/Sample.cpp Sun Dec 09 15:03:17 2012 +0100 @@ -0,0 +1,105 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 Medical Physics Department, CHU of Liege, + * 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 . + **/ + + +#include +#include +#include +#include +#include + + +/** + * This is a demo program that shows how to setup a REST server with + * the Orthanc Core API. Once the server is running, here are some + * sample command lines to interact with it: + * + * # curl http://localhost:8042 + * # curl 'http://localhost:8042?name=Hide' + * # curl http://localhost:8042 -X DELETE + * # curl http://localhost:8042 -X PUT -d "PutBody" + * # curl http://localhost:8042 -X POST -d "PostBody" + **/ + +static void GetRoot(Orthanc::RestApi::GetCall& call) +{ + std::string answer = "Hello world\n"; + answer += "Glad to meet you, Mr. " + call.GetArgument("name", "Nobody") + "\n"; + call.GetOutput().AnswerBuffer(answer, "text/plain"); +} + +static void DeleteRoot(Orthanc::RestApi::DeleteCall& call) +{ + call.GetOutput().AnswerBuffer("Hey, you have just deleted the server!\n", + "text/plain"); +} + +static void PostRoot(Orthanc::RestApi::PostCall& call) +{ + call.GetOutput().AnswerBuffer("I have received a POST with body: [" + + call.GetPostBody() + "]\n", "text/plain"); +} + +static void PutRoot(Orthanc::RestApi::PutCall& call) +{ + call.GetOutput().AnswerBuffer("I have received a PUT with body: [" + + call.GetPutBody() + "]\n", "text/plain"); +} + +int main() +{ + // Initialize the logging mechanism + google::InitGoogleLogging("Orthanc"); + FLAGS_logtostderr = true; + FLAGS_minloglevel = 0; // Use the verbose mode + FLAGS_v = 0; + + // Define the callbacks of the REST API + std::auto_ptr rest(new Orthanc::RestApi); + rest->Register("/", GetRoot); + rest->Register("/", PostRoot); + rest->Register("/", PutRoot); + rest->Register("/", DeleteRoot); + + // Setup the embedded HTTP server + Orthanc::MongooseServer httpServer; + httpServer.SetPortNumber(8042); // Use TCP port 8042 + httpServer.SetRemoteAccessAllowed(true); // Do not block remote requests + httpServer.RegisterHandler(rest.release()); // The REST API is the handler + + // Start the server and wait for the user to hit "Ctrl-C" + httpServer.Start(); + LOG(WARNING) << "REST server has started"; + Orthanc::Toolbox::ServerBarrier(); + LOG(WARNING) << "REST server has stopped"; + + return 0; +} diff -r 3b3525dee661 -r 58f969933720 UnitTests/ServerIndex.cpp --- a/UnitTests/ServerIndex.cpp Sun Dec 09 15:01:00 2012 +0100 +++ b/UnitTests/ServerIndex.cpp Sun Dec 09 15:03:17 2012 +0100 @@ -1,6 +1,7 @@ #include "gtest/gtest.h" #include "../OrthancServer/DatabaseWrapper.h" +#include "../Core/Uuid.h" #include #include @@ -12,7 +13,7 @@ class ServerIndexListener : public IServerIndexListener { public: - std::set deletedFiles_; + std::vector deletedFiles_; std::string ancestorId_; ResourceType ancestorType_; @@ -29,9 +30,10 @@ ancestorType_ = type; } - virtual void SignalFileDeleted(const std::string& fileUuid) + virtual void SignalFileDeleted(const FileInfo& info) { - deletedFiles_.insert(fileUuid); + const std::string fileUuid = info.GetUuid(); + deletedFiles_.push_back(fileUuid); LOG(INFO) << "A file must be removed: " << fileUuid; } }; @@ -170,8 +172,12 @@ index.DeleteResource(a[0]); ASSERT_EQ(2u, listener.deletedFiles_.size()); - ASSERT_FALSE(listener.deletedFiles_.find("my json file") == listener.deletedFiles_.end()); - ASSERT_FALSE(listener.deletedFiles_.find("my dicom file") == listener.deletedFiles_.end()); + ASSERT_FALSE(std::find(listener.deletedFiles_.begin(), + listener.deletedFiles_.end(), + "my json file") == listener.deletedFiles_.end()); + ASSERT_FALSE(std::find(listener.deletedFiles_.begin(), + listener.deletedFiles_.end(), + "my dicom file") == listener.deletedFiles_.end()); ASSERT_EQ(2u, index.GetTableRecordCount("Resources")); ASSERT_EQ(0u, index.GetTableRecordCount("Metadata")); @@ -183,7 +189,9 @@ ASSERT_EQ(2u, index.GetTableRecordCount("GlobalProperties")); ASSERT_EQ(3u, listener.deletedFiles_.size()); - ASSERT_FALSE(listener.deletedFiles_.find("world") == listener.deletedFiles_.end()); + ASSERT_FALSE(std::find(listener.deletedFiles_.begin(), + listener.deletedFiles_.end(), + "world") == listener.deletedFiles_.end()); } @@ -256,3 +264,135 @@ index.DeleteResource(a[6]); ASSERT_EQ("", listener.ancestorId_); // No more ancestor } + + +TEST(DatabaseWrapper, PatientRecycling) +{ + ServerIndexListener listener; + DatabaseWrapper index(listener); + + std::vector patients; + for (int i = 0; i < 10; i++) + { + std::string p = "Patient " + boost::lexical_cast(i); + patients.push_back(index.CreateResource(p, ResourceType_Patient)); + index.AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10)); + ASSERT_FALSE(index.IsProtectedPatient(patients[i])); + } + + ASSERT_EQ(10u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(10u, index.GetTableRecordCount("PatientRecyclingOrder")); + + listener.Reset(); + + index.DeleteResource(patients[5]); + index.DeleteResource(patients[0]); + ASSERT_EQ(8u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(8u, index.GetTableRecordCount("PatientRecyclingOrder")); + + ASSERT_EQ(2u, listener.deletedFiles_.size()); + ASSERT_EQ("Patient 5", listener.deletedFiles_[0]); + ASSERT_EQ("Patient 0", listener.deletedFiles_[1]); + + int64_t p; + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]); + index.DeleteResource(p); + index.DeleteResource(patients[8]); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]); + index.DeleteResource(p); + ASSERT_FALSE(index.SelectPatientToRecycle(p)); + + ASSERT_EQ(10u, listener.deletedFiles_.size()); + ASSERT_EQ(0u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder")); +} + + +TEST(DatabaseWrapper, PatientProtection) +{ + ServerIndexListener listener; + DatabaseWrapper index(listener); + + std::vector patients; + for (int i = 0; i < 5; i++) + { + std::string p = "Patient " + boost::lexical_cast(i); + patients.push_back(index.CreateResource(p, ResourceType_Patient)); + index.AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10)); + ASSERT_FALSE(index.IsProtectedPatient(patients[i])); + } + + ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + + ASSERT_FALSE(index.IsProtectedPatient(patients[2])); + index.SetProtectedPatient(patients[2], true); + ASSERT_TRUE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); + ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); + + index.SetProtectedPatient(patients[2], true); + ASSERT_TRUE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[2], false); + ASSERT_FALSE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[2], false); + ASSERT_FALSE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + + ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[2], true); + ASSERT_TRUE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[2], false); + ASSERT_FALSE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[3], true); + ASSERT_TRUE(index.IsProtectedPatient(patients[3])); + ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); + + ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(0u, listener.deletedFiles_.size()); + + // Unprotecting a patient puts it at the last position in the recycling queue + int64_t p; + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); + index.DeleteResource(p); + ASSERT_FALSE(index.SelectPatientToRecycle(p, patients[2])); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); + index.DeleteResource(p); + // "patients[3]" is still protected + ASSERT_FALSE(index.SelectPatientToRecycle(p)); + + ASSERT_EQ(4u, listener.deletedFiles_.size()); + ASSERT_EQ(1u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder")); + + index.SetProtectedPatient(patients[3], false); + ASSERT_EQ(1u, index.GetTableRecordCount("PatientRecyclingOrder")); + ASSERT_FALSE(index.SelectPatientToRecycle(p, patients[3])); + ASSERT_TRUE(index.SelectPatientToRecycle(p, patients[2])); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); + index.DeleteResource(p); + + ASSERT_EQ(5u, listener.deletedFiles_.size()); + ASSERT_EQ(0u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder")); +}