Mercurial > hg > orthanc
changeset 4627:f7d5372b59b3 db-changes
handling revisions of attachments
line wrap: on
line diff
--- a/NEWS Mon Apr 19 10:28:43 2021 +0200 +++ b/NEWS Tue Apr 20 15:11:59 2021 +0200 @@ -12,8 +12,8 @@ -------- * API version upgraded to 12 -* "/.../{id}/metadata/{name}" URIs handle the HTTP headers "If-Match", "If-None-Match" and - "ETag" to cope with revisions +* "/.../{id}/metadata/{name}" and "/.../{id}/attachments/{name}/..." URIs handle the + HTTP headers "If-Match", "If-None-Match" and "ETag" to cope with revisions Plugins -------
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -651,8 +651,10 @@ virtual void AddAttachment(int64_t id, - const FileInfo& attachment) ORTHANC_OVERRIDE + const FileInfo& attachment, + int64_t revision) ORTHANC_OVERRIDE { + // "revision" is not used, as it was added in Orthanc 1.9.2 OrthancPluginAttachment tmp; tmp.uuid = attachment.GetUuid().c_str(); tmp.contentType = static_cast<int32_t>(attachment.GetContentType()); @@ -1104,6 +1106,7 @@ virtual bool LookupAttachment(FileInfo& attachment, + int64_t& revision, int64_t id, FileContentType contentType) ORTHANC_OVERRIDE { @@ -1111,6 +1114,8 @@ CheckSuccess(that_.backend_.lookupAttachment (that_.GetContext(), that_.payload_, id, static_cast<int32_t>(contentType))); + + revision = 0; // Dummy value, as revisions were added in Orthanc 1.9.2 if (type_ == _OrthancPluginDatabaseAnswerType_None) {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -305,7 +305,8 @@ virtual void AddAttachment(int64_t id, - const FileInfo& attachment) ORTHANC_OVERRIDE + const FileInfo& attachment, + int64_t revision) ORTHANC_OVERRIDE { OrthancPluginAttachment tmp; tmp.uuid = attachment.GetUuid().c_str(); @@ -316,7 +317,7 @@ tmp.compressedSize = attachment.GetCompressedSize(); tmp.compressedHash = attachment.GetCompressedMD5().c_str(); - CheckSuccess(that_.backend_.addAttachment(transaction_, id, &tmp)); + CheckSuccess(that_.backend_.addAttachment(transaction_, id, &tmp, revision)); CheckNoEvent(); } @@ -666,10 +667,11 @@ virtual bool LookupAttachment(FileInfo& attachment, + int64_t& revision, int64_t id, FileContentType contentType) ORTHANC_OVERRIDE { - CheckSuccess(that_.backend_.lookupAttachment(transaction_, id, static_cast<int32_t>(contentType))); + CheckSuccess(that_.backend_.lookupAttachment(transaction_, &revision, id, static_cast<int32_t>(contentType))); CheckNoEvent(); uint32_t count; @@ -1127,8 +1129,8 @@ CHECK_FUNCTION_EXISTS(backend_, getLastExportedResource); CHECK_FUNCTION_EXISTS(backend_, getMainDicomTags); CHECK_FUNCTION_EXISTS(backend_, getPublicId); + CHECK_FUNCTION_EXISTS(backend_, getResourceType); CHECK_FUNCTION_EXISTS(backend_, getResourcesCount); - CHECK_FUNCTION_EXISTS(backend_, getResourceType); CHECK_FUNCTION_EXISTS(backend_, getTotalCompressedSize); CHECK_FUNCTION_EXISTS(backend_, getTotalUncompressedSize); CHECK_FUNCTION_EXISTS(backend_, isDiskSizeAbove); @@ -1142,8 +1144,8 @@ CHECK_FUNCTION_EXISTS(backend_, lookupMetadata); CHECK_FUNCTION_EXISTS(backend_, lookupParent); CHECK_FUNCTION_EXISTS(backend_, lookupResource); + CHECK_FUNCTION_EXISTS(backend_, lookupResourceAndParent); CHECK_FUNCTION_EXISTS(backend_, lookupResources); - CHECK_FUNCTION_EXISTS(backend_, lookupResourceAndParent); CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle); CHECK_FUNCTION_EXISTS(backend_, selectPatientToRecycle2); CHECK_FUNCTION_EXISTS(backend_, setGlobalProperty);
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Tue Apr 20 15:11:59 2021 +0200 @@ -1109,10 +1109,12 @@ OrthancPluginErrorCode (*commit) (OrthancPluginDatabaseTransaction* transaction, int64_t fileSizeDelta); - + + /* A call to "addAttachment()" guarantees that this attachment is not already existing ("INSERT") */ OrthancPluginErrorCode (*addAttachment) (OrthancPluginDatabaseTransaction* transaction, int64_t id, - const OrthancPluginAttachment* attachment); + const OrthancPluginAttachment* attachment, + int64_t revision); OrthancPluginErrorCode (*clearChanges) (OrthancPluginDatabaseTransaction* transaction); @@ -1243,6 +1245,7 @@ /* Answer is read using "readAnswerAttachment()" */ OrthancPluginErrorCode (*lookupAttachment) (OrthancPluginDatabaseTransaction* transaction, + int64_t* revision /* out */, int64_t resourceId, int32_t contentType); @@ -1297,6 +1300,7 @@ int32_t property, const char* value); + /* In "setMetadata()", the metadata might already be existing ("INSERT OR REPLACE") */ OrthancPluginErrorCode (*setMetadata) (OrthancPluginDatabaseTransaction* transaction, int64_t id, int32_t metadata,
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Apr 20 15:11:59 2021 +0200 @@ -78,8 +78,12 @@ // attachments (cf. "fastGetTotalSize_") virtual void Commit(int64_t fileSizeDelta) = 0; + // A call to "AddAttachment()" guarantees that this attachment + // is not already existing. This is different from + // "SetMetadata()" that might have to replace an older value. virtual void AddAttachment(int64_t id, - const FileInfo& attachment) = 0; + const FileInfo& attachment, + int64_t revision) = 0; virtual void ClearChanges() = 0; @@ -150,6 +154,7 @@ virtual void LogExportedResource(const ExportedResource& resource) = 0; virtual bool LookupAttachment(FileInfo& attachment, + int64_t& revision, int64_t id, FileContentType contentType) = 0;
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -326,8 +326,10 @@ virtual void AddAttachment(int64_t id, - const FileInfo& attachment) ORTHANC_OVERRIDE + const FileInfo& attachment, + int64_t revision) ORTHANC_OVERRIDE { + // TODO - REVISIONS SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); s.BindInt64(0, id); s.BindInt(1, attachment.GetContentType()); @@ -799,6 +801,7 @@ virtual bool LookupAttachment(FileInfo& attachment, + int64_t& revision, int64_t id, FileContentType contentType) ORTHANC_OVERRIDE { @@ -821,6 +824,7 @@ static_cast<CompressionType>(s.ColumnInt(2)), s.ColumnInt64(3), s.ColumnString(5)); + revision = 0; // TODO - REVISIONS return true; } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -920,7 +920,8 @@ target["Type"] = "Instance"; FileInfo attachment; - if (!transaction.LookupAttachment(attachment, internalId, FileContentType_Dicom)) + int64_t revision; // ignored + if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom)) { throw OrthancException(ErrorCode_InternalError); } @@ -1015,10 +1016,11 @@ bool StatelessDatabaseOperations::LookupAttachment(FileInfo& attachment, + int64_t& revision, const std::string& instancePublicId, FileContentType contentType) { - class Operations : public ReadOnlyOperationsT4<bool&, FileInfo&, const std::string&, FileContentType> + class Operations : public ReadOnlyOperationsT5<bool&, FileInfo&, int64_t&, const std::string&, FileContentType> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, @@ -1026,13 +1028,13 @@ { int64_t internalId; ResourceType type; - if (!transaction.LookupResource(internalId, type, tuple.get<2>())) + if (!transaction.LookupResource(internalId, type, tuple.get<3>())) { throw OrthancException(ErrorCode_UnknownResource); } - else if (transaction.LookupAttachment(tuple.get<1>(), internalId, tuple.get<3>())) + else if (transaction.LookupAttachment(tuple.get<1>(), tuple.get<2>(), internalId, tuple.get<4>())) { - assert(tuple.get<1>().GetContentType() == tuple.get<3>()); + assert(tuple.get<1>().GetContentType() == tuple.get<4>()); tuple.get<0>() = true; } else @@ -1044,7 +1046,7 @@ bool found; Operations operations; - operations.Apply(*this, found, attachment, instancePublicId, contentType); + operations.Apply(*this, found, attachment, revision, instancePublicId, contentType); return found; } @@ -1547,7 +1549,8 @@ it = f.begin(); it != f.end(); ++it) { FileInfo attachment; - if (transaction.LookupAttachment(attachment, resource, *it)) + int64_t revision; // ignored + if (transaction.LookupAttachment(attachment, revision, resource, *it)) { if (attachment.GetContentType() == FileContentType_Dicom) { @@ -2244,7 +2247,7 @@ } else { - newRevision_ = oldRevision_ + 1; + newRevision_ = expectedRevision + 1; } } else @@ -2478,23 +2481,38 @@ } - void StatelessDatabaseOperations::DeleteAttachment(const std::string& publicId, - FileContentType type) + bool StatelessDatabaseOperations::DeleteAttachment(const std::string& publicId, + FileContentType type, + bool hasRevision, + int64_t revision) { class Operations : public IReadWriteOperations { private: const std::string& publicId_; FileContentType type_; - + bool hasRevision_; + int64_t revision_; + bool found_; + public: Operations(const std::string& publicId, - FileContentType type) : + FileContentType type, + bool hasRevision, + int64_t revision) : publicId_(publicId), - type_(type) + type_(type), + hasRevision_(hasRevision), + revision_(revision), + found_(false) { } + bool HasFound() const + { + return found_; + } + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE { ResourceType resourceType; @@ -2505,18 +2523,35 @@ } else { - transaction.DeleteAttachment(id, type_); + FileInfo info; + int64_t expectedRevision; + if (transaction.LookupAttachment(info, expectedRevision, id, type_)) + { + if (hasRevision_ && + expectedRevision != revision_) + { + throw OrthancException(ErrorCode_Revision); + } + + found_ = true; + transaction.DeleteAttachment(id, type_); - if (IsUserContentType(type_)) + if (IsUserContentType(type_)) + { + transaction.LogChange(id, ChangeType_UpdatedAttachment, resourceType, publicId_); + } + } + else { - transaction.LogChange(id, ChangeType_UpdatedAttachment, resourceType, publicId_); + found_ = false; } } } }; - Operations operations(publicId, type); + Operations operations(publicId, type, hasRevision, revision); Apply(operations); + return operations.HasFound(); } @@ -3018,7 +3053,7 @@ for (Attachments::const_iterator it = attachments_.begin(); it != attachments_.end(); ++it) { - transaction.AddAttachment(instanceId, *it); + transaction.AddAttachment(instanceId, *it, 0 /* this is the first revision */); } @@ -3219,30 +3254,42 @@ } - StoreStatus StatelessDatabaseOperations::AddAttachment(const FileInfo& attachment, + StoreStatus StatelessDatabaseOperations::AddAttachment(int64_t& newRevision, + const FileInfo& attachment, const std::string& publicId, uint64_t maximumStorageSize, - unsigned int maximumPatients) + unsigned int maximumPatients, + bool hasOldRevision, + int64_t oldRevision) { class Operations : public IReadWriteOperations { private: + int64_t& newRevision_; StoreStatus status_; const FileInfo& attachment_; const std::string& publicId_; uint64_t maximumStorageSize_; unsigned int maximumPatientCount_; - + bool hasOldRevision_; + int64_t oldRevision_; + public: - Operations(const FileInfo& attachment, + Operations(int64_t& newRevision, + const FileInfo& attachment, const std::string& publicId, uint64_t maximumStorageSize, - unsigned int maximumPatientCount) : + unsigned int maximumPatientCount, + bool hasOldRevision, + int64_t oldRevision) : + newRevision_(newRevision), status_(StoreStatus_Failure), attachment_(attachment), publicId_(publicId), maximumStorageSize_(maximumStorageSize), - maximumPatientCount_(maximumPatientCount) + maximumPatientCount_(maximumPatientCount), + hasOldRevision_(hasOldRevision), + oldRevision_(oldRevision) { } @@ -3261,8 +3308,30 @@ } else { - // Remove possible previous attachment - transaction.DeleteAttachment(resourceId, attachment_.GetContentType()); + // Possibly remove previous attachment + { + FileInfo oldFile; + int64_t expectedRevision; + if (transaction.LookupAttachment(oldFile, expectedRevision, resourceId, attachment_.GetContentType())) + { + if (hasOldRevision_ && + expectedRevision != oldRevision_) + { + throw OrthancException(ErrorCode_Revision); + } + else + { + newRevision_ = expectedRevision + 1; + transaction.DeleteAttachment(resourceId, attachment_.GetContentType()); + } + } + else + { + // The attachment is not existing yet: Ignore "oldRevision" + // and initialize a new sequence of revisions + newRevision_ = 0; + } + } // Locate the patient of the target resource int64_t patientId = resourceId; @@ -3286,7 +3355,7 @@ transaction.Recycle(maximumStorageSize_, maximumPatientCount_, attachment_.GetCompressedSize(), transaction.GetPublicId(patientId)); - transaction.AddAttachment(resourceId, attachment_); + transaction.AddAttachment(resourceId, attachment_, newRevision_); if (IsUserContentType(attachment_.GetContentType())) { @@ -3301,7 +3370,7 @@ }; - Operations operations(attachment, publicId, maximumStorageSize, maximumPatients); + Operations operations(newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision); Apply(operations); return operations.GetStatus(); }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Apr 20 15:11:59 2021 +0200 @@ -240,10 +240,11 @@ } bool LookupAttachment(FileInfo& attachment, + int64_t& revision, int64_t id, FileContentType contentType) { - return transaction_.LookupAttachment(attachment, id, contentType); + return transaction_.LookupAttachment(attachment, revision, id, contentType); } bool LookupGlobalProperty(std::string& target, @@ -294,9 +295,10 @@ } void AddAttachment(int64_t id, - const FileInfo& attachment) + const FileInfo& attachment, + int64_t revision) { - transaction_.AddAttachment(id, attachment); + transaction_.AddAttachment(id, attachment, revision); } void ClearChanges() @@ -481,6 +483,7 @@ /* out */ uint64_t& countInstances); bool LookupAttachment(FileInfo& attachment, + int64_t& revision, const std::string& instancePublicId, FileContentType contentType); @@ -600,8 +603,10 @@ bool shared, const std::string& value); - void DeleteAttachment(const std::string& publicId, - FileContentType type); + bool DeleteAttachment(const std::string& publicId, + FileContentType type, + bool hasRevision, + int64_t revision); void LogChange(int64_t internalId, ChangeType changeType, @@ -623,9 +628,12 @@ uint64_t maximumStorageSize, unsigned int maximumPatients); - StoreStatus AddAttachment(const FileInfo& attachment, + StoreStatus AddAttachment(int64_t& newRevision /*out*/, + const FileInfo& attachment, const std::string& publicId, uint64_t maximumStorageSize, - unsigned int maximumPatients); + unsigned int maximumPatients, + bool hasOldRevision, + int64_t oldRevision); }; }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -1541,7 +1541,7 @@ .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetUriArgument("name", "The name of the metadata, or its index (cf. `UserMetadata` configuration option)") .SetHttpHeader("If-Match", "Revision of the metadata, to check if its content has not changed and can " - "be deleted. This option is mandatory if `CheckRevision` option is `true`."); + "be deleted. This header is mandatory if `CheckRevision` option is `true`."); return; } @@ -1686,15 +1686,48 @@ } - static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call) + static void AddAttachmentDocumentation(RestApiGetCall& call, + const std::string& resourceType) + { + call.GetDocumentation() + .SetUriArgument("id", "Orthanc identifier of the " + resourceType + " of interest") + .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") + .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations") + .SetHttpHeader("If-None-Match", "Optional revision of the attachment, to check if its content has changed"); + } + + + static bool GetAttachmentInfo(FileInfo& info, + RestApiGetCall& call) { CheckValidResourceType(call); - std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); + const std::string publicId = call.GetUriComponent("id", ""); + const std::string name = call.GetUriComponent("name", ""); FileContentType contentType = StringToContentType(name); - return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType); + int64_t revision; + if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType)) + { + call.GetOutput().GetLowLevelOutput(). + AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\""); // New in Orthanc 1.9.2 + + int64_t userRevision; + if (GetRevisionHeader(userRevision, call, "If-None-Match") && + revision == userRevision) + { + call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified); + return false; + } + else + { + return true; + } + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } } @@ -1704,12 +1737,11 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag("Other") .SetSummary("List operations on attachments") .SetDescription("Get the list of the operations that are available for attachments associated with the given " + r) - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_Json, "List of the available operations") .SetHttpGetSample("https://demo.orthanc-server.com/instances/d94d9a03-3003b047-a4affc69-322313b2-680530a2/attachments/dicom", true); return; @@ -1765,7 +1797,9 @@ std::string(uncompress ? "" : ". The attachment will not be decompressed if `StorageCompression` is `true`.")) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") - .AddAnswerType(MimeType_Binary, "The attachment"); + .AddAnswerType(MimeType_Binary, "The attachment") + .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations") + .SetHttpHeader("If-None-Match", "Optional revision of the metadata, to check if its content has changed"); return; } @@ -1778,14 +1812,32 @@ if (uncompress) { - context.AnswerAttachment(call.GetOutput(), publicId, type); + FileInfo info; + if (GetAttachmentInfo(info, call)) + { + context.AnswerAttachment(call.GetOutput(), publicId, type); + } } else { // Return the raw data (possibly compressed), as stored on the filesystem std::string content; - context.ReadAttachment(content, publicId, type, false); - call.GetOutput().AnswerBuffer(content, MimeType_Binary); + int64_t revision; + context.ReadAttachment(content, revision, publicId, type, false); + + call.GetOutput().GetLowLevelOutput(). + AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(revision) + "\""); // New in Orthanc 1.9.2 + + int64_t userRevision; + if (GetRevisionHeader(userRevision, call, "If-None-Match") && + revision == userRevision) + { + call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified); + } + else + { + call.GetOutput().AnswerBuffer(content, MimeType_Binary); + } } } @@ -1796,12 +1848,11 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Get size of attachment") .SetDescription("Get the size of one attachment associated with the given " + r) - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "The size of the attachment"); return; } @@ -1820,13 +1871,12 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Get size of attachment on disk") .SetDescription("Get the size of one attachment associated with the given " + r + ", as stored on the disk. " "This is different from `.../size` iff `EnableStorage` is `true`.") - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "The size of the attachment, as stored on the disk"); return; } @@ -1845,12 +1895,11 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Get MD5 of attachment") .SetDescription("Get the MD5 hash of one attachment associated with the given " + r) - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment"); return; } @@ -1870,13 +1919,12 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Get MD5 of attachment on disk") .SetDescription("Get the MD5 hash of one attachment associated with the given " + r + ", as stored on the disk. " "This is different from `.../md5` iff `EnableStorage` is `true`.") - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "The MD5 of the attachment, as stored on the disk"); return; } @@ -1911,9 +1959,11 @@ std::string publicId = call.GetUriComponent("id", ""); std::string name = call.GetUriComponent("name", ""); + FileContentType contentType = StringToContentType(name); FileInfo info; - if (!GetAttachmentInfo(info, call) || + int64_t revision; // Ignored + if (!OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType) || info.GetCompressedMD5() == "" || info.GetUncompressedMD5() == "") { @@ -1925,7 +1975,7 @@ // First check whether the compressed data is correctly stored in the disk std::string data; - context.ReadAttachment(data, publicId, StringToContentType(name), false); + context.ReadAttachment(data, revision, publicId, StringToContentType(name), false); std::string actualMD5; Toolbox::ComputeMD5(actualMD5, data); @@ -1940,7 +1990,7 @@ } else { - context.ReadAttachment(data, publicId, StringToContentType(name), true); + context.ReadAttachment(data, revision, publicId, StringToContentType(name), true); Toolbox::ComputeMD5(actualMD5, data); ok = (actualMD5 == info.GetUncompressedMD5()); } @@ -1972,7 +2022,8 @@ .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddRequestType(MimeType_Binary, "Binary data containing the attachment") - .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success"); + .AddAnswerType(MimeType_Json, "Empty JSON object in the case of a success") + .SetHttpHeader("If-Match", "Revision of the attachment, if this is not the first time this attachment is set."); return; } @@ -1983,9 +2034,31 @@ std::string name = call.GetUriComponent("name", ""); FileContentType contentType = StringToContentType(name); - if (IsUserContentType(contentType) && // It is forbidden to modify internal attachments - context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize())) + if (IsUserContentType(contentType)) // It is forbidden to modify internal attachments { + int64_t oldRevision; + bool hasOldRevision = GetRevisionHeader(oldRevision, call, "if-match"); + + if (!hasOldRevision) + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) + { + // "StatelessDatabaseOperations::AddAttachment()" will ignore + // the actual value of "oldRevision" if the metadata is + // inexistent as expected + hasOldRevision = true; + oldRevision = -1; // dummy value + } + } + + int64_t newRevision; + context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(), + call.GetBodySize(), hasOldRevision, oldRevision); + + call.GetOutput().GetLowLevelOutput(). + AddHeader("ETag", "\"" + boost::lexical_cast<std::string>(newRevision) + "\""); // New in Orthanc 1.9.2 + call.GetOutput().AnswerBuffer("{}", MimeType_Json); } else @@ -2007,7 +2080,9 @@ .SetDescription("Delete an attachment associated with the given DICOM " + r + ". This call will fail if trying to delete a system attachment (i.e. whose index is < 1024).") .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)"); + .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") + .SetHttpHeader("If-Match", "Revision of the attachment, to check if its content has not changed and can " + "be deleted. This header is mandatory if `CheckRevision` option is `true`."); return; } @@ -2042,8 +2117,34 @@ if (allowed) { - OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType); - call.GetOutput().AnswerBuffer("{}", MimeType_Json); + bool found; + int64_t revision; + if (GetRevisionHeader(revision, call, "if-match")) + { + found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, true, revision); + } + else + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) + { + throw OrthancException(ErrorCode_Revision, + "HTTP header \"If-Match\" is missing, as \"CheckRevision\" is \"true\""); + } + else + { + found = OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType, false, -1 /* dummy value */); + } + } + + if (found) + { + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } } else { @@ -2085,12 +2186,11 @@ { ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + AddAttachmentDocumentation(call, r); call.GetDocumentation() .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) .SetSummary("Is attachment compressed?") .SetDescription("Test whether the attachment has been stored as a compressed file on the disk.") - .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") - .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_PlainText, "`0` if the attachment was stored uncompressed, `1` if it was compressed"); return; } @@ -2886,7 +2986,7 @@ for (std::list<std::string>::const_iterator instance = instances.begin(); instance != instances.end(); ++instance) { - index.DeleteAttachment(*instance, FileContentType_DicomAsJson); + index.DeleteAttachment(*instance, FileContentType_DicomAsJson, false /* no revision checks */, -1 /* dummy */); } }
--- a/OrthancServer/Sources/OrthancWebDav.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -153,7 +153,8 @@ if (level_ == ResourceType_Instance) { FileInfo info; - if (context_.GetIndex().LookupAttachment(info, publicId, FileContentType_Dicom)) + int64_t revision; // Ignored + if (context_.GetIndex().LookupAttachment(info, revision, publicId, FileContentType_Dicom)) { std::unique_ptr<File> f(new File(s + ".dcm")); f->SetMimeType(MimeType_Dicom); @@ -508,7 +509,8 @@ LookupTime(time, context_, *it, ResourceType_Instance, MetadataType_Instance_ReceptionDate); FileInfo info; - if (context_.GetIndex().LookupAttachment(info, *it, FileContentType_Dicom)) + int64_t revision; // Ignored + if (context_.GetIndex().LookupAttachment(info, revision, *it, FileContentType_Dicom)) { std::unique_ptr<File> resource(new File(*it + ".dcm")); resource->SetMimeType(MimeType_Dicom);
--- a/OrthancServer/Sources/ServerContext.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -753,13 +753,16 @@ FileContentType content) { FileInfo attachment; - if (!index_.LookupAttachment(attachment, resourceId, content)) + int64_t revision; + if (!index_.LookupAttachment(attachment, revision, resourceId, content)) { throw OrthancException(ErrorCode_UnknownResource); } - - StorageAccessor accessor(area_, GetMetricsRegistry()); - accessor.AnswerFile(output, attachment, GetFileContentMime(content)); + else + { + StorageAccessor accessor(area_, GetMetricsRegistry()); + accessor.AnswerFile(output, attachment, GetFileContentMime(content)); + } } @@ -773,7 +776,8 @@ << compression; FileInfo attachment; - if (!index_.LookupAttachment(attachment, resourceId, attachmentType)) + int64_t revision; + if (!index_.LookupAttachment(attachment, revision, resourceId, attachmentType)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -794,7 +798,8 @@ try { - StoreStatus status = index_.AddAttachment(modified, resourceId); + int64_t newRevision; // ignored + StoreStatus status = index_.AddAttachment(newRevision, modified, resourceId, true, revision); if (status != StoreStatus_Success) { accessor.Remove(modified); @@ -833,8 +838,9 @@ **/ FileInfo attachment; + int64_t revision; - if (index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomUntilPixelData)) + if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData)) { std::string dicom; @@ -893,7 +899,7 @@ if (hasPixelDataOffset && area_.HasReadRange() && - index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom) && + index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom) && attachment.GetCompressionType() == CompressionType_None) { /** @@ -922,7 +928,7 @@ } } else if (ignoreTagLength.empty() && - index_.LookupAttachment(attachment, instancePublicId, FileContentType_DicomAsJson)) + index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomAsJson)) { /** * CASE 3: This instance was created using Orthanc <= @@ -978,8 +984,10 @@ if (!area_.HasReadRange() || compressionEnabled_) { - AddAttachment(instancePublicId, FileContentType_DicomUntilPixelData, - dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset); + int64_t newRevision; + AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData, + dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset, + false /* no old revision */, -1 /* dummy revision */); } } } @@ -999,7 +1007,8 @@ void ServerContext::ReadDicom(std::string& dicom, const std::string& instancePublicId) { - ReadAttachment(dicom, instancePublicId, FileContentType_Dicom, true /* uncompress */); + int64_t revision; + ReadAttachment(dicom, revision, instancePublicId, FileContentType_Dicom, true /* uncompress */); } @@ -1012,14 +1021,14 @@ } FileInfo attachment; - if (!index_.LookupAttachment(attachment, instancePublicId, FileContentType_Dicom)) + int64_t revision; // Ignored + if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom)) { throw OrthancException(ErrorCode_InternalError, "Unable to read the DICOM file of instance " + instancePublicId); } std::string s; - int64_t revision; // Ignored if (attachment.GetCompressionType() == CompressionType_None && index_.LookupMetadata(s, revision, instancePublicId, ResourceType_Instance, @@ -1046,12 +1055,13 @@ void ServerContext::ReadAttachment(std::string& result, + int64_t& revision, const std::string& instancePublicId, FileContentType content, bool uncompressIfNeeded) { FileInfo attachment; - if (!index_.LookupAttachment(attachment, instancePublicId, content)) + if (!index_.LookupAttachment(attachment, revision, instancePublicId, content)) { throw OrthancException(ErrorCode_InternalError, "Unable to read attachment " + EnumerationToString(content) + @@ -1147,10 +1157,13 @@ } - bool ServerContext::AddAttachment(const std::string& resourceId, + bool ServerContext::AddAttachment(int64_t& newRevision, + const std::string& resourceId, FileContentType attachmentType, const void* data, - size_t size) + size_t size, + bool hasOldRevision, + int64_t oldRevision) { LOG(INFO) << "Adding attachment " << EnumerationToString(attachmentType) << " to resource " << resourceId; @@ -1160,7 +1173,7 @@ StorageAccessor accessor(area_, GetMetricsRegistry()); FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_); - StoreStatus status = index_.AddAttachment(attachment, resourceId); + StoreStatus status = index_.AddAttachment(newRevision, attachment, resourceId, hasOldRevision, oldRevision); if (status != StoreStatus_Success) { accessor.Remove(attachment);
--- a/OrthancServer/Sources/ServerContext.h Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/ServerContext.h Tue Apr 20 15:11:59 2021 +0200 @@ -295,10 +295,13 @@ return compressionEnabled_; } - bool AddAttachment(const std::string& resourceId, + bool AddAttachment(int64_t& newRevision, + const std::string& resourceId, FileContentType attachmentType, const void* data, - size_t size); + size_t size, + bool hasOldRevision, + int64_t oldRevision); StoreStatus Store(std::string& resultPublicId, DicomInstanceToStore& dicom, @@ -327,6 +330,7 @@ // This method is for low-level operations on "/instances/.../attachments/..." void ReadAttachment(std::string& result, + int64_t& revision, const std::string& instancePublicId, FileContentType content, bool uncompressIfNeeded);
--- a/OrthancServer/Sources/ServerIndex.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -546,8 +546,11 @@ } - StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment, - const std::string& publicId) + StoreStatus ServerIndex::AddAttachment(int64_t& newRevision, + const FileInfo& attachment, + const std::string& publicId, + bool hasOldRevision, + int64_t oldRevision) { uint64_t maximumStorageSize; unsigned int maximumPatients; @@ -559,6 +562,6 @@ } return StatelessDatabaseOperations::AddAttachment( - attachment, publicId, maximumStorageSize, maximumPatients); + newRevision, attachment, publicId, maximumStorageSize, maximumPatients, hasOldRevision, oldRevision); } }
--- a/OrthancServer/Sources/ServerIndex.h Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/ServerIndex.h Tue Apr 20 15:11:59 2021 +0200 @@ -97,7 +97,10 @@ bool hasPixelDataOffset, uint64_t pixelDataOffset); - StoreStatus AddAttachment(const FileInfo& attachment, - const std::string& publicId); + StoreStatus AddAttachment(int64_t& newRevision /*out*/, + const FileInfo& attachment, + const std::string& publicId, + bool hasOldRevision, + int64_t oldRevision); }; }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -229,7 +229,8 @@ if (level_ == ResourceType_Instance) { FileInfo tmp; - if (index.LookupAttachment(tmp, id, FileContentType_Dicom)) + int64_t revision; // ignored + if (index.LookupAttachment(tmp, revision, id, FileContentType_Dicom)) { instances_.push_back(Instance(id, tmp.GetUncompressedSize())); }
--- a/OrthancServer/Sources/ServerToolbox.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -165,7 +165,8 @@ // Get the DICOM file attached to some instances in the resource FileInfo attachment; - if (!transaction.LookupAttachment(attachment, instance, FileContentType_Dicom)) + int64_t revision; + if (!transaction.LookupAttachment(attachment, revision, instance, FileContentType_Dicom)) { throw OrthancException(ErrorCode_InternalError, "Cannot retrieve the DICOM file associated with instance " + @@ -294,7 +295,8 @@ ServerContext::DicomCacheLocker locker(context, *it); // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext" - context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson); + context.GetIndex().DeleteAttachment( + *it, FileContentType_DicomAsJson, false /* no revision */, -1 /* dummy revision */); context.GetIndex().ReconstructInstance(locker.GetDicom()); }
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Mon Apr 19 10:28:43 2021 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Apr 20 15:11:59 2021 +0200 @@ -302,10 +302,10 @@ ASSERT_EQ(0u, md.size()); transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", - CompressionType_ZlibWithSize, 21, "compressedMD5")); - transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5")); - transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5")); - + CompressionType_ZlibWithSize, 21, "compressedMD5"), 42); + transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"), 43); + transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"), 44); + // TODO - REVISIONS - "42" is revision number, that is not currently stored (*) transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42); @@ -362,7 +362,8 @@ ASSERT_EQ("World", s); FileInfo att; - ASSERT_TRUE(transaction_->LookupAttachment(att, a[4], FileContentType_DicomAsJson)); + ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson)); + ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) ASSERT_EQ("my json file", att.GetUuid()); ASSERT_EQ(21u, att.GetCompressedSize()); ASSERT_EQ("md5", att.GetUncompressedMD5()); @@ -370,7 +371,8 @@ ASSERT_EQ(42u, att.GetUncompressedSize()); ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType()); - ASSERT_TRUE(transaction_->LookupAttachment(att, a[6], FileContentType_Dicom)); + ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom)); + ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) ASSERT_EQ("world", att.GetUuid()); ASSERT_EQ(44u, att.GetCompressedSize()); ASSERT_EQ("md5", att.GetUncompressedMD5()); @@ -482,7 +484,7 @@ std::string p = "Patient " + boost::lexical_cast<std::string>(i); patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast<std::string>(i))); + "md5-" + boost::lexical_cast<std::string>(i)), 42); ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } @@ -543,7 +545,7 @@ std::string p = "Patient " + boost::lexical_cast<std::string>(i); patients.push_back(transaction_->CreateResource(p, ResourceType_Patient)); transaction_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, - "md5-" + boost::lexical_cast<std::string>(i))); + "md5-" + boost::lexical_cast<std::string>(i)), 42); ASSERT_FALSE(transaction_->IsProtectedPatient(patients[i])); } @@ -783,7 +785,9 @@ for (size_t i = 0; i < ids.size(); i++) { FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5"); - index.AddAttachment(info, ids[i]); + int64_t revision = -1; + index.AddAttachment(revision, info, ids[i], false /* no previous revision */, -1); + ASSERT_EQ(0, revision); index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances); @@ -861,12 +865,17 @@ { FileInfo nope; - ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson)); + int64_t revision; + ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson)); } FileInfo dicom1, pixelData1; - ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, id, FileContentType_Dicom)); - ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, id, FileContentType_DicomUntilPixelData)); + int64_t revision; + ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, revision, id, FileContentType_Dicom)); + ASSERT_EQ(0, revision); + revision = -1; + ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData1, revision, id, FileContentType_DicomUntilPixelData)); + ASSERT_EQ(0, revision); context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances); @@ -906,12 +915,16 @@ { FileInfo nope; - ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, id, FileContentType_DicomAsJson)); + int64_t revision; + ASSERT_FALSE(context.GetIndex().LookupAttachment(nope, revision, id, FileContentType_DicomAsJson)); } FileInfo dicom2, pixelData2; - ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, id, FileContentType_Dicom)); - ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, id, FileContentType_DicomUntilPixelData)); + ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, revision, id, FileContentType_Dicom)); + ASSERT_EQ(0, revision); + revision = -1; + ASSERT_TRUE(context.GetIndex().LookupAttachment(pixelData2, revision, id, FileContentType_DicomUntilPixelData)); + ASSERT_EQ(0, revision); context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances);