comparison OrthancServer/Sources/ServerJobs/ArchiveJob.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents OrthancServer/ServerJobs/ArchiveJob.cpp@5fe8c6d3212e
children 05b8fd21089c
comparison
equal deleted inserted replaced
4043:6c6239aec462 4044:d25f4c0fa160
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-2020 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/Cache/SharedArchive.h"
38 #include "../../Core/Compression/HierarchicalZipWriter.h"
39 #include "../../Core/DicomParsing/DicomDirWriter.h"
40 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
41 #include "../../Core/Logging.h"
42 #include "../../Core/OrthancException.h"
43 #include "../OrthancConfiguration.h"
44 #include "../ServerContext.h"
45
46 #include <stdio.h>
47
48 #if defined(_MSC_VER)
49 #define snprintf _snprintf
50 #endif
51
52 static const uint64_t MEGA_BYTES = 1024 * 1024;
53 static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024;
54
55 static const char* const MEDIA_IMAGES_FOLDER = "IMAGES";
56 static const char* const KEY_DESCRIPTION = "Description";
57 static const char* const KEY_INSTANCES_COUNT = "InstancesCount";
58 static const char* const KEY_UNCOMPRESSED_SIZE_MB = "UncompressedSizeMB";
59 static const char* const KEY_TRANSCODE = "Transcode";
60
61
62 namespace Orthanc
63 {
64 static bool IsZip64Required(uint64_t uncompressedSize,
65 unsigned int countInstances)
66 {
67 static const uint64_t SAFETY_MARGIN = 64 * MEGA_BYTES; // Should be large enough to hold DICOMDIR
68 static const unsigned int FILES_MARGIN = 10;
69
70 /**
71 * Determine whether ZIP64 is required. Original ZIP format can
72 * store up to 2GB of data (some implementation supporting up to
73 * 4GB of data), and up to 65535 files.
74 * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64
75 **/
76
77 const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES - SAFETY_MARGIN ||
78 countInstances >= 65535 - FILES_MARGIN);
79
80 LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size "
81 << (uncompressedSize / MEGA_BYTES) << "MB using the "
82 << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
83
84 return isZip64;
85 }
86
87
88 class ArchiveJob::ResourceIdentifiers : public boost::noncopyable
89 {
90 private:
91 ResourceType level_;
92 std::string patient_;
93 std::string study_;
94 std::string series_;
95 std::string instance_;
96
97 static void GoToParent(ServerIndex& index,
98 std::string& current)
99 {
100 std::string tmp;
101
102 if (index.LookupParent(tmp, current))
103 {
104 current = tmp;
105 }
106 else
107 {
108 throw OrthancException(ErrorCode_UnknownResource);
109 }
110 }
111
112
113 public:
114 ResourceIdentifiers(ServerIndex& index,
115 const std::string& publicId)
116 {
117 if (!index.LookupResourceType(level_, publicId))
118 {
119 throw OrthancException(ErrorCode_UnknownResource);
120 }
121
122 std::string current = publicId;;
123 switch (level_) // Do not add "break" below!
124 {
125 case ResourceType_Instance:
126 instance_ = current;
127 GoToParent(index, current);
128
129 case ResourceType_Series:
130 series_ = current;
131 GoToParent(index, current);
132
133 case ResourceType_Study:
134 study_ = current;
135 GoToParent(index, current);
136
137 case ResourceType_Patient:
138 patient_ = current;
139 break;
140
141 default:
142 throw OrthancException(ErrorCode_InternalError);
143 }
144 }
145
146 ResourceType GetLevel() const
147 {
148 return level_;
149 }
150
151 const std::string& GetIdentifier(ResourceType level) const
152 {
153 // Some sanity check to ensure enumerations are not altered
154 assert(ResourceType_Patient < ResourceType_Study);
155 assert(ResourceType_Study < ResourceType_Series);
156 assert(ResourceType_Series < ResourceType_Instance);
157
158 if (level > level_)
159 {
160 throw OrthancException(ErrorCode_InternalError);
161 }
162
163 switch (level)
164 {
165 case ResourceType_Patient:
166 return patient_;
167
168 case ResourceType_Study:
169 return study_;
170
171 case ResourceType_Series:
172 return series_;
173
174 case ResourceType_Instance:
175 return instance_;
176
177 default:
178 throw OrthancException(ErrorCode_InternalError);
179 }
180 }
181 };
182
183
184 class ArchiveJob::IArchiveVisitor : public boost::noncopyable
185 {
186 public:
187 virtual ~IArchiveVisitor()
188 {
189 }
190
191 virtual void Open(ResourceType level,
192 const std::string& publicId) = 0;
193
194 virtual void Close() = 0;
195
196 virtual void AddInstance(const std::string& instanceId,
197 const FileInfo& dicom) = 0;
198 };
199
200
201 class ArchiveJob::ArchiveIndex : public boost::noncopyable
202 {
203 private:
204 struct Instance
205 {
206 std::string id_;
207 FileInfo dicom_;
208
209 Instance(const std::string& id,
210 const FileInfo& dicom) :
211 id_(id), dicom_(dicom)
212 {
213 }
214 };
215
216 // A "NULL" value for ArchiveIndex indicates a non-expanded node
217 typedef std::map<std::string, ArchiveIndex*> Resources;
218
219 ResourceType level_;
220 Resources resources_; // Only at patient/study/series level
221 std::list<Instance> instances_; // Only at instance level
222
223
224 void AddResourceToExpand(ServerIndex& index,
225 const std::string& id)
226 {
227 if (level_ == ResourceType_Instance)
228 {
229 FileInfo tmp;
230 if (index.LookupAttachment(tmp, id, FileContentType_Dicom))
231 {
232 instances_.push_back(Instance(id, tmp));
233 }
234 }
235 else
236 {
237 resources_[id] = NULL;
238 }
239 }
240
241
242 public:
243 ArchiveIndex(ResourceType level) :
244 level_(level)
245 {
246 }
247
248 ~ArchiveIndex()
249 {
250 for (Resources::iterator it = resources_.begin();
251 it != resources_.end(); ++it)
252 {
253 delete it->second;
254 }
255 }
256
257
258 void Add(ServerIndex& index,
259 const ResourceIdentifiers& resource)
260 {
261 const std::string& id = resource.GetIdentifier(level_);
262 Resources::iterator previous = resources_.find(id);
263
264 if (level_ == ResourceType_Instance)
265 {
266 AddResourceToExpand(index, id);
267 }
268 else if (resource.GetLevel() == level_)
269 {
270 // Mark this resource for further expansion
271 if (previous != resources_.end())
272 {
273 delete previous->second;
274 }
275
276 resources_[id] = NULL;
277 }
278 else if (previous == resources_.end())
279 {
280 // This is the first time we meet this resource
281 std::unique_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
282 child->Add(index, resource);
283 resources_[id] = child.release();
284 }
285 else if (previous->second != NULL)
286 {
287 previous->second->Add(index, resource);
288 }
289 else
290 {
291 // Nothing to do: This item is marked for further expansion
292 }
293 }
294
295
296 void Expand(ServerIndex& index)
297 {
298 if (level_ == ResourceType_Instance)
299 {
300 // Expanding an instance node makes no sense
301 return;
302 }
303
304 for (Resources::iterator it = resources_.begin();
305 it != resources_.end(); ++it)
306 {
307 if (it->second == NULL)
308 {
309 // This is resource is marked for expansion
310 std::list<std::string> children;
311 index.GetChildren(children, it->first);
312
313 std::unique_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
314
315 for (std::list<std::string>::const_iterator
316 it2 = children.begin(); it2 != children.end(); ++it2)
317 {
318 child->AddResourceToExpand(index, *it2);
319 }
320
321 it->second = child.release();
322 }
323
324 assert(it->second != NULL);
325 it->second->Expand(index);
326 }
327 }
328
329
330 void Apply(IArchiveVisitor& visitor) const
331 {
332 if (level_ == ResourceType_Instance)
333 {
334 for (std::list<Instance>::const_iterator
335 it = instances_.begin(); it != instances_.end(); ++it)
336 {
337 visitor.AddInstance(it->id_, it->dicom_);
338 }
339 }
340 else
341 {
342 for (Resources::const_iterator it = resources_.begin();
343 it != resources_.end(); ++it)
344 {
345 assert(it->second != NULL); // There must have been a call to "Expand()"
346 visitor.Open(level_, it->first);
347 it->second->Apply(visitor);
348 visitor.Close();
349 }
350 }
351 }
352 };
353
354
355
356 class ArchiveJob::ZipCommands : public boost::noncopyable
357 {
358 private:
359 enum Type
360 {
361 Type_OpenDirectory,
362 Type_CloseDirectory,
363 Type_WriteInstance
364 };
365
366 class Command : public boost::noncopyable
367 {
368 private:
369 Type type_;
370 std::string filename_;
371 std::string instanceId_;
372 FileInfo info_;
373
374 public:
375 explicit Command(Type type) :
376 type_(type)
377 {
378 assert(type_ == Type_CloseDirectory);
379 }
380
381 Command(Type type,
382 const std::string& filename) :
383 type_(type),
384 filename_(filename)
385 {
386 assert(type_ == Type_OpenDirectory);
387 }
388
389 Command(Type type,
390 const std::string& filename,
391 const std::string& instanceId,
392 const FileInfo& info) :
393 type_(type),
394 filename_(filename),
395 instanceId_(instanceId),
396 info_(info)
397 {
398 assert(type_ == Type_WriteInstance);
399 }
400
401 void Apply(HierarchicalZipWriter& writer,
402 ServerContext& context,
403 DicomDirWriter* dicomDir,
404 const std::string& dicomDirFolder,
405 bool transcode,
406 DicomTransferSyntax transferSyntax) const
407 {
408 switch (type_)
409 {
410 case Type_OpenDirectory:
411 writer.OpenDirectory(filename_.c_str());
412 break;
413
414 case Type_CloseDirectory:
415 writer.CloseDirectory();
416 break;
417
418 case Type_WriteInstance:
419 {
420 std::string content;
421
422 try
423 {
424 context.ReadAttachment(content, info_);
425 }
426 catch (OrthancException& e)
427 {
428 LOG(WARNING) << "An instance was removed after the job was issued: " << instanceId_;
429 return;
430 }
431
432 //boost::this_thread::sleep(boost::posix_time::milliseconds(300));
433
434 writer.OpenFile(filename_.c_str());
435
436 bool transcodeSuccess = false;
437
438 std::unique_ptr<ParsedDicomFile> parsed;
439
440 if (transcode)
441 {
442 // New in Orthanc 1.7.0
443 std::set<DicomTransferSyntax> syntaxes;
444 syntaxes.insert(transferSyntax);
445
446 IDicomTranscoder::DicomImage source, transcoded;
447 source.SetExternalBuffer(content);
448
449 if (context.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
450 {
451 writer.Write(transcoded.GetBufferData(), transcoded.GetBufferSize());
452
453 if (dicomDir != NULL)
454 {
455 std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
456 dicomDir->Add(dicomDirFolder, filename_, *tmp);
457 }
458
459 transcodeSuccess = true;
460 }
461 else
462 {
463 LOG(INFO) << "Cannot transcode instance " << instanceId_
464 << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax);
465 }
466 }
467
468 if (!transcodeSuccess)
469 {
470 writer.Write(content);
471
472 if (dicomDir != NULL)
473 {
474 if (parsed.get() == NULL)
475 {
476 parsed.reset(new ParsedDicomFile(content));
477 }
478
479 dicomDir->Add(dicomDirFolder, filename_, *parsed);
480 }
481 }
482
483 break;
484 }
485
486 default:
487 throw OrthancException(ErrorCode_InternalError);
488 }
489 }
490 };
491
492 std::deque<Command*> commands_;
493 uint64_t uncompressedSize_;
494 unsigned int instancesCount_;
495
496
497 void ApplyInternal(HierarchicalZipWriter& writer,
498 ServerContext& context,
499 size_t index,
500 DicomDirWriter* dicomDir,
501 const std::string& dicomDirFolder,
502 bool transcode,
503 DicomTransferSyntax transferSyntax) const
504 {
505 if (index >= commands_.size())
506 {
507 throw OrthancException(ErrorCode_ParameterOutOfRange);
508 }
509
510 commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder, transcode, transferSyntax);
511 }
512
513 public:
514 ZipCommands() :
515 uncompressedSize_(0),
516 instancesCount_(0)
517 {
518 }
519
520 ~ZipCommands()
521 {
522 for (std::deque<Command*>::iterator it = commands_.begin();
523 it != commands_.end(); ++it)
524 {
525 assert(*it != NULL);
526 delete *it;
527 }
528 }
529
530 size_t GetSize() const
531 {
532 return commands_.size();
533 }
534
535 unsigned int GetInstancesCount() const
536 {
537 return instancesCount_;
538 }
539
540 uint64_t GetUncompressedSize() const
541 {
542 return uncompressedSize_;
543 }
544
545 // "media" flavor (with DICOMDIR)
546 void Apply(HierarchicalZipWriter& writer,
547 ServerContext& context,
548 size_t index,
549 DicomDirWriter& dicomDir,
550 const std::string& dicomDirFolder,
551 bool transcode,
552 DicomTransferSyntax transferSyntax) const
553 {
554 ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder, transcode, transferSyntax);
555 }
556
557 // "archive" flavor (without DICOMDIR)
558 void Apply(HierarchicalZipWriter& writer,
559 ServerContext& context,
560 size_t index,
561 bool transcode,
562 DicomTransferSyntax transferSyntax) const
563 {
564 ApplyInternal(writer, context, index, NULL, "", transcode, transferSyntax);
565 }
566
567 void AddOpenDirectory(const std::string& filename)
568 {
569 commands_.push_back(new Command(Type_OpenDirectory, filename));
570 }
571
572 void AddCloseDirectory()
573 {
574 commands_.push_back(new Command(Type_CloseDirectory));
575 }
576
577 void AddWriteInstance(const std::string& filename,
578 const std::string& instanceId,
579 const FileInfo& info)
580 {
581 commands_.push_back(new Command(Type_WriteInstance, filename, instanceId, info));
582 instancesCount_ ++;
583 uncompressedSize_ += info.GetUncompressedSize();
584 }
585
586 bool IsZip64() const
587 {
588 return IsZip64Required(GetUncompressedSize(), GetInstancesCount());
589 }
590 };
591
592
593
594 class ArchiveJob::ArchiveIndexVisitor : public IArchiveVisitor
595 {
596 private:
597 ZipCommands& commands_;
598 ServerContext& context_;
599 char instanceFormat_[24];
600 unsigned int counter_;
601
602 static std::string GetTag(const DicomMap& tags,
603 const DicomTag& tag)
604 {
605 const DicomValue* v = tags.TestAndGetValue(tag);
606 if (v != NULL &&
607 !v->IsBinary() &&
608 !v->IsNull())
609 {
610 return v->GetContent();
611 }
612 else
613 {
614 return "";
615 }
616 }
617
618 public:
619 ArchiveIndexVisitor(ZipCommands& commands,
620 ServerContext& context) :
621 commands_(commands),
622 context_(context),
623 counter_(0)
624 {
625 if (commands.GetSize() != 0)
626 {
627 throw OrthancException(ErrorCode_BadSequenceOfCalls);
628 }
629
630 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
631 }
632
633 virtual void Open(ResourceType level,
634 const std::string& publicId)
635 {
636 std::string path;
637
638 DicomMap tags;
639 if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level))
640 {
641 switch (level)
642 {
643 case ResourceType_Patient:
644 path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME);
645 break;
646
647 case ResourceType_Study:
648 path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION);
649 break;
650
651 case ResourceType_Series:
652 {
653 std::string modality = GetTag(tags, DICOM_TAG_MODALITY);
654 path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION);
655
656 if (modality.size() == 0)
657 {
658 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
659 }
660 else if (modality.size() == 1)
661 {
662 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm",
663 toupper(modality[0]));
664 }
665 else if (modality.size() >= 2)
666 {
667 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm",
668 toupper(modality[0]), toupper(modality[1]));
669 }
670
671 counter_ = 0;
672
673 break;
674 }
675
676 default:
677 throw OrthancException(ErrorCode_InternalError);
678 }
679 }
680
681 path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path));
682
683 if (path.empty())
684 {
685 path = std::string("Unknown ") + EnumerationToString(level);
686 }
687
688 commands_.AddOpenDirectory(path.c_str());
689 }
690
691 virtual void Close()
692 {
693 commands_.AddCloseDirectory();
694 }
695
696 virtual void AddInstance(const std::string& instanceId,
697 const FileInfo& dicom)
698 {
699 char filename[24];
700 snprintf(filename, sizeof(filename) - 1, instanceFormat_, counter_);
701 counter_ ++;
702
703 commands_.AddWriteInstance(filename, instanceId, dicom);
704 }
705 };
706
707
708 class ArchiveJob::MediaIndexVisitor : public IArchiveVisitor
709 {
710 private:
711 ZipCommands& commands_;
712 ServerContext& context_;
713 unsigned int counter_;
714
715 public:
716 MediaIndexVisitor(ZipCommands& commands,
717 ServerContext& context) :
718 commands_(commands),
719 context_(context),
720 counter_(0)
721 {
722 }
723
724 virtual void Open(ResourceType level,
725 const std::string& publicId)
726 {
727 }
728
729 virtual void Close()
730 {
731 }
732
733 virtual void AddInstance(const std::string& instanceId,
734 const FileInfo& dicom)
735 {
736 // "DICOM restricts the filenames on DICOM media to 8
737 // characters (some systems wrongly use 8.3, but this does not
738 // conform to the standard)."
739 std::string filename = "IM" + boost::lexical_cast<std::string>(counter_);
740 commands_.AddWriteInstance(filename, instanceId, dicom);
741
742 counter_ ++;
743 }
744 };
745
746
747 class ArchiveJob::ZipWriterIterator : public boost::noncopyable
748 {
749 private:
750 TemporaryFile& target_;
751 ServerContext& context_;
752 ZipCommands commands_;
753 std::unique_ptr<HierarchicalZipWriter> zip_;
754 std::unique_ptr<DicomDirWriter> dicomDir_;
755 bool isMedia_;
756
757 public:
758 ZipWriterIterator(TemporaryFile& target,
759 ServerContext& context,
760 ArchiveIndex& archive,
761 bool isMedia,
762 bool enableExtendedSopClass) :
763 target_(target),
764 context_(context),
765 isMedia_(isMedia)
766 {
767 if (isMedia)
768 {
769 MediaIndexVisitor visitor(commands_, context);
770 archive.Expand(context.GetIndex());
771
772 commands_.AddOpenDirectory(MEDIA_IMAGES_FOLDER);
773 archive.Apply(visitor);
774 commands_.AddCloseDirectory();
775
776 dicomDir_.reset(new DicomDirWriter);
777 dicomDir_->EnableExtendedSopClass(enableExtendedSopClass);
778 }
779 else
780 {
781 ArchiveIndexVisitor visitor(commands_, context);
782 archive.Expand(context.GetIndex());
783 archive.Apply(visitor);
784 }
785
786 zip_.reset(new HierarchicalZipWriter(target.GetPath().c_str()));
787 zip_->SetZip64(commands_.IsZip64());
788 }
789
790 size_t GetStepsCount() const
791 {
792 return commands_.GetSize() + 1;
793 }
794
795 void RunStep(size_t index,
796 bool transcode,
797 DicomTransferSyntax transferSyntax)
798 {
799 if (index > commands_.GetSize())
800 {
801 throw OrthancException(ErrorCode_ParameterOutOfRange);
802 }
803 else if (index == commands_.GetSize())
804 {
805 // Last step: Add the DICOMDIR
806 if (isMedia_)
807 {
808 assert(dicomDir_.get() != NULL);
809 std::string s;
810 dicomDir_->Encode(s);
811
812 zip_->OpenFile("DICOMDIR");
813 zip_->Write(s);
814 }
815 }
816 else
817 {
818 if (isMedia_)
819 {
820 assert(dicomDir_.get() != NULL);
821 commands_.Apply(*zip_, context_, index, *dicomDir_,
822 MEDIA_IMAGES_FOLDER, transcode, transferSyntax);
823 }
824 else
825 {
826 assert(dicomDir_.get() == NULL);
827 commands_.Apply(*zip_, context_, index, transcode, transferSyntax);
828 }
829 }
830 }
831
832 unsigned int GetInstancesCount() const
833 {
834 return commands_.GetInstancesCount();
835 }
836
837 uint64_t GetUncompressedSize() const
838 {
839 return commands_.GetUncompressedSize();
840 }
841 };
842
843
844 ArchiveJob::ArchiveJob(ServerContext& context,
845 bool isMedia,
846 bool enableExtendedSopClass) :
847 context_(context),
848 archive_(new ArchiveIndex(ResourceType_Patient)), // root
849 isMedia_(isMedia),
850 enableExtendedSopClass_(enableExtendedSopClass),
851 currentStep_(0),
852 instancesCount_(0),
853 uncompressedSize_(0),
854 transcode_(false),
855 transferSyntax_(DicomTransferSyntax_LittleEndianImplicit)
856 {
857 }
858
859
860 ArchiveJob::~ArchiveJob()
861 {
862 if (!mediaArchiveId_.empty())
863 {
864 context_.GetMediaArchive().Remove(mediaArchiveId_);
865 }
866 }
867
868
869 void ArchiveJob::SetSynchronousTarget(boost::shared_ptr<TemporaryFile>& target)
870 {
871 if (target.get() == NULL)
872 {
873 throw OrthancException(ErrorCode_NullPointer);
874 }
875 else if (writer_.get() != NULL || // Already started
876 synchronousTarget_.get() != NULL ||
877 asynchronousTarget_.get() != NULL)
878 {
879 throw OrthancException(ErrorCode_BadSequenceOfCalls);
880 }
881 else
882 {
883 synchronousTarget_ = target;
884 }
885 }
886
887
888 void ArchiveJob::SetDescription(const std::string& description)
889 {
890 if (writer_.get() != NULL) // Already started
891 {
892 throw OrthancException(ErrorCode_BadSequenceOfCalls);
893 }
894 else
895 {
896 description_ = description;
897 }
898 }
899
900
901 void ArchiveJob::AddResource(const std::string& publicId)
902 {
903 if (writer_.get() != NULL) // Already started
904 {
905 throw OrthancException(ErrorCode_BadSequenceOfCalls);
906 }
907 else
908 {
909 ResourceIdentifiers resource(context_.GetIndex(), publicId);
910 archive_->Add(context_.GetIndex(), resource);
911 }
912 }
913
914
915 void ArchiveJob::SetTranscode(DicomTransferSyntax transferSyntax)
916 {
917 if (writer_.get() != NULL) // Already started
918 {
919 throw OrthancException(ErrorCode_BadSequenceOfCalls);
920 }
921 else
922 {
923 transcode_ = true;
924 transferSyntax_ = transferSyntax;
925 }
926 }
927
928
929 void ArchiveJob::Reset()
930 {
931 throw OrthancException(ErrorCode_BadSequenceOfCalls,
932 "Cannot resubmit the creation of an archive");
933 }
934
935
936 void ArchiveJob::Start()
937 {
938 TemporaryFile* target = NULL;
939
940 if (synchronousTarget_.get() == NULL)
941 {
942 {
943 OrthancConfiguration::ReaderLock lock;
944 asynchronousTarget_.reset(lock.GetConfiguration().CreateTemporaryFile());
945 }
946
947 target = asynchronousTarget_.get();
948 }
949 else
950 {
951 target = synchronousTarget_.get();
952 }
953
954 assert(target != NULL);
955 target->Touch(); // Make sure we can write to the temporary file
956
957 if (writer_.get() != NULL)
958 {
959 throw OrthancException(ErrorCode_BadSequenceOfCalls);
960 }
961
962 writer_.reset(new ZipWriterIterator(*target, context_, *archive_,
963 isMedia_, enableExtendedSopClass_));
964
965 instancesCount_ = writer_->GetInstancesCount();
966 uncompressedSize_ = writer_->GetUncompressedSize();
967 }
968
969
970
971 namespace
972 {
973 class DynamicTemporaryFile : public IDynamicObject
974 {
975 private:
976 std::unique_ptr<TemporaryFile> file_;
977
978 public:
979 DynamicTemporaryFile(TemporaryFile* f) : file_(f)
980 {
981 if (f == NULL)
982 {
983 throw OrthancException(ErrorCode_NullPointer);
984 }
985 }
986
987 const TemporaryFile& GetFile() const
988 {
989 assert(file_.get() != NULL);
990 return *file_;
991 }
992 };
993 }
994
995
996 void ArchiveJob::FinalizeTarget()
997 {
998 writer_.reset(); // Flush all the results
999
1000 if (asynchronousTarget_.get() != NULL)
1001 {
1002 // Asynchronous behavior: Move the resulting file into the media archive
1003 mediaArchiveId_ = context_.GetMediaArchive().Add(
1004 new DynamicTemporaryFile(asynchronousTarget_.release()));
1005 }
1006 }
1007
1008
1009 JobStepResult ArchiveJob::Step(const std::string& jobId)
1010 {
1011 assert(writer_.get() != NULL);
1012
1013 if (synchronousTarget_.get() != NULL &&
1014 synchronousTarget_.unique())
1015 {
1016 LOG(WARNING) << "A client has disconnected while creating an archive";
1017 return JobStepResult::Failure(ErrorCode_NetworkProtocol,
1018 "A client has disconnected while creating an archive");
1019 }
1020
1021 if (writer_->GetStepsCount() == 0)
1022 {
1023 FinalizeTarget();
1024 return JobStepResult::Success();
1025 }
1026 else
1027 {
1028 writer_->RunStep(currentStep_, transcode_, transferSyntax_);
1029
1030 currentStep_ ++;
1031
1032 if (currentStep_ == writer_->GetStepsCount())
1033 {
1034 FinalizeTarget();
1035 return JobStepResult::Success();
1036 }
1037 else
1038 {
1039 return JobStepResult::Continue();
1040 }
1041 }
1042 }
1043
1044
1045 float ArchiveJob::GetProgress()
1046 {
1047 if (writer_.get() == NULL ||
1048 writer_->GetStepsCount() == 0)
1049 {
1050 return 1;
1051 }
1052 else
1053 {
1054 return (static_cast<float>(currentStep_) /
1055 static_cast<float>(writer_->GetStepsCount() - 1));
1056 }
1057 }
1058
1059
1060 void ArchiveJob::GetJobType(std::string& target)
1061 {
1062 if (isMedia_)
1063 {
1064 target = "Media";
1065 }
1066 else
1067 {
1068 target = "Archive";
1069 }
1070 }
1071
1072
1073 void ArchiveJob::GetPublicContent(Json::Value& value)
1074 {
1075 value = Json::objectValue;
1076 value[KEY_DESCRIPTION] = description_;
1077 value[KEY_INSTANCES_COUNT] = instancesCount_;
1078 value[KEY_UNCOMPRESSED_SIZE_MB] =
1079 static_cast<unsigned int>(uncompressedSize_ / MEGA_BYTES);
1080
1081 if (transcode_)
1082 {
1083 value[KEY_TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
1084 }
1085 }
1086
1087
1088 bool ArchiveJob::GetOutput(std::string& output,
1089 MimeType& mime,
1090 const std::string& key)
1091 {
1092 if (key == "archive" &&
1093 !mediaArchiveId_.empty())
1094 {
1095 SharedArchive::Accessor accessor(context_.GetMediaArchive(), mediaArchiveId_);
1096
1097 if (accessor.IsValid())
1098 {
1099 const DynamicTemporaryFile& f = dynamic_cast<DynamicTemporaryFile&>(accessor.GetItem());
1100 f.GetFile().Read(output);
1101 mime = MimeType_Zip;
1102 return true;
1103 }
1104 else
1105 {
1106 return false;
1107 }
1108 }
1109 else
1110 {
1111 return false;
1112 }
1113 }
1114 }