2632
|
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-2018 Osimis S.A., Belgium
|
|
6 *
|
|
7 * This program is free software: you can redistribute it and/or
|
|
8 * modify it under the terms of the GNU General Public License as
|
|
9 * published by the Free Software Foundation, either version 3 of the
|
|
10 * License, or (at your option) any later version.
|
|
11 *
|
|
12 * In addition, as a special exception, the copyright holders of this
|
|
13 * program give permission to link the code of its release with the
|
|
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it
|
|
15 * that use the same license as the "OpenSSL" library), and distribute
|
|
16 * the linked executables. You must obey the GNU General Public License
|
|
17 * in all respects for all of the code used other than "OpenSSL". If you
|
|
18 * modify file(s) with this exception, you may extend this exception to
|
|
19 * your version of the file(s), but you are not obligated to do so. If
|
|
20 * you do not wish to do so, delete this exception statement from your
|
|
21 * version. If you delete this exception statement from all source files
|
|
22 * in the program, then also delete it here.
|
|
23 *
|
|
24 * This program is distributed in the hope that it will be useful, but
|
|
25 * WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
27 * General Public License for more details.
|
|
28 *
|
|
29 * You should have received a copy of the GNU General Public License
|
|
30 * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
31 **/
|
|
32
|
|
33
|
|
34 #include "../PrecompiledHeadersServer.h"
|
|
35 #include "ArchiveJob.h"
|
|
36
|
|
37 #include "../../Core/Compression/HierarchicalZipWriter.h"
|
|
38 #include "../../Core/DicomParsing/DicomDirWriter.h"
|
|
39 #include "../../Core/Logging.h"
|
|
40 #include "../../Core/OrthancException.h"
|
|
41
|
|
42 #include <stdio.h>
|
|
43
|
|
44 #if defined(_MSC_VER)
|
|
45 #define snprintf _snprintf
|
|
46 #endif
|
|
47
|
|
48 static const uint64_t MEGA_BYTES = 1024 * 1024;
|
|
49 static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024;
|
|
50 static const char* MEDIA_IMAGES_FOLDER = "IMAGES";
|
|
51
|
|
52 namespace Orthanc
|
|
53 {
|
|
54 static bool IsZip64Required(uint64_t uncompressedSize,
|
|
55 unsigned int countInstances)
|
|
56 {
|
|
57 static const uint64_t SAFETY_MARGIN = 64 * MEGA_BYTES; // Should be large enough to hold DICOMDIR
|
|
58 static const unsigned int FILES_MARGIN = 10;
|
|
59
|
|
60 /**
|
|
61 * Determine whether ZIP64 is required. Original ZIP format can
|
|
62 * store up to 2GB of data (some implementation supporting up to
|
|
63 * 4GB of data), and up to 65535 files.
|
|
64 * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64
|
|
65 **/
|
|
66
|
|
67 const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES - SAFETY_MARGIN ||
|
|
68 countInstances >= 65535 - FILES_MARGIN);
|
|
69
|
|
70 LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size "
|
|
71 << (uncompressedSize / MEGA_BYTES) << "MB using the "
|
|
72 << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
|
|
73
|
|
74 return isZip64;
|
|
75 }
|
|
76
|
|
77
|
|
78 class ArchiveJob::ResourceIdentifiers : public boost::noncopyable
|
|
79 {
|
|
80 private:
|
|
81 ResourceType level_;
|
|
82 std::string patient_;
|
|
83 std::string study_;
|
|
84 std::string series_;
|
|
85 std::string instance_;
|
|
86
|
|
87 static void GoToParent(ServerIndex& index,
|
|
88 std::string& current)
|
|
89 {
|
|
90 std::string tmp;
|
|
91
|
|
92 if (index.LookupParent(tmp, current))
|
|
93 {
|
|
94 current = tmp;
|
|
95 }
|
|
96 else
|
|
97 {
|
|
98 throw OrthancException(ErrorCode_UnknownResource);
|
|
99 }
|
|
100 }
|
|
101
|
|
102
|
|
103 public:
|
|
104 ResourceIdentifiers(ServerIndex& index,
|
|
105 const std::string& publicId)
|
|
106 {
|
|
107 if (!index.LookupResourceType(level_, publicId))
|
|
108 {
|
|
109 throw OrthancException(ErrorCode_UnknownResource);
|
|
110 }
|
|
111
|
|
112 std::string current = publicId;;
|
|
113 switch (level_) // Do not add "break" below!
|
|
114 {
|
|
115 case ResourceType_Instance:
|
|
116 instance_ = current;
|
|
117 GoToParent(index, current);
|
|
118
|
|
119 case ResourceType_Series:
|
|
120 series_ = current;
|
|
121 GoToParent(index, current);
|
|
122
|
|
123 case ResourceType_Study:
|
|
124 study_ = current;
|
|
125 GoToParent(index, current);
|
|
126
|
|
127 case ResourceType_Patient:
|
|
128 patient_ = current;
|
|
129 break;
|
|
130
|
|
131 default:
|
|
132 throw OrthancException(ErrorCode_InternalError);
|
|
133 }
|
|
134 }
|
|
135
|
|
136 ResourceType GetLevel() const
|
|
137 {
|
|
138 return level_;
|
|
139 }
|
|
140
|
|
141 const std::string& GetIdentifier(ResourceType level) const
|
|
142 {
|
|
143 // Some sanity check to ensure enumerations are not altered
|
|
144 assert(ResourceType_Patient < ResourceType_Study);
|
|
145 assert(ResourceType_Study < ResourceType_Series);
|
|
146 assert(ResourceType_Series < ResourceType_Instance);
|
|
147
|
|
148 if (level > level_)
|
|
149 {
|
|
150 throw OrthancException(ErrorCode_InternalError);
|
|
151 }
|
|
152
|
|
153 switch (level)
|
|
154 {
|
|
155 case ResourceType_Patient:
|
|
156 return patient_;
|
|
157
|
|
158 case ResourceType_Study:
|
|
159 return study_;
|
|
160
|
|
161 case ResourceType_Series:
|
|
162 return series_;
|
|
163
|
|
164 case ResourceType_Instance:
|
|
165 return instance_;
|
|
166
|
|
167 default:
|
|
168 throw OrthancException(ErrorCode_InternalError);
|
|
169 }
|
|
170 }
|
|
171 };
|
|
172
|
|
173
|
|
174 class ArchiveJob::IArchiveVisitor : public boost::noncopyable
|
|
175 {
|
|
176 public:
|
|
177 virtual ~IArchiveVisitor()
|
|
178 {
|
|
179 }
|
|
180
|
|
181 virtual void Open(ResourceType level,
|
|
182 const std::string& publicId) = 0;
|
|
183
|
|
184 virtual void Close() = 0;
|
|
185
|
|
186 virtual void AddInstance(const std::string& instanceId,
|
|
187 const FileInfo& dicom) = 0;
|
|
188 };
|
|
189
|
|
190
|
|
191 class ArchiveJob::ArchiveIndex : public boost::noncopyable
|
|
192 {
|
|
193 private:
|
|
194 struct Instance
|
|
195 {
|
|
196 std::string id_;
|
|
197 FileInfo dicom_;
|
|
198
|
|
199 Instance(const std::string& id,
|
|
200 const FileInfo& dicom) :
|
|
201 id_(id), dicom_(dicom)
|
|
202 {
|
|
203 }
|
|
204 };
|
|
205
|
|
206 // A "NULL" value for ArchiveIndex indicates a non-expanded node
|
|
207 typedef std::map<std::string, ArchiveIndex*> Resources;
|
|
208
|
|
209 ResourceType level_;
|
|
210 Resources resources_; // Only at patient/study/series level
|
|
211 std::list<Instance> instances_; // Only at instance level
|
|
212
|
|
213
|
|
214 void AddResourceToExpand(ServerIndex& index,
|
|
215 const std::string& id)
|
|
216 {
|
|
217 if (level_ == ResourceType_Instance)
|
|
218 {
|
|
219 FileInfo tmp;
|
|
220 if (index.LookupAttachment(tmp, id, FileContentType_Dicom))
|
|
221 {
|
|
222 instances_.push_back(Instance(id, tmp));
|
|
223 }
|
|
224 }
|
|
225 else
|
|
226 {
|
|
227 resources_[id] = NULL;
|
|
228 }
|
|
229 }
|
|
230
|
|
231
|
|
232 public:
|
|
233 ArchiveIndex(ResourceType level) :
|
|
234 level_(level)
|
|
235 {
|
|
236 }
|
|
237
|
|
238 ~ArchiveIndex()
|
|
239 {
|
|
240 for (Resources::iterator it = resources_.begin();
|
|
241 it != resources_.end(); ++it)
|
|
242 {
|
|
243 delete it->second;
|
|
244 }
|
|
245 }
|
|
246
|
|
247
|
|
248 void Add(ServerIndex& index,
|
|
249 const ResourceIdentifiers& resource)
|
|
250 {
|
|
251 const std::string& id = resource.GetIdentifier(level_);
|
|
252 Resources::iterator previous = resources_.find(id);
|
|
253
|
|
254 if (level_ == ResourceType_Instance)
|
|
255 {
|
|
256 AddResourceToExpand(index, id);
|
|
257 }
|
|
258 else if (resource.GetLevel() == level_)
|
|
259 {
|
|
260 // Mark this resource for further expansion
|
|
261 if (previous != resources_.end())
|
|
262 {
|
|
263 delete previous->second;
|
|
264 }
|
|
265
|
|
266 resources_[id] = NULL;
|
|
267 }
|
|
268 else if (previous == resources_.end())
|
|
269 {
|
|
270 // This is the first time we meet this resource
|
|
271 std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
|
|
272 child->Add(index, resource);
|
|
273 resources_[id] = child.release();
|
|
274 }
|
|
275 else if (previous->second != NULL)
|
|
276 {
|
|
277 previous->second->Add(index, resource);
|
|
278 }
|
|
279 else
|
|
280 {
|
|
281 // Nothing to do: This item is marked for further expansion
|
|
282 }
|
|
283 }
|
|
284
|
|
285
|
|
286 void Expand(ServerIndex& index)
|
|
287 {
|
|
288 if (level_ == ResourceType_Instance)
|
|
289 {
|
|
290 // Expanding an instance node makes no sense
|
|
291 return;
|
|
292 }
|
|
293
|
|
294 for (Resources::iterator it = resources_.begin();
|
|
295 it != resources_.end(); ++it)
|
|
296 {
|
|
297 if (it->second == NULL)
|
|
298 {
|
|
299 // This is resource is marked for expansion
|
|
300 std::list<std::string> children;
|
|
301 index.GetChildren(children, it->first);
|
|
302
|
|
303 std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
|
|
304
|
|
305 for (std::list<std::string>::const_iterator
|
|
306 it2 = children.begin(); it2 != children.end(); ++it2)
|
|
307 {
|
|
308 child->AddResourceToExpand(index, *it2);
|
|
309 }
|
|
310
|
|
311 it->second = child.release();
|
|
312 }
|
|
313
|
|
314 assert(it->second != NULL);
|
|
315 it->second->Expand(index);
|
|
316 }
|
|
317 }
|
|
318
|
|
319
|
|
320 void Apply(IArchiveVisitor& visitor) const
|
|
321 {
|
|
322 if (level_ == ResourceType_Instance)
|
|
323 {
|
|
324 for (std::list<Instance>::const_iterator
|
|
325 it = instances_.begin(); it != instances_.end(); ++it)
|
|
326 {
|
|
327 visitor.AddInstance(it->id_, it->dicom_);
|
|
328 }
|
|
329 }
|
|
330 else
|
|
331 {
|
|
332 for (Resources::const_iterator it = resources_.begin();
|
|
333 it != resources_.end(); ++it)
|
|
334 {
|
|
335 assert(it->second != NULL); // There must have been a call to "Expand()"
|
|
336 visitor.Open(level_, it->first);
|
|
337 it->second->Apply(visitor);
|
|
338 visitor.Close();
|
|
339 }
|
|
340 }
|
|
341 }
|
|
342 };
|
|
343
|
|
344
|
|
345
|
|
346 class ArchiveJob::ZipCommands : public boost::noncopyable
|
|
347 {
|
|
348 private:
|
|
349 enum Type
|
|
350 {
|
|
351 Type_OpenDirectory,
|
|
352 Type_CloseDirectory,
|
|
353 Type_WriteInstance
|
|
354 };
|
|
355
|
|
356 class Command : public boost::noncopyable
|
|
357 {
|
|
358 private:
|
|
359 Type type_;
|
|
360 std::string filename_;
|
|
361 std::string instanceId_;
|
|
362 FileInfo info_;
|
|
363
|
|
364 public:
|
|
365 explicit Command(Type type) :
|
|
366 type_(type)
|
|
367 {
|
|
368 assert(type_ == Type_CloseDirectory);
|
|
369 }
|
|
370
|
|
371 Command(Type type,
|
|
372 const std::string& filename) :
|
|
373 type_(type),
|
|
374 filename_(filename)
|
|
375 {
|
|
376 assert(type_ == Type_OpenDirectory);
|
|
377 }
|
|
378
|
|
379 Command(Type type,
|
|
380 const std::string& filename,
|
|
381 const std::string& instanceId,
|
|
382 const FileInfo& info) :
|
|
383 type_(type),
|
|
384 filename_(filename),
|
|
385 instanceId_(instanceId),
|
|
386 info_(info)
|
|
387 {
|
|
388 assert(type_ == Type_WriteInstance);
|
|
389 }
|
|
390
|
|
391 void Apply(HierarchicalZipWriter& writer,
|
|
392 ServerContext& context,
|
|
393 DicomDirWriter* dicomDir,
|
|
394 const std::string& dicomDirFolder) const
|
|
395 {
|
|
396 switch (type_)
|
|
397 {
|
|
398 case Type_OpenDirectory:
|
|
399 writer.OpenDirectory(filename_.c_str());
|
|
400 break;
|
|
401
|
|
402 case Type_CloseDirectory:
|
|
403 writer.CloseDirectory();
|
|
404 break;
|
|
405
|
|
406 case Type_WriteInstance:
|
|
407 {
|
|
408 std::string content;
|
|
409
|
|
410 try
|
|
411 {
|
|
412 context.ReadAttachment(content, info_);
|
|
413 }
|
|
414 catch (OrthancException& e)
|
|
415 {
|
|
416 LOG(WARNING) << "An instance was removed after the job was issued: " << instanceId_;
|
|
417 return;
|
|
418 }
|
2636
|
419
|
|
420 //boost::this_thread::sleep(boost::posix_time::milliseconds(300));
|
2632
|
421
|
|
422 writer.OpenFile(filename_.c_str());
|
|
423 writer.Write(content);
|
|
424
|
|
425 if (dicomDir != NULL)
|
|
426 {
|
|
427 ParsedDicomFile parsed(content);
|
|
428 dicomDir->Add(dicomDirFolder, filename_, parsed);
|
|
429 }
|
|
430
|
|
431 break;
|
|
432 }
|
|
433
|
|
434 default:
|
|
435 throw OrthancException(ErrorCode_InternalError);
|
|
436 }
|
|
437 }
|
|
438 };
|
|
439
|
|
440 std::deque<Command*> commands_;
|
|
441 uint64_t uncompressedSize_;
|
|
442 unsigned int instancesCount_;
|
|
443
|
|
444
|
|
445 void ApplyInternal(HierarchicalZipWriter& writer,
|
|
446 ServerContext& context,
|
|
447 size_t index,
|
|
448 DicomDirWriter* dicomDir,
|
|
449 const std::string& dicomDirFolder) const
|
|
450 {
|
|
451 if (index >= commands_.size())
|
|
452 {
|
|
453 throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
454 }
|
|
455
|
|
456 commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder);
|
|
457 }
|
|
458
|
|
459 public:
|
|
460 ZipCommands() :
|
|
461 uncompressedSize_(0),
|
|
462 instancesCount_(0)
|
|
463 {
|
|
464 }
|
|
465
|
|
466 ~ZipCommands()
|
|
467 {
|
|
468 for (std::deque<Command*>::iterator it = commands_.begin();
|
|
469 it != commands_.end(); ++it)
|
|
470 {
|
|
471 assert(*it != NULL);
|
|
472 delete *it;
|
|
473 }
|
|
474 }
|
|
475
|
|
476 size_t GetSize() const
|
|
477 {
|
|
478 return commands_.size();
|
|
479 }
|
|
480
|
|
481 unsigned int GetInstancesCount() const
|
|
482 {
|
|
483 return instancesCount_;
|
|
484 }
|
|
485
|
|
486 uint64_t GetUncompressedSize() const
|
|
487 {
|
|
488 return uncompressedSize_;
|
|
489 }
|
|
490
|
|
491 void Apply(HierarchicalZipWriter& writer,
|
|
492 ServerContext& context,
|
|
493 size_t index,
|
|
494 DicomDirWriter& dicomDir,
|
|
495 const std::string& dicomDirFolder) const
|
|
496 {
|
|
497 ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder);
|
|
498 }
|
|
499
|
|
500 void Apply(HierarchicalZipWriter& writer,
|
|
501 ServerContext& context,
|
|
502 size_t index) const
|
|
503 {
|
|
504 ApplyInternal(writer, context, index, NULL, "");
|
|
505 }
|
|
506
|
|
507 void AddOpenDirectory(const std::string& filename)
|
|
508 {
|
|
509 commands_.push_back(new Command(Type_OpenDirectory, filename));
|
|
510 }
|
|
511
|
|
512 void AddCloseDirectory()
|
|
513 {
|
|
514 commands_.push_back(new Command(Type_CloseDirectory));
|
|
515 }
|
|
516
|
|
517 void AddWriteInstance(const std::string& filename,
|
|
518 const std::string& instanceId,
|
|
519 const FileInfo& info)
|
|
520 {
|
|
521 commands_.push_back(new Command(Type_WriteInstance, filename, instanceId, info));
|
|
522 instancesCount_ ++;
|
|
523 uncompressedSize_ += info.GetUncompressedSize();
|
|
524 }
|
|
525
|
|
526 bool IsZip64() const
|
|
527 {
|
|
528 return IsZip64Required(GetUncompressedSize(), GetInstancesCount());
|
|
529 }
|
|
530 };
|
|
531
|
|
532
|
|
533
|
|
534 class ArchiveJob::ArchiveIndexVisitor : public IArchiveVisitor
|
|
535 {
|
|
536 private:
|
|
537 ZipCommands& commands_;
|
|
538 ServerContext& context_;
|
|
539 char instanceFormat_[24];
|
|
540 unsigned int counter_;
|
|
541
|
|
542 static std::string GetTag(const DicomMap& tags,
|
|
543 const DicomTag& tag)
|
|
544 {
|
|
545 const DicomValue* v = tags.TestAndGetValue(tag);
|
|
546 if (v != NULL &&
|
|
547 !v->IsBinary() &&
|
|
548 !v->IsNull())
|
|
549 {
|
|
550 return v->GetContent();
|
|
551 }
|
|
552 else
|
|
553 {
|
|
554 return "";
|
|
555 }
|
|
556 }
|
|
557
|
|
558 public:
|
|
559 ArchiveIndexVisitor(ZipCommands& commands,
|
|
560 ServerContext& context) :
|
|
561 commands_(commands),
|
|
562 context_(context),
|
|
563 counter_(0)
|
|
564 {
|
|
565 if (commands.GetSize() != 0)
|
|
566 {
|
|
567 throw OrthancException(ErrorCode_BadSequenceOfCalls);
|
|
568 }
|
|
569
|
|
570 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
|
|
571 }
|
|
572
|
|
573 virtual void Open(ResourceType level,
|
|
574 const std::string& publicId)
|
|
575 {
|
|
576 std::string path;
|
|
577
|
|
578 DicomMap tags;
|
|
579 if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level))
|
|
580 {
|
|
581 switch (level)
|
|
582 {
|
|
583 case ResourceType_Patient:
|
|
584 path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME);
|
|
585 break;
|
|
586
|
|
587 case ResourceType_Study:
|
|
588 path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION);
|
|
589 break;
|
|
590
|
|
591 case ResourceType_Series:
|
|
592 {
|
|
593 std::string modality = GetTag(tags, DICOM_TAG_MODALITY);
|
|
594 path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION);
|
|
595
|
|
596 if (modality.size() == 0)
|
|
597 {
|
|
598 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
|
|
599 }
|
|
600 else if (modality.size() == 1)
|
|
601 {
|
|
602 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm",
|
|
603 toupper(modality[0]));
|
|
604 }
|
|
605 else if (modality.size() >= 2)
|
|
606 {
|
|
607 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm",
|
|
608 toupper(modality[0]), toupper(modality[1]));
|
|
609 }
|
|
610
|
|
611 counter_ = 0;
|
|
612
|
|
613 break;
|
|
614 }
|
|
615
|
|
616 default:
|
|
617 throw OrthancException(ErrorCode_InternalError);
|
|
618 }
|
|
619 }
|
|
620
|
|
621 path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path));
|
|
622
|
|
623 if (path.empty())
|
|
624 {
|
|
625 path = std::string("Unknown ") + EnumerationToString(level);
|
|
626 }
|
|
627
|
|
628 commands_.AddOpenDirectory(path.c_str());
|
|
629 }
|
|
630
|
|
631 virtual void Close()
|
|
632 {
|
|
633 commands_.AddCloseDirectory();
|
|
634 }
|
|
635
|
|
636 virtual void AddInstance(const std::string& instanceId,
|
|
637 const FileInfo& dicom)
|
|
638 {
|
|
639 char filename[24];
|
|
640 snprintf(filename, sizeof(filename) - 1, instanceFormat_, counter_);
|
|
641 counter_ ++;
|
|
642
|
|
643 commands_.AddWriteInstance(filename, instanceId, dicom);
|
|
644 }
|
|
645 };
|
|
646
|
|
647
|
|
648 class ArchiveJob::MediaIndexVisitor : public IArchiveVisitor
|
|
649 {
|
|
650 private:
|
|
651 ZipCommands& commands_;
|
|
652 ServerContext& context_;
|
|
653 unsigned int counter_;
|
|
654
|
|
655 public:
|
|
656 MediaIndexVisitor(ZipCommands& commands,
|
|
657 ServerContext& context) :
|
|
658 commands_(commands),
|
|
659 context_(context),
|
|
660 counter_(0)
|
|
661 {
|
|
662 }
|
|
663
|
|
664 virtual void Open(ResourceType level,
|
|
665 const std::string& publicId)
|
|
666 {
|
|
667 }
|
|
668
|
|
669 virtual void Close()
|
|
670 {
|
|
671 }
|
|
672
|
|
673 virtual void AddInstance(const std::string& instanceId,
|
|
674 const FileInfo& dicom)
|
|
675 {
|
|
676 // "DICOM restricts the filenames on DICOM media to 8
|
|
677 // characters (some systems wrongly use 8.3, but this does not
|
|
678 // conform to the standard)."
|
|
679 std::string filename = "IM" + boost::lexical_cast<std::string>(counter_);
|
|
680 commands_.AddWriteInstance(filename, instanceId, dicom);
|
|
681
|
|
682 counter_ ++;
|
|
683 }
|
|
684 };
|
|
685
|
|
686
|
|
687 class ArchiveJob::ZipWriterIterator : public boost::noncopyable
|
|
688 {
|
|
689 private:
|
|
690 TemporaryFile& target_;
|
|
691 ServerContext& context_;
|
|
692 ZipCommands commands_;
|
|
693 std::auto_ptr<HierarchicalZipWriter> zip_;
|
|
694 std::auto_ptr<DicomDirWriter> dicomDir_;
|
|
695 bool isMedia_;
|
|
696
|
|
697 public:
|
|
698 ZipWriterIterator(TemporaryFile& target,
|
|
699 ServerContext& context,
|
|
700 ArchiveIndex& archive,
|
|
701 bool isMedia,
|
|
702 bool enableExtendedSopClass) :
|
|
703 target_(target),
|
|
704 context_(context),
|
|
705 isMedia_(isMedia)
|
|
706 {
|
|
707 if (isMedia)
|
|
708 {
|
|
709 MediaIndexVisitor visitor(commands_, context);
|
|
710 archive.Expand(context.GetIndex());
|
|
711
|
|
712 commands_.AddOpenDirectory(MEDIA_IMAGES_FOLDER);
|
|
713 archive.Apply(visitor);
|
|
714 commands_.AddCloseDirectory();
|
|
715
|
|
716 dicomDir_.reset(new DicomDirWriter);
|
|
717 dicomDir_->EnableExtendedSopClass(enableExtendedSopClass);
|
|
718 }
|
|
719 else
|
|
720 {
|
|
721 ArchiveIndexVisitor visitor(commands_, context);
|
|
722 archive.Expand(context.GetIndex());
|
|
723 archive.Apply(visitor);
|
|
724 }
|
|
725
|
|
726 zip_.reset(new HierarchicalZipWriter(target.GetPath().c_str()));
|
|
727 zip_->SetZip64(commands_.IsZip64());
|
|
728 }
|
|
729
|
|
730 size_t GetStepsCount() const
|
|
731 {
|
|
732 return commands_.GetSize() + 1;
|
|
733 }
|
|
734
|
|
735 void RunStep(size_t index)
|
|
736 {
|
|
737 if (index > commands_.GetSize())
|
|
738 {
|
|
739 throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
740 }
|
|
741 else if (index == commands_.GetSize())
|
|
742 {
|
|
743 // Last step: Add the DICOMDIR
|
|
744 if (isMedia_)
|
|
745 {
|
|
746 assert(dicomDir_.get() != NULL);
|
|
747 std::string s;
|
|
748 dicomDir_->Encode(s);
|
|
749
|
|
750 zip_->OpenFile("DICOMDIR");
|
|
751 zip_->Write(s);
|
|
752 }
|
|
753 }
|
|
754 else
|
|
755 {
|
|
756 if (isMedia_)
|
|
757 {
|
|
758 assert(dicomDir_.get() != NULL);
|
|
759 commands_.Apply(*zip_, context_, index, *dicomDir_, MEDIA_IMAGES_FOLDER);
|
|
760 }
|
|
761 else
|
|
762 {
|
|
763 assert(dicomDir_.get() == NULL);
|
|
764 commands_.Apply(*zip_, context_, index);
|
|
765 }
|
|
766 }
|
|
767 }
|
|
768
|
|
769 unsigned int GetInstancesCount() const
|
|
770 {
|
|
771 return commands_.GetInstancesCount();
|
|
772 }
|
|
773
|
|
774 uint64_t GetUncompressedSize() const
|
|
775 {
|
|
776 return commands_.GetUncompressedSize();
|
|
777 }
|
|
778 };
|
|
779
|
|
780
|
|
781 ArchiveJob::ArchiveJob(boost::shared_ptr<TemporaryFile>& target,
|
|
782 ServerContext& context,
|
|
783 bool isMedia,
|
|
784 bool enableExtendedSopClass) :
|
|
785 target_(target),
|
|
786 context_(context),
|
|
787 archive_(new ArchiveIndex(ResourceType_Patient)), // root
|
|
788 isMedia_(isMedia),
|
|
789 enableExtendedSopClass_(enableExtendedSopClass),
|
|
790 currentStep_(0),
|
|
791 instancesCount_(0),
|
|
792 uncompressedSize_(0)
|
|
793 {
|
|
794 if (target.get() == NULL)
|
|
795 {
|
|
796 throw OrthancException(ErrorCode_NullPointer);
|
|
797 }
|
|
798 }
|
|
799
|
|
800
|
|
801 void ArchiveJob::AddResource(const std::string& publicId)
|
|
802 {
|
|
803 if (writer_.get() != NULL) // Already started
|
|
804 {
|
|
805 throw OrthancException(ErrorCode_BadSequenceOfCalls);
|
|
806 }
|
|
807
|
|
808 ResourceIdentifiers resource(context_.GetIndex(), publicId);
|
|
809 archive_->Add(context_.GetIndex(), resource);
|
|
810 }
|
|
811
|
|
812
|
|
813 void ArchiveJob::SignalResubmit()
|
|
814 {
|
|
815 LOG(ERROR) << "Cannot resubmit the creation of an archive";
|
|
816 throw OrthancException(ErrorCode_BadSequenceOfCalls);
|
|
817 }
|
|
818
|
|
819
|
|
820 void ArchiveJob::Start()
|
|
821 {
|
|
822 if (writer_.get() != NULL)
|
|
823 {
|
|
824 throw OrthancException(ErrorCode_BadSequenceOfCalls);
|
|
825 }
|
|
826
|
|
827 writer_.reset(new ZipWriterIterator(*target_, context_, *archive_,
|
|
828 isMedia_, enableExtendedSopClass_));
|
|
829
|
|
830 instancesCount_ = writer_->GetInstancesCount();
|
|
831 uncompressedSize_ = writer_->GetUncompressedSize();
|
|
832 }
|
|
833
|
|
834
|
|
835 JobStepResult ArchiveJob::ExecuteStep()
|
|
836 {
|
|
837 assert(writer_.get() != NULL);
|
|
838
|
|
839 if (target_.unique())
|
|
840 {
|
|
841 LOG(WARNING) << "A client has disconnected while creating an archive";
|
|
842 return JobStepResult::Failure(ErrorCode_NetworkProtocol);
|
|
843 }
|
|
844
|
|
845 if (writer_->GetStepsCount() == 0)
|
|
846 {
|
|
847 writer_.reset(NULL); // Flush all the results
|
|
848 return JobStepResult::Success();
|
|
849 }
|
|
850 else
|
|
851 {
|
|
852 writer_->RunStep(currentStep_);
|
|
853
|
|
854 currentStep_ ++;
|
|
855
|
|
856 if (currentStep_ == writer_->GetStepsCount())
|
|
857 {
|
|
858 writer_.reset(NULL); // Flush all the results
|
|
859 return JobStepResult::Success();
|
|
860 }
|
|
861 else
|
|
862 {
|
|
863 return JobStepResult::Continue();
|
|
864 }
|
|
865 }
|
|
866 }
|
|
867
|
|
868
|
|
869 float ArchiveJob::GetProgress()
|
|
870 {
|
|
871 if (writer_.get() == NULL ||
|
|
872 writer_->GetStepsCount() == 0)
|
|
873 {
|
|
874 return 1;
|
|
875 }
|
|
876 else
|
|
877 {
|
|
878 return (static_cast<float>(currentStep_) /
|
|
879 static_cast<float>(writer_->GetStepsCount() - 1));
|
|
880 }
|
|
881 }
|
|
882
|
|
883
|
|
884 void ArchiveJob::GetJobType(std::string& target)
|
|
885 {
|
|
886 if (isMedia_)
|
|
887 {
|
|
888 target = "Media";
|
|
889 }
|
|
890 else
|
|
891 {
|
|
892 target = "Archive";
|
|
893 }
|
|
894 }
|
|
895
|
|
896
|
|
897 void ArchiveJob::GetPublicContent(Json::Value& value)
|
|
898 {
|
|
899 value["Description"] = description_;
|
|
900 value["InstancesCount"] = instancesCount_;
|
|
901 value["UncompressedSizeMB"] =
|
2643
|
902 static_cast<unsigned int>(uncompressedSize_ / MEGA_BYTES);
|
2632
|
903 }
|
|
904 }
|