Mercurial > hg > orthanc-indexer
annotate Sources/Plugin.cpp @ 4:da409b29cc02
by default, put the indexer database next to the orthanc database
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 23 Sep 2021 13:14:31 +0200 |
parents | e731f308b8b1 |
children | f76fdef338a3 |
rev | line source |
---|---|
0 | 1 /** |
2 * Indexer plugin for Orthanc | |
3 * Copyright (C) 2021 Sebastien Jodogne, UCLouvain, Belgium | |
4 * | |
5 * This program is free software: you can redistribute it and/or | |
6 * modify it under the terms of the GNU General Public License as | |
7 * published by the Free Software Foundation, either version 3 of the | |
8 * License, or (at your option) any later version. | |
9 * | |
10 * This program is distributed in the hope that it will be useful, but | |
11 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
13 * General Public License for more details. | |
14 * | |
15 * You should have received a copy of the GNU General Public License | |
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
17 **/ | |
18 | |
19 | |
20 #include "IndexerDatabase.h" | |
21 #include "StorageArea.h" | |
22 | |
23 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" | |
24 | |
25 #include <DicomFormat/DicomInstanceHasher.h> | |
26 #include <DicomFormat/DicomMap.h> | |
27 #include <Logging.h> | |
28 #include <SerializationToolbox.h> | |
29 #include <SystemToolbox.h> | |
30 | |
31 #include <boost/filesystem.hpp> | |
32 #include <boost/thread.hpp> | |
3
e731f308b8b1
added missing include
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
1
diff
changeset
|
33 #include <stack> |
0 | 34 |
35 | |
36 static std::list<std::string> folders_; | |
37 static IndexerDatabase database_; | |
38 static std::unique_ptr<StorageArea> storageArea_; | |
39 | |
40 | |
41 | |
42 static bool ComputeInstanceId(std::string& instanceId, | |
43 const void* dicom, | |
44 size_t size) | |
45 { | |
46 if (size > 0 && | |
47 Orthanc::DicomMap::IsDicomFile(dicom, size)) | |
48 { | |
49 try | |
50 { | |
51 OrthancPlugins::OrthancString s; | |
52 s.Assign(OrthancPluginDicomBufferToJson(OrthancPlugins::GetGlobalContext(), dicom, size, | |
53 OrthancPluginDicomToJsonFormat_Short, | |
54 OrthancPluginDicomToJsonFlags_None, 256)); | |
55 | |
56 Json::Value json; | |
57 s.ToJson(json); | |
58 | |
59 static const char* const PATIENT_ID = "0010,0020"; | |
60 static const char* const STUDY_INSTANCE_UID = "0020,000d"; | |
61 static const char* const SERIES_INSTANCE_UID = "0020,000e"; | |
62 static const char* const SOP_INSTANCE_UID = "0008,0018"; | |
63 | |
64 Orthanc::DicomInstanceHasher hasher( | |
65 json.isMember(PATIENT_ID) ? Orthanc::SerializationToolbox::ReadString(json, PATIENT_ID) : "", | |
66 Orthanc::SerializationToolbox::ReadString(json, STUDY_INSTANCE_UID), | |
67 Orthanc::SerializationToolbox::ReadString(json, SERIES_INSTANCE_UID), | |
68 Orthanc::SerializationToolbox::ReadString(json, SOP_INSTANCE_UID)); | |
69 | |
70 instanceId = hasher.HashInstance(); | |
71 return true; | |
72 } | |
73 catch (Orthanc::OrthancException&) | |
74 { | |
75 return false; | |
76 } | |
77 } | |
78 else | |
79 { | |
80 return false; | |
81 } | |
82 } | |
83 | |
84 | |
85 | |
86 static void ProcessFile(const std::string& path, | |
87 const std::time_t time, | |
88 const uintmax_t size) | |
89 { | |
90 std::string oldInstanceId; | |
91 IndexerDatabase::FileStatus status = database_.LookupFile(oldInstanceId, path, time, size); | |
92 | |
93 if (status == IndexerDatabase::FileStatus_New || | |
94 status == IndexerDatabase::FileStatus_Modified) | |
95 { | |
96 if (status == IndexerDatabase::FileStatus_Modified) | |
97 { | |
98 database_.RemoveFile(path); | |
99 } | |
100 | |
101 std::string dicom; | |
102 Orthanc::SystemToolbox::ReadFile(dicom, path); | |
103 | |
104 std::string instanceId; | |
105 if (!dicom.empty() && | |
106 ComputeInstanceId(instanceId, dicom.c_str(), dicom.size())) | |
107 { | |
108 LOG(INFO) << "New DICOM file detected by the indexer plugin: " << path; | |
109 | |
110 // The following line must be *before* the "RestApiDelete()" to | |
111 // deal with the case of having two copies of the same DICOM | |
112 // file in the indexed folders, but with different timestamps | |
113 database_.AddDicomInstance(path, time, size, instanceId); | |
114 | |
115 if (status == IndexerDatabase::FileStatus_Modified) | |
116 { | |
117 OrthancPlugins::RestApiDelete("/instances/" + oldInstanceId, false); | |
118 } | |
119 | |
120 try | |
121 { | |
122 Json::Value upload; | |
123 OrthancPlugins::RestApiPost(upload, "/instances", dicom, false); | |
124 } | |
125 catch (Orthanc::OrthancException&) | |
126 { | |
127 } | |
128 } | |
129 else | |
130 { | |
131 LOG(INFO) << "Skipping indexing of non-DICOM file: " << path; | |
132 database_.AddNonDicomFile(path, time, size); | |
133 | |
134 if (status == IndexerDatabase::FileStatus_Modified) | |
135 { | |
136 OrthancPlugins::RestApiDelete("/instances/" + oldInstanceId, false); | |
137 } | |
138 } | |
139 } | |
140 } | |
141 | |
142 | |
143 static void LookupDeletedFiles() | |
144 { | |
145 class Visitor : public IndexerDatabase::IFileVisitor | |
146 { | |
147 private: | |
148 typedef std::pair<std::string, std::string> DeletedDicom; | |
149 | |
150 std::list<DeletedDicom> deletedDicom_; | |
151 | |
152 public: | |
153 virtual void VisitInstance(const std::string& path, | |
154 bool isDicom, | |
155 const std::string& instanceId) ORTHANC_OVERRIDE | |
156 { | |
157 if (!Orthanc::SystemToolbox::IsRegularFile(path) && | |
158 isDicom) | |
159 { | |
160 deletedDicom_.push_back(std::make_pair(path, instanceId)); | |
161 } | |
162 } | |
163 | |
164 void ExecuteDelete() | |
165 { | |
166 for (std::list<DeletedDicom>::const_iterator | |
167 it = deletedDicom_.begin(); it != deletedDicom_.end(); ++it) | |
168 { | |
169 const std::string& path = it->first; | |
170 const std::string& instanceId = it->second; | |
171 | |
172 if (database_.RemoveFile(path)) | |
173 { | |
174 OrthancPlugins::RestApiDelete("/instances/" + instanceId, false); | |
175 } | |
176 } | |
177 } | |
178 }; | |
179 | |
180 Visitor visitor; | |
181 database_.Apply(visitor); | |
182 visitor.ExecuteDelete(); | |
183 } | |
184 | |
185 | |
186 static void MonitorDirectories(bool* stop) | |
187 { | |
188 for (;;) | |
189 { | |
190 std::stack<boost::filesystem::path> s; | |
191 | |
192 for (std::list<std::string>::const_iterator it = folders_.begin(); | |
193 it != folders_.end(); ++it) | |
194 { | |
195 s.push(*it); | |
196 } | |
197 | |
198 while (!s.empty()) | |
199 { | |
200 if (*stop) | |
201 { | |
202 return; | |
203 } | |
204 | |
205 boost::filesystem::path d = s.top(); | |
206 s.pop(); | |
207 | |
208 boost::filesystem::directory_iterator current; | |
209 | |
210 try | |
211 { | |
212 current = boost::filesystem::directory_iterator(d); | |
213 } | |
214 catch (boost::filesystem::filesystem_error&) | |
215 { | |
216 LOG(INFO) << "Indexer plugin cannot read directory: " << d.string(); | |
217 } | |
218 | |
219 const boost::filesystem::directory_iterator end; | |
220 | |
221 while (current != end) | |
222 { | |
223 try | |
224 { | |
225 const boost::filesystem::file_status status = boost::filesystem::status(current->path()); | |
226 | |
227 switch (status.type()) | |
228 { | |
229 case boost::filesystem::regular_file: | |
230 case boost::filesystem::reparse_file: | |
231 try | |
232 { | |
233 ProcessFile(current->path().string(), | |
234 boost::filesystem::last_write_time(current->path()), | |
235 boost::filesystem::file_size(current->path())); | |
236 } | |
237 catch (Orthanc::OrthancException& e) | |
238 { | |
239 LOG(ERROR) << e.What(); | |
240 } | |
241 break; | |
242 | |
243 case boost::filesystem::directory_file: | |
244 s.push(current->path()); | |
245 break; | |
246 | |
247 default: | |
248 break; | |
249 } | |
250 } | |
251 catch (boost::filesystem::filesystem_error&) | |
252 { | |
253 } | |
254 | |
255 ++current; | |
256 } | |
257 } | |
258 | |
259 try | |
260 { | |
261 LookupDeletedFiles(); | |
262 } | |
263 catch (Orthanc::OrthancException& e) | |
264 { | |
265 LOG(ERROR) << e.What(); | |
266 } | |
267 | |
268 for (unsigned int i = 0; i < /*100*/10; i++) | |
269 { | |
270 if (*stop) | |
271 { | |
272 return; | |
273 } | |
274 | |
275 boost::this_thread::sleep(boost::posix_time::milliseconds(100)); | |
276 } | |
277 } | |
278 } | |
279 | |
280 | |
281 static OrthancPluginErrorCode StorageCreate(const char *uuid, | |
282 const void *content, | |
283 int64_t size, | |
284 OrthancPluginContentType type) | |
285 { | |
286 try | |
287 { | |
288 std::string instanceId; | |
289 if (type == OrthancPluginContentType_Dicom && | |
1
d745ea3db32c
fix import of external DICOM
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
0
diff
changeset
|
290 ComputeInstanceId(instanceId, content, size) && |
d745ea3db32c
fix import of external DICOM
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
0
diff
changeset
|
291 database_.AddAttachment(uuid, instanceId)) |
0 | 292 { |
293 // This attachment corresponds to an external DICOM file that is | |
294 // stored in one of the indexed folders, only store a link to it | |
295 } | |
296 else | |
297 { | |
298 // This attachment must be stored in the internal storage area | |
299 storageArea_->Create(uuid, content, size); | |
300 } | |
301 | |
302 return OrthancPluginErrorCode_Success; | |
303 } | |
304 catch (Orthanc::OrthancException& e) | |
305 { | |
306 LOG(ERROR) << e.What(); | |
307 return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); | |
308 } | |
309 catch (...) | |
310 { | |
311 return OrthancPluginErrorCode_InternalError; | |
312 } | |
313 } | |
314 | |
315 | |
316 | |
317 static bool LookupExternalDicom(std::string& externalPath, | |
318 const char *uuid, | |
319 OrthancPluginContentType type) | |
320 { | |
321 return (type == OrthancPluginContentType_Dicom && | |
322 database_.LookupAttachment(externalPath, uuid)); | |
323 } | |
324 | |
325 | |
326 static OrthancPluginErrorCode StorageReadRange(OrthancPluginMemoryBuffer64 *target, | |
327 const char *uuid, | |
328 OrthancPluginContentType type, | |
329 uint64_t rangeStart) | |
330 { | |
331 try | |
332 { | |
333 std::string externalPath; | |
334 if (LookupExternalDicom(externalPath, uuid, type)) | |
335 { | |
336 StorageArea::ReadRangeFromPath(target, externalPath, rangeStart); | |
337 } | |
338 else | |
339 { | |
340 storageArea_->ReadRange(target, uuid, rangeStart); | |
341 } | |
342 | |
343 return OrthancPluginErrorCode_Success; | |
344 } | |
345 catch (Orthanc::OrthancException& e) | |
346 { | |
347 LOG(ERROR) << e.What(); | |
348 return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); | |
349 } | |
350 catch (...) | |
351 { | |
352 return OrthancPluginErrorCode_InternalError; | |
353 } | |
354 } | |
355 | |
356 | |
357 static OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64 *target, | |
358 const char *uuid, | |
359 OrthancPluginContentType type) | |
360 { | |
361 try | |
362 { | |
363 std::string externalPath; | |
364 if (LookupExternalDicom(externalPath, uuid, type)) | |
365 { | |
366 StorageArea::ReadWholeFromPath(target, externalPath); | |
367 } | |
368 else | |
369 { | |
370 storageArea_->ReadWhole(target, uuid); | |
371 } | |
372 | |
373 return OrthancPluginErrorCode_Success; | |
374 } | |
375 catch (Orthanc::OrthancException& e) | |
376 { | |
377 LOG(ERROR) << e.What(); | |
378 return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); | |
379 } | |
380 catch (...) | |
381 { | |
382 return OrthancPluginErrorCode_InternalError; | |
383 } | |
384 } | |
385 | |
386 | |
387 static OrthancPluginErrorCode StorageRemove(const char *uuid, | |
388 OrthancPluginContentType type) | |
389 { | |
390 try | |
391 { | |
392 std::string externalPath; | |
393 if (LookupExternalDicom(externalPath, uuid, type)) | |
394 { | |
395 database_.RemoveAttachment(uuid); | |
396 } | |
397 else | |
398 { | |
399 storageArea_->RemoveAttachment(uuid); | |
400 } | |
401 | |
402 return OrthancPluginErrorCode_Success; | |
403 } | |
404 catch (Orthanc::OrthancException& e) | |
405 { | |
406 LOG(ERROR) << e.What(); | |
407 return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); | |
408 } | |
409 catch (...) | |
410 { | |
411 return OrthancPluginErrorCode_InternalError; | |
412 } | |
413 } | |
414 | |
415 | |
416 static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, | |
417 OrthancPluginResourceType resourceType, | |
418 const char* resourceId) | |
419 { | |
420 static bool stop_; | |
421 static boost::thread thread_; | |
422 | |
423 switch (changeType) | |
424 { | |
425 case OrthancPluginChangeType_OrthancStarted: | |
426 stop_ = false; | |
427 thread_ = boost::thread(MonitorDirectories, &stop_); | |
428 break; | |
429 | |
430 case OrthancPluginChangeType_OrthancStopped: | |
431 stop_ = true; | |
432 if (thread_.joinable()) | |
433 { | |
434 thread_.join(); | |
435 } | |
436 | |
437 break; | |
438 | |
439 default: | |
440 break; | |
441 } | |
442 | |
443 return OrthancPluginErrorCode_Success; | |
444 } | |
445 | |
446 | |
447 extern "C" | |
448 { | |
449 ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) | |
450 { | |
451 OrthancPlugins::SetGlobalContext(context); | |
452 Orthanc::Logging::InitializePluginContext(context); | |
453 Orthanc::Logging::EnableInfoLevel(true); | |
454 | |
455 /* Check the version of the Orthanc core */ | |
456 if (OrthancPluginCheckVersion(context) == 0) | |
457 { | |
458 OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, | |
459 ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, | |
460 ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); | |
461 return -1; | |
462 } | |
463 | |
464 OrthancPluginSetDescription(context, "Index directories out of Orthanc."); | |
465 | |
466 OrthancPlugins::OrthancConfiguration configuration; | |
467 | |
468 OrthancPlugins::OrthancConfiguration indexer; | |
469 configuration.GetSection(indexer, "Indexer"); | |
470 | |
471 bool enabled = indexer.GetBooleanValue("Enable", false); | |
472 if (enabled) | |
473 { | |
474 try | |
475 { | |
4
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
476 static const char* const DATABASE = "Database"; |
0 | 477 static const char* const FOLDERS = "Folders"; |
4
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
478 static const char* const INDEX_DIRECTORY = "IndexDirectory"; |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
479 static const char* const ORTHANC_STORAGE = "OrthancStorage"; |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
480 static const char* const STORAGE_DIRECTORY = "StorageDirectory"; |
0 | 481 |
482 if (!indexer.LookupListOfStrings(folders_, FOLDERS, true) || | |
483 folders_.empty()) | |
484 { | |
485 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, | |
486 "Missing configuration option for Indexer plugin: " + std::string(FOLDERS)); | |
487 } | |
488 | |
4
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
489 std::string path; |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
490 if (!indexer.LookupStringValue(path, DATABASE)) |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
491 { |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
492 std::string folder; |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
493 if (!configuration.LookupStringValue(folder, INDEX_DIRECTORY)) |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
494 { |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
495 folder = configuration.GetStringValue(STORAGE_DIRECTORY, ORTHANC_STORAGE); |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
496 } |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
497 |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
498 path = (boost::filesystem::path(folder) / "indexer.db").string(); |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
499 } |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
500 |
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
501 LOG(WARNING) << "Path to the database of the Indexer plugin: " << path; |
0 | 502 database_.Open(path); |
503 | |
4
da409b29cc02
by default, put the indexer database next to the orthanc database
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
3
diff
changeset
|
504 storageArea_.reset(new StorageArea(configuration.GetStringValue(STORAGE_DIRECTORY, ORTHANC_STORAGE))); |
0 | 505 } |
506 catch (Orthanc::OrthancException& e) | |
507 { | |
508 return -1; | |
509 } | |
510 catch (...) | |
511 { | |
512 LOG(ERROR) << "Native exception while initializing the plugin"; | |
513 return -1; | |
514 } | |
515 | |
516 OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); | |
517 OrthancPluginRegisterStorageArea2(context, StorageCreate, StorageReadWhole, StorageReadRange, StorageRemove); | |
518 } | |
519 else | |
520 { | |
521 OrthancPlugins::LogWarning("OrthancIndexer is disabled"); | |
522 } | |
523 | |
524 return 0; | |
525 } | |
526 | |
527 | |
528 ORTHANC_PLUGINS_API void OrthancPluginFinalize() | |
529 { | |
530 OrthancPlugins::LogWarning("Folder indexer plugin is finalizing"); | |
531 } | |
532 | |
533 | |
534 ORTHANC_PLUGINS_API const char* OrthancPluginGetName() | |
535 { | |
536 return "indexer"; | |
537 } | |
538 | |
539 | |
540 ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() | |
541 { | |
542 return ORTHANC_PLUGIN_VERSION; | |
543 } | |
544 } |