changeset 5783:56352ae88120 find-refactoring

wip: new ReadOnly configuration
author Alain Mazy <am@orthanc.team>
date Mon, 16 Sep 2024 18:31:37 +0200
parents f1ccb67fce31
children b0d778f1e66d
files NEWS OrthancFramework/Resources/CodeGeneration/ErrorCodes.json OrthancFramework/Sources/Enumerations.cpp OrthancFramework/Sources/Enumerations.h OrthancFramework/Sources/SQLite/OrthancSQLiteException.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Resources/Configuration.json OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/main.cpp
diffstat 15 files changed, 205 insertions(+), 65 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Sat Sep 14 11:24:11 2024 +0200
+++ b/NEWS	Mon Sep 16 18:31:37 2024 +0200
@@ -5,6 +5,18 @@
   - /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1')
   - /studies?since=x&limit=0 and sibbling routes: limit=0 now means "no limit" instead of "no results"
 
+General
+-------
+
+* Database:
+  - Introduced database optimizations "ExtendedFind" to replace many small SQL queries
+    by a small number of large SQL queries to greatly reduce the cost of DB latency.
+    Furthermore, this "ExtendedFind" brings new sorting and filtering features to the 
+    Rest API (TODO).
+  - Introduced database optimizations "ExtendedChanges" to allow filtering of /changes.
+  - Reduced the number of SQL queries when ingesting DICOM files.
+
+
 REST API
 --------
 
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Mon Sep 16 18:31:37 2024 +0200
@@ -262,6 +262,11 @@
     "Name": "DuplicateResource", 
     "Description": "Duplicate resource"
   }, 
+  {
+    "Code": 47,
+    "Name": "IncompatibleConfigurations", 
+    "Description": "Your configuration file contains configuration that are mutually incompatible"
+  },
 
 
 
--- a/OrthancFramework/Sources/Enumerations.cpp	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancFramework/Sources/Enumerations.cpp	Mon Sep 16 18:31:37 2024 +0200
@@ -186,6 +186,9 @@
       case ErrorCode_DuplicateResource:
         return "Duplicate resource";
 
+      case ErrorCode_IncompatibleConfigurations:
+        return "Your configuration file contains configuration that are mutually incompatible";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -220,7 +223,7 @@
         return "SQLite: Cannot step over a cached statement";
 
       case ErrorCode_SQLiteBindOutOfRange:
-        return "SQLite: Bing a value while out of range (serious error)";
+        return "SQLite: Bind a value while out of range (serious error)";
 
       case ErrorCode_SQLitePrepareStatement:
         return "SQLite: Cannot prepare a cached statement";
--- a/OrthancFramework/Sources/Enumerations.h	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancFramework/Sources/Enumerations.h	Mon Sep 16 18:31:37 2024 +0200
@@ -172,6 +172,7 @@
     ErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
     ErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
     ErrorCode_DuplicateResource = 46    /*!< Duplicate resource */,
+    ErrorCode_IncompatibleConfigurations = 47    /*!< Your configuration file contains configuration that are mutually incompatible */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -183,7 +184,7 @@
     ErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
     ErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
     ErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
-    ErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
+    ErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bind a value while out of range (serious error) */,
     ErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
     ErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
     ErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
--- a/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Mon Sep 16 18:31:37 2024 +0200
@@ -129,7 +129,7 @@
             return "SQLite: Cannot step over a cached statement";
 
           case ErrorCode_SQLiteBindOutOfRange:
-            return "SQLite: Bing a value while out of range (serious error)";
+            return "SQLite: Bind a value while out of range (serious error)";
 
           case ErrorCode_SQLitePrepareStatement:
             return "SQLite: Cannot prepare a cached statement";
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Sep 16 18:31:37 2024 +0200
@@ -262,6 +262,7 @@
     OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
     OrthancPluginErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
     OrthancPluginErrorCode_DuplicateResource = 46    /*!< Duplicate resource */,
+    OrthancPluginErrorCode_IncompatibleConfigurations = 47    /*!< Your configuration file contains configuration that are mutually incompatible */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -273,7 +274,7 @@
     OrthancPluginErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
     OrthancPluginErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
     OrthancPluginErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
-    OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
+    OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bind a value while out of range (serious error) */,
     OrthancPluginErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
     OrthancPluginErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
     OrthancPluginErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
--- a/OrthancServer/Resources/Configuration.json	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Resources/Configuration.json	Mon Sep 16 18:31:37 2024 +0200
@@ -991,6 +991,13 @@
     // Display a warning message when Orthanc and its plugins are unable
     // to decode a frame (new in Orthanc 1.12.5).
     "W003_DecoderFailure": true
-  }
+  },
+
+  // Configure Orthanc in read only mode.
+  // In this mode, many Orthanc features that requires a write access to the 
+  // Index DB or the disk storage won't be available at all.
+  // (new in Orthanc 1.12.5)
+  "ReadOnly" : false
+
 
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Sep 16 18:31:37 2024 +0200
@@ -481,6 +481,10 @@
         else
         {
           assert(writeOperations != NULL);
+          if (readOnly_)
+          {
+            throw OrthancException(ErrorCode_ReadOnly, "The DB is trying to execute a ReadWrite transaction while Orthanc has been started in ReadOnly mode.");
+          }
           
           Transaction transaction(db_, *factory_, TransactionType_ReadWrite);
           {
@@ -518,10 +522,11 @@
   }
 
   
-  StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : 
+  StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db, bool readOnly) : 
     db_(db),
     mainDicomTagsRegistry_(new MainDicomTagsRegistry),
-    maxRetries_(0)
+    maxRetries_(0),
+    readOnly_(readOnly)
   {
   }
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Sep 16 18:31:37 2024 +0200
@@ -598,6 +598,7 @@
     boost::shared_mutex                          mutex_;
     std::unique_ptr<ITransactionContextFactory>  factory_;
     unsigned int                                 maxRetries_;
+    bool                                         readOnly_;
 
     void ApplyInternal(IReadOnlyOperations* readOperations,
                        IReadWriteOperations* writeOperations);
@@ -608,7 +609,7 @@
                              unsigned int maximumPatientCount);
 
   public:
-    explicit StatelessDatabaseOperations(IDatabaseWrapper& database);
+    explicit StatelessDatabaseOperations(IDatabaseWrapper& database, bool readOnly);
 
     void SetTransactionContextFactory(ITransactionContextFactory* factory /* takes ownership */);
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Sep 16 18:31:37 2024 +0200
@@ -4369,13 +4369,23 @@
     Register("/series", ListResources<ResourceType_Series>);
     Register("/studies", ListResources<ResourceType_Study>);
 
-    Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
+    if (!context_.IsReadOnly())
+    {
+      Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
+      Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
+      Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
+      Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
+
+      Register("/tools/bulk-delete", BulkDelete);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: DELETE routes are not available";
+    }
+
     Register("/instances/{id}", GetSingleResource<ResourceType_Instance>);
-    Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
     Register("/patients/{id}", GetSingleResource<ResourceType_Patient>);
-    Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
     Register("/series/{id}", GetSingleResource<ResourceType_Series>);
-    Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
     Register("/studies/{id}", GetSingleResource<ResourceType_Study>);
 
     Register("/instances/{id}/statistics", GetResourceStatistics);
@@ -4420,7 +4430,16 @@
     Register("/instances/{id}/numpy", GetNumpyInstance);  // New in Orthanc 1.10.0
 
     Register("/patients/{id}/protected", IsProtectedPatient);
-    Register("/patients/{id}/protected", SetPatientProtection);
+  
+    if (!context_.IsReadOnly())
+    {
+      Register("/patients/{id}/protected", SetPatientProtection);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: PUT /patients/{id}/protected route is not available";
+    }
+
 
     std::vector<std::string> resourceTypes;
     resourceTypes.push_back("patients");
@@ -4438,14 +4457,15 @@
       // New in Orthanc 1.12.0
       Register("/" + resourceTypes[i] + "/{id}/labels", ListLabels);
       Register("/" + resourceTypes[i] + "/{id}/labels/{label}", GetLabel);
-      Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel);
-      Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel);
+
+      if (!context_.IsReadOnly())
+      {
+        Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel);
+        Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel);
+      }
 
       Register("/" + resourceTypes[i] + "/{id}/attachments", ListAttachments);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", GetAttachmentOperations);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", UploadAttachment);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize);
@@ -4453,12 +4473,29 @@
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/is-compressed", IsAttachmentCompressed);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/md5", GetAttachmentMD5);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/size", GetAttachmentSize);
-      Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/info", GetAttachmentInfo);
       Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/verify-md5", VerifyAttachment);
+
+      if (!context_.IsReadOnly())
+      {
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment);
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", UploadAttachment);
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>);
+        Register("/" + resourceTypes[i] + "/{id}/attachments/{name}/uncompress", ChangeAttachmentCompression<CompressionType_None>);
+      }
     }
 
-    Register("/tools/invalidate-tags", InvalidateTags);
+    if (context_.IsReadOnly())
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT, POST and DELETE attachments routes";
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating PUT and DELETE labels routes";
+    }
+
+    if (!context_.IsReadOnly())
+    {
+      Register("/tools/invalidate-tags", InvalidateTags);
+    }
+
     Register("/tools/lookup", Lookup);
     Register("/tools/find", Find);
 
@@ -4485,13 +4522,19 @@
     Register("/series/{id}/ordered-slices", OrderSlices);
     Register("/series/{id}/numpy", GetNumpySeries);  // New in Orthanc 1.10.0
 
-    Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
-    Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
-    Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
-    Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);
-    Register("/tools/reconstruct", ReconstructAllResources);
+    if (!context_.IsReadOnly())
+    {
+      Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
+      Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
+      Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
+      Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);
+      Register("/tools/reconstruct", ReconstructAllResources);
+    }
+    else
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: deactivating /reconstruct routes";
+    }
 
     Register("/tools/bulk-content", BulkContent);
-    Register("/tools/bulk-delete", BulkDelete);
   }
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Sep 16 18:31:37 2024 +0200
@@ -95,6 +95,7 @@
     static const char* const CAPABILITIES = "Capabilities";
     static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges";
     static const char* const HAS_EXTENDED_FIND = "HasExtendedFind";
+    static const char* const READ_ONLY = "ReadOnly";
 
     if (call.IsDocumentation())
     {
@@ -143,6 +144,8 @@
                         "Whether the database back-end supports labels (new in Orthanc 1.12.0)")
         .SetAnswerField(CAPABILITIES, RestApiCallDocumentation::Type_JsonObject,
                         "Whether the back-end supports optional features like 'HasExtendedChanges', 'HasExtendedFind' (new in Orthanc 1.12.5) ")
+        .SetAnswerField(READ_ONLY, RestApiCallDocumentation::Type_Boolean,
+                        "Whether Orthanc is running in read only mode (new in Orthanc 1.12.5)")
         .SetHttpGetSample("https://orthanc.uclouvain.be/demo/system", true);
       return;
     }
@@ -174,6 +177,7 @@
 
     result[STORAGE_AREA_PLUGIN] = Json::nullValue;
     result[DATABASE_BACKEND_PLUGIN] = Json::nullValue;
+    result[READ_ONLY] = context.IsReadOnly();
 
 #if ORTHANC_ENABLE_PLUGINS == 1
     result[PLUGINS_ENABLED] = true;
--- a/OrthancServer/Sources/ServerContext.cpp	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Mon Sep 16 18:31:37 2024 +0200
@@ -389,7 +389,8 @@
     ingestTranscodingOfCompressed_(true),
     preferredTransferSyntax_(DicomTransferSyntax_LittleEndianExplicit),
     deidentifyLogs_(false),
-    serverStartTimeUtc_(boost::posix_time::second_clock::universal_time())
+    serverStartTimeUtc_(boost::posix_time::second_clock::universal_time()),
+    readOnly_(false)
   {
     try
     {
--- a/OrthancServer/Sources/ServerContext.h	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Mon Sep 16 18:31:37 2024 +0200
@@ -277,6 +277,7 @@
     boost::mutex dynamicOptionsMutex_;
     bool isUnknownSopClassAccepted_;
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
+    bool readOnly_;
 
     StoreResult StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
@@ -345,6 +346,21 @@
       return compressionEnabled_;
     }
 
+    void SetReadOnly(bool readOnly)
+    {
+      readOnly_ = true;
+    }
+    
+    bool IsReadOnly() const
+    {
+      return readOnly_;
+    }
+
+    bool IsSaveJobs() const
+    {
+      return saveJobs_;
+    }
+
     bool AddAttachment(int64_t& newRevision,
                        const std::string& resourceId,
                        FileContentType attachmentType,
--- a/OrthancServer/Sources/ServerIndex.cpp	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Mon Sep 16 18:31:37 2024 +0200
@@ -347,7 +347,7 @@
   ServerIndex::ServerIndex(ServerContext& context,
                            IDatabaseWrapper& db,
                            unsigned int threadSleepGranularityMilliseconds) :
-    StatelessDatabaseOperations(db),
+    StatelessDatabaseOperations(db, context.IsReadOnly()),
     done_(false),
     maximumStorageMode_(MaxStorageMode_Recycle),
     maximumStorageSize_(0),
@@ -357,12 +357,22 @@
 
     // Initial recycling if the parameters have changed since the last
     // execution of Orthanc
-    StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
+    if (!context.IsReadOnly())
+    {
+      StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
+    }
 
     // For some DB engines (like SQLite), make sure we flush the DB to disk at regular interval
     if (GetDatabaseCapabilities().HasFlushToDisk())
     {
-      flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds);
+      if (context.IsReadOnly())
+      {
+        LOG(WARNING) << "READ-ONLY SYSTEM: not starting the flush disk thread";
+      }
+      else
+      {
+        flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds);
+      }
     }
 
     // For some DB plugins that implements the UpdateAndGetStatistics function, updating 
@@ -370,11 +380,25 @@
     // -> make sure they are updated at regular interval
     if (GetDatabaseCapabilities().HasUpdateAndGetStatistics())
     {
-      updateStatisticsThread_ = boost::thread(UpdateStatisticsThread, this, threadSleepGranularityMilliseconds);
+      if (context.IsReadOnly())
+      {
+        LOG(WARNING) << "READ-ONLY SYSTEM: not starting the UpdateStatisticsThread";
+      }
+      else
+      {
+        updateStatisticsThread_ = boost::thread(UpdateStatisticsThread, this, threadSleepGranularityMilliseconds);
+      }
     }
 
-    unstableResourcesMonitorThread_ = boost::thread
-      (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds);
+    if (context.IsReadOnly())
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: not starting the unstable resources monitor thread";
+    }
+    else
+    {
+      unstableResourcesMonitorThread_ = boost::thread
+        (UnstableResourcesMonitorThread, this, threadSleepGranularityMilliseconds);
+    }
   }
 
 
--- a/OrthancServer/Sources/main.cpp	Sat Sep 14 11:24:11 2024 +0200
+++ b/OrthancServer/Sources/main.cpp	Mon Sep 16 18:31:37 2024 +0200
@@ -825,6 +825,7 @@
     PrintErrorCode(ErrorCode_MainDicomTagsMultiplyDefined, "A main DICOM Tag has been defined multiple times for the same resource level");
     PrintErrorCode(ErrorCode_ForbiddenAccess, "Access to a resource is forbidden");
     PrintErrorCode(ErrorCode_DuplicateResource, "Duplicate resource");
+    PrintErrorCode(ErrorCode_IncompatibleConfigurations, "Your configuration file contains configuration that are mutually incompatible");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
@@ -836,7 +837,7 @@
     PrintErrorCode(ErrorCode_SQLiteFlush, "SQLite: Unable to flush the database");
     PrintErrorCode(ErrorCode_SQLiteCannotRun, "SQLite: Cannot run a cached statement");
     PrintErrorCode(ErrorCode_SQLiteCannotStep, "SQLite: Cannot step over a cached statement");
-    PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bing a value while out of range (serious error)");
+    PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bind a value while out of range (serious error)");
     PrintErrorCode(ErrorCode_SQLitePrepareStatement, "SQLite: Cannot prepare a cached statement");
     PrintErrorCode(ErrorCode_SQLiteTransactionAlreadyStarted, "SQLite: Beginning the same transaction twice");
     PrintErrorCode(ErrorCode_SQLiteTransactionCommit, "SQLite: Failure when committing the transaction");
@@ -1563,41 +1564,57 @@
   {
     OrthancConfiguration::ReaderLock lock;
 
-    context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
-    context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
+    // New option in Orthanc 1.12.5
+    context.SetReadOnly(lock.GetConfiguration().GetBooleanParameter("ReadOnly", false));
+
+    if (context.IsReadOnly())
+    {
+      LOG(WARNING) << "READ-ONLY SYSTEM: ignoring these configurations: StorageCompression, StoreMD5ForAttachments, OverwriteInstances, MaximumPatientCount, MaximumStorageSize, MaximumStorageMode"; 
 
-    // New option in Orthanc 1.4.2
-    context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
+      if (context.IsSaveJobs())
+      {
+        throw OrthancException(ErrorCode_IncompatibleConfigurations, "\"SaveJobs\" can not be true when \"ReadOnly\" is true");
+      }
+    }
+    else
+    {
+      context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
+      context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
+
+      // New option in Orthanc 1.4.2
+      context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
 
-    try
-    {
-      context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumPatientCount(0);
+      try
+      {
+        context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
+      }
+      catch (...)
+      {
+        context.GetIndex().SetMaximumPatientCount(0);
+      }
+
+      try
+      {
+        uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
+        context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
+      }
+      catch (...)
+      {
+        context.GetIndex().SetMaximumStorageSize(0);
+      }
+
+      try
+      {
+        std::string mode = lock.GetConfiguration().GetStringParameter("MaximumStorageMode", "Recycle");
+        context.GetIndex().SetMaximumStorageMode(StringToMaxStorageMode(mode));
+      }
+      catch (...)
+      {
+        context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle);
+      }
     }
 
-    try
-    {
-      uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
-      context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumStorageSize(0);
-    }
-
-    try
-    {
-      std::string mode = lock.GetConfiguration().GetStringParameter("MaximumStorageMode", "Recycle");
-      context.GetIndex().SetMaximumStorageMode(StringToMaxStorageMode(mode));
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle);
-    }
-
+    // note: this config is valid in ReadOnlyMode
     try
     {
       uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageCacheSize", 128);