Mercurial > hg > orthanc
comparison OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp @ 5080:d7274e43ea7c attach-custom-data
allow plugins to store a customData in the Attachments table to e.g. store custom paths without requiring an external DB
author | Alain Mazy <am@osimis.io> |
---|---|
date | Thu, 08 Sep 2022 17:42:08 +0200 |
parents | |
children | c673997507ea |
comparison
equal
deleted
inserted
replaced
5079:4366b4c41441 | 5080:d7274e43ea7c |
---|---|
1 /** | |
2 * Orthanc - A Lightweight, RESTful DICOM Store | |
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics | |
4 * Department, University Hospital of Liege, Belgium | |
5 * Copyright (C) 2017-2022 Osimis S.A., Belgium | |
6 * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium | |
7 * | |
8 * This program is free software: you can redistribute it and/or | |
9 * modify it under the terms of the GNU General Public License as | |
10 * published by the Free Software Foundation, either version 3 of the | |
11 * License, or (at your option) any later version. | |
12 * | |
13 * This program is distributed in the hope that it will be useful, but | |
14 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
16 * General Public License for more details. | |
17 * | |
18 * You should have received a copy of the GNU General Public License | |
19 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 **/ | |
21 | |
22 | |
23 #include "../../../../OrthancFramework/Sources/Compatibility.h" | |
24 #include "../../../../OrthancFramework/Sources/OrthancException.h" | |
25 #include "../../../../OrthancFramework/Sources/SystemToolbox.h" | |
26 #include "../../../../OrthancFramework/Sources/Toolbox.h" | |
27 #include "../../../../OrthancFramework/Sources/Logging.h" | |
28 #include "../Common/OrthancPluginCppWrapper.h" | |
29 | |
30 #include <boost/filesystem.hpp> | |
31 #include <boost/filesystem/fstream.hpp> | |
32 #include <boost/iostreams/device/file_descriptor.hpp> | |
33 #include <boost/iostreams/stream.hpp> | |
34 | |
35 | |
36 #include <json/value.h> | |
37 #include <json/writer.h> | |
38 #include <string.h> | |
39 #include <iostream> | |
40 #include <algorithm> | |
41 #include <map> | |
42 #include <list> | |
43 #include <time.h> | |
44 | |
45 namespace fs = boost::filesystem; | |
46 | |
47 fs::path absoluteRootPath_; | |
48 bool fsyncOnWrite_ = true; | |
49 | |
50 | |
51 fs::path GetLegacyRelativePath(const std::string& uuid) | |
52 { | |
53 | |
54 if (!Orthanc::Toolbox::IsUuid(uuid)) | |
55 { | |
56 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
57 } | |
58 | |
59 fs::path path = absoluteRootPath_; | |
60 | |
61 path /= std::string(&uuid[0], &uuid[2]); | |
62 path /= std::string(&uuid[2], &uuid[4]); | |
63 path /= uuid; | |
64 | |
65 #if BOOST_HAS_FILESYSTEM_V3 == 1 | |
66 path.make_preferred(); | |
67 #endif | |
68 | |
69 return path; | |
70 } | |
71 | |
72 fs::path GetAbsolutePath(const std::string& uuid, const std::string& customData) | |
73 { | |
74 fs::path path = absoluteRootPath_; | |
75 | |
76 if (!customData.empty()) | |
77 { | |
78 if (customData.substr(0, 2) == "1.") // version 1 | |
79 { | |
80 path /= customData.substr(2); | |
81 } | |
82 else | |
83 { | |
84 throw "TODO: unknown version"; | |
85 } | |
86 | |
87 } | |
88 else | |
89 { | |
90 path /= GetLegacyRelativePath(uuid); | |
91 } | |
92 | |
93 path.make_preferred(); | |
94 return path; | |
95 } | |
96 | |
97 std::string GetCustomData(const fs::path& path) | |
98 { | |
99 return std::string("1.") + path.string(); // prefix the relative path with a version | |
100 } | |
101 | |
102 void AddDateDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL) | |
103 { | |
104 if (tags.isMember(tagName) && tags[tagName].asString().size() == 8) | |
105 { | |
106 std::string date = tags[tagName].asString(); | |
107 path /= date.substr(0, 4); | |
108 path /= date.substr(4, 2); | |
109 path /= date.substr(6, 2); | |
110 } | |
111 else if (defaultValue != NULL) | |
112 { | |
113 path /= defaultValue; | |
114 } | |
115 } | |
116 | |
117 void AddSringDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL) | |
118 { | |
119 if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0) | |
120 { | |
121 path /= tags[tagName].asString(); | |
122 } | |
123 else if (defaultValue != NULL) | |
124 { | |
125 path /= defaultValue; | |
126 } | |
127 } | |
128 | |
129 void AddIntDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, size_t zeroPaddingWidth = 0, const char* defaultValue = NULL) | |
130 { | |
131 if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0) | |
132 { | |
133 std::string tagValue = tags[tagName].asString(); | |
134 if (zeroPaddingWidth > 0 && tagValue.size() < zeroPaddingWidth) | |
135 { | |
136 std::string padding(zeroPaddingWidth - tagValue.size(), '0'); | |
137 path /= padding + tagValue; | |
138 } | |
139 else | |
140 { | |
141 path /= tagValue; | |
142 } | |
143 } | |
144 else if (defaultValue != NULL) | |
145 { | |
146 path /= defaultValue; | |
147 } | |
148 } | |
149 | |
150 fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed) | |
151 { | |
152 fs::path path; | |
153 | |
154 if (type == OrthancPluginContentType_Dicom || type == OrthancPluginContentType_DicomUntilPixelData) | |
155 { | |
156 // TODO: allow customization ... note: right now, we always need the uuid in the path !! | |
157 | |
158 AddDateDicomTagToPath(path, tags, "StudyDate", "NO_STUDY_DATE"); | |
159 AddSringDicomTagToPath(path, tags, "PatientID"); // no default value, tag is always present if the instance is accepted by Orthanc | |
160 AddSringDicomTagToPath(path, tags, "StudyInstanceUID"); | |
161 AddSringDicomTagToPath(path, tags, "SeriesInstanceUID"); | |
162 //AddIntDicomTagToPath(path, tags, "InstanceNumber", 8, uuid); | |
163 path /= uuid; | |
164 } | |
165 else | |
166 { | |
167 path = GetLegacyRelativePath(uuid); | |
168 } | |
169 | |
170 std::string extension; | |
171 | |
172 switch (type) | |
173 { | |
174 case OrthancPluginContentType_Dicom: | |
175 extension = ".dcm"; | |
176 break; | |
177 case OrthancPluginContentType_DicomUntilPixelData: | |
178 extension = ".dcm.head"; | |
179 break; | |
180 default: | |
181 extension = ".unk"; | |
182 } | |
183 if (isCompressed) | |
184 { | |
185 extension = extension + ".cmp"; // compression is zlib + size -> we can not use the .zip extension | |
186 } | |
187 | |
188 path += extension; | |
189 | |
190 return path; | |
191 } | |
192 | |
193 | |
194 OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData, | |
195 const char* uuid, | |
196 const Json::Value& tags, | |
197 const void* content, | |
198 int64_t size, | |
199 OrthancPluginContentType type, | |
200 bool isCompressed) | |
201 { | |
202 fs::path relativePath = GetRelativePathFromTags(tags, uuid, type, isCompressed); | |
203 std::string customDataString = GetCustomData(relativePath); | |
204 | |
205 fs::path absolutePath = absoluteRootPath_ / relativePath; | |
206 | |
207 if (fs::exists(absolutePath)) | |
208 { | |
209 // Extremely unlikely case if uuid is included in the path: This Uuid has already been created | |
210 // in the past. | |
211 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
212 | |
213 // TODO for the future: handle duplicates path (e.g: there's no uuid in the path and we are uploading the same file again) | |
214 // OrthancPlugins::LogWarning(std::string("Overwriting file \"") + path.string() + "\" (" + uuid + ")"); | |
215 } | |
216 | |
217 if (fs::exists(absolutePath.parent_path())) | |
218 { | |
219 if (!fs::is_directory(absolutePath.parent_path())) | |
220 { | |
221 throw Orthanc::OrthancException(Orthanc::ErrorCode_DirectoryOverFile); | |
222 } | |
223 } | |
224 else | |
225 { | |
226 if (!fs::create_directories(absolutePath.parent_path())) | |
227 { | |
228 throw Orthanc::OrthancException(Orthanc::ErrorCode_FileStorageCannotWrite); | |
229 } | |
230 } | |
231 | |
232 Orthanc::SystemToolbox::WriteFile(content, size, absolutePath.string(), fsyncOnWrite_); | |
233 | |
234 OrthancPluginCreateMemoryBuffer(OrthancPlugins::GetGlobalContext(), customData, customDataString.size()); | |
235 memcpy(customData->data, customDataString.data(), customDataString.size()); | |
236 | |
237 return OrthancPluginErrorCode_Success; | |
238 | |
239 } | |
240 | |
241 OrthancPluginErrorCode StorageCreateInstance(OrthancPluginMemoryBuffer* customData, | |
242 const char* uuid, | |
243 const OrthancPluginDicomInstance* instance, | |
244 const void* content, | |
245 int64_t size, | |
246 OrthancPluginContentType type, | |
247 bool isCompressed) | |
248 { | |
249 try | |
250 { | |
251 OrthancPlugins::LogInfo(std::string("Creating instance attachment \"") + uuid + "\""); | |
252 | |
253 OrthancPlugins::DicomInstance dicomInstance(instance); | |
254 Json::Value tags; | |
255 dicomInstance.GetSimplifiedJson(tags); | |
256 | |
257 return StorageCreate(customData, uuid, tags, content, size, type, isCompressed); | |
258 } | |
259 catch (Orthanc::OrthancException& e) | |
260 { | |
261 return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); | |
262 } | |
263 catch (...) | |
264 { | |
265 return OrthancPluginErrorCode_StorageAreaPlugin; | |
266 } | |
267 | |
268 return OrthancPluginErrorCode_Success; | |
269 } | |
270 | |
271 | |
272 OrthancPluginErrorCode StorageCreateAttachment(OrthancPluginMemoryBuffer* customData, | |
273 const char* uuid, | |
274 const char* resourceId, | |
275 OrthancPluginResourceType resourceType, | |
276 const void* content, | |
277 int64_t size, | |
278 OrthancPluginContentType type, | |
279 bool isCompressed) | |
280 { | |
281 try | |
282 { | |
283 OrthancPlugins::LogInfo(std::string("Creating attachment \"") + uuid + "\""); | |
284 | |
285 //TODO_CUSTOM_DATA: get tags from the Rest API... | |
286 Json::Value tags; | |
287 | |
288 return StorageCreate(customData, uuid, tags, content, size, type, isCompressed); | |
289 } | |
290 catch (Orthanc::OrthancException& e) | |
291 { | |
292 return static_cast<OrthancPluginErrorCode>(e.GetErrorCode()); | |
293 } | |
294 catch (...) | |
295 { | |
296 return OrthancPluginErrorCode_StorageAreaPlugin; | |
297 } | |
298 | |
299 return OrthancPluginErrorCode_Success; | |
300 } | |
301 | |
302 OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target, | |
303 const char* uuid, | |
304 const char* customData, | |
305 OrthancPluginContentType type) | |
306 { | |
307 OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\""); | |
308 | |
309 std::string path = GetAbsolutePath(uuid, customData).string(); | |
310 | |
311 if (!Orthanc::SystemToolbox::IsRegularFile(path)) | |
312 { | |
313 OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path); | |
314 return OrthancPluginErrorCode_InexistentFile; | |
315 } | |
316 | |
317 try | |
318 { | |
319 fs::ifstream f; | |
320 f.open(path, std::ifstream::in | std::ifstream::binary); | |
321 if (!f.good()) | |
322 { | |
323 OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path); | |
324 return OrthancPluginErrorCode_InexistentFile; | |
325 } | |
326 | |
327 // get file size | |
328 f.seekg(0, std::ios::end); | |
329 std::streamsize fileSize = f.tellg(); | |
330 f.seekg(0, std::ios::beg); | |
331 | |
332 // The ReadWhole must allocate the buffer itself | |
333 if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, fileSize) != OrthancPluginErrorCode_Success) | |
334 { | |
335 OrthancPlugins::LogError(std::string("Unable to allocate memory to read file: ") + path); | |
336 return OrthancPluginErrorCode_NotEnoughMemory; | |
337 } | |
338 | |
339 if (fileSize != 0) | |
340 { | |
341 f.read(reinterpret_cast<char*>(target->data), fileSize); | |
342 } | |
343 | |
344 f.close(); | |
345 } | |
346 catch (...) | |
347 { | |
348 OrthancPlugins::LogError(std::string("Unexpected error while reading: ") + path); | |
349 return OrthancPluginErrorCode_StorageAreaPlugin; | |
350 } | |
351 | |
352 return OrthancPluginErrorCode_Success; | |
353 } | |
354 | |
355 | |
356 OrthancPluginErrorCode StorageReadRange (OrthancPluginMemoryBuffer64* target, | |
357 const char* uuid, | |
358 const char* customData, | |
359 OrthancPluginContentType type, | |
360 uint64_t rangeStart) | |
361 { | |
362 OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\""); | |
363 | |
364 std::string path = GetAbsolutePath(uuid, customData).string(); | |
365 | |
366 if (!Orthanc::SystemToolbox::IsRegularFile(path)) | |
367 { | |
368 OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path); | |
369 return OrthancPluginErrorCode_InexistentFile; | |
370 } | |
371 | |
372 try | |
373 { | |
374 fs::ifstream f; | |
375 f.open(path, std::ifstream::in | std::ifstream::binary); | |
376 if (!f.good()) | |
377 { | |
378 OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path); | |
379 return OrthancPluginErrorCode_InexistentFile; | |
380 } | |
381 | |
382 f.seekg(rangeStart, std::ios::beg); | |
383 | |
384 // The ReadRange uses a target that has already been allocated by orthanc | |
385 f.read(reinterpret_cast<char*>(target->data), target->size); | |
386 | |
387 f.close(); | |
388 } | |
389 catch (...) | |
390 { | |
391 OrthancPlugins::LogError(std::string("Unexpected error while reading: ") + path); | |
392 return OrthancPluginErrorCode_StorageAreaPlugin; | |
393 } | |
394 | |
395 return OrthancPluginErrorCode_Success; | |
396 } | |
397 | |
398 | |
399 OrthancPluginErrorCode StorageRemove (const char* uuid, | |
400 const char* customData, | |
401 OrthancPluginContentType type) | |
402 { | |
403 // LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type); | |
404 | |
405 fs::path p = GetAbsolutePath(uuid, customData); | |
406 | |
407 try | |
408 { | |
409 fs::remove(p); | |
410 } | |
411 catch (...) | |
412 { | |
413 // Ignore the error | |
414 } | |
415 | |
416 // Remove the empty parent directories, (ignoring the error code if these directories are not empty) | |
417 | |
418 try | |
419 { | |
420 fs::path parent = p.parent_path(); | |
421 | |
422 while (parent != absoluteRootPath_) | |
423 { | |
424 fs::remove(parent); | |
425 parent = parent.parent_path(); | |
426 } | |
427 } | |
428 catch (...) | |
429 { | |
430 // Ignore the error | |
431 } | |
432 | |
433 return OrthancPluginErrorCode_Success; | |
434 } | |
435 | |
436 extern "C" | |
437 { | |
438 | |
439 ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) | |
440 { | |
441 OrthancPlugins::SetGlobalContext(context); | |
442 Orthanc::Logging::InitializePluginContext(context); | |
443 | |
444 /* Check the version of the Orthanc core */ | |
445 if (OrthancPluginCheckVersion(context) == 0) | |
446 { | |
447 OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, | |
448 ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, | |
449 ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); | |
450 return -1; | |
451 } | |
452 | |
453 OrthancPlugins::LogWarning("AdvancedStorage plugin is initializing"); | |
454 OrthancPluginSetDescription(context, "Provides alternative layout for your storage."); | |
455 | |
456 OrthancPlugins::OrthancConfiguration orthancConfiguration; | |
457 | |
458 OrthancPlugins::OrthancConfiguration advancedStorage; | |
459 orthancConfiguration.GetSection(advancedStorage, "AdvancedStorage"); | |
460 | |
461 bool enabled = advancedStorage.GetBooleanValue("Enable", false); | |
462 if (enabled) | |
463 { | |
464 /* | |
465 { | |
466 "AdvancedStorage": { | |
467 | |
468 // Enables/disables the plugin | |
469 "Enable": false, | |
470 } | |
471 } | |
472 */ | |
473 | |
474 absoluteRootPath_ = fs::absolute(fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage"))); | |
475 LOG(WARNING) << "AdvancedStorage - Path to the storage area: " << absoluteRootPath_.string(); | |
476 | |
477 OrthancPluginRegisterStorageArea3(context, StorageCreateInstance, StorageCreateAttachment, StorageReadWhole, StorageReadRange, StorageRemove); | |
478 } | |
479 else | |
480 { | |
481 OrthancPlugins::LogWarning("AdvancedStorage plugin is disabled by the configuration file"); | |
482 } | |
483 | |
484 return 0; | |
485 } | |
486 | |
487 | |
488 ORTHANC_PLUGINS_API void OrthancPluginFinalize() | |
489 { | |
490 OrthancPlugins::LogWarning("AdvancedStorage plugin is finalizing"); | |
491 } | |
492 | |
493 | |
494 ORTHANC_PLUGINS_API const char* OrthancPluginGetName() | |
495 { | |
496 return "advanced-storage"; | |
497 } | |
498 | |
499 | |
500 ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() | |
501 { | |
502 return ORTHANC_PLUGIN_VERSION; | |
503 } | |
504 } |