1 /*
2  *
3  *  Copyright (C) 2013-2017, OFFIS e.V.
4  *  All rights reserved.  See COPYRIGHT file for details.
5  *
6  *  This software and supporting documentation were developed by
7  *
8  *    OFFIS e.V.
9  *    R&D Division Health
10  *    Escherweg 2
11  *    D-26121 Oldenburg, Germany
12  *
13  *
14  *  Module:  dcmnet
15  *
16  *  Author:  Joerg Riesmeier
17  *
18  *  Purpose: DICOM Storage Service Class Provider (SCP)
19  *
20  */
21 
22 
23 #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
24 
25 #include "dcmtk/dcmnet/dstorscp.h"
26 #include "dcmtk/dcmnet/diutil.h"
27 
28 
29 // constant definitions
30 
31 const char *DcmStorageSCP::DEF_StandardSubdirectory  = "data";
32 const char *DcmStorageSCP::DEF_UndefinedSubdirectory = "undef";
33 const char *DcmStorageSCP::DEF_FilenameExtension     = "";
34 
35 
36 // implementation of the main interface class
37 
DcmStorageSCP()38 DcmStorageSCP::DcmStorageSCP()
39   : DcmSCP(),
40     OutputDirectory(),
41     StandardSubdirectory(DEF_StandardSubdirectory),
42     UndefinedSubdirectory(DEF_UndefinedSubdirectory),
43     FilenameExtension(DEF_FilenameExtension),
44     DirectoryGeneration(DGM_Default),
45     FilenameGeneration(FGM_Default),
46     FilenameCreator(),
47     DatasetStorage(DSM_Default)
48 {
49     // make sure that the SCP at least supports C-ECHO with default transfer syntax
50     OFList<OFString> transferSyntaxes;
51     transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
52     addPresentationContext(UID_VerificationSOPClass, transferSyntaxes);
53 }
54 
55 
~DcmStorageSCP()56 DcmStorageSCP::~DcmStorageSCP()
57 {
58     // clear internal state
59     clear();
60 }
61 
62 
clear()63 void DcmStorageSCP::clear()
64 {
65     // DcmSCP::clear();
66     OutputDirectory.clear();
67     StandardSubdirectory = DEF_StandardSubdirectory;
68     UndefinedSubdirectory = DEF_UndefinedSubdirectory;
69     FilenameExtension = DEF_FilenameExtension;
70     DirectoryGeneration = DGM_Default;
71     FilenameGeneration = FGM_Default;
72     DatasetStorage = DSM_Default;
73 }
74 
75 
76 // get methods
77 
getOutputDirectory() const78 const OFString &DcmStorageSCP::getOutputDirectory() const
79 {
80     return OutputDirectory;
81 }
82 
83 
getDirectoryGenerationMode() const84 DcmStorageSCP::E_DirectoryGenerationMode DcmStorageSCP::getDirectoryGenerationMode() const
85 {
86     return DirectoryGeneration;
87 }
88 
89 
getFilenameGenerationMode() const90 DcmStorageSCP::E_FilenameGenerationMode DcmStorageSCP::getFilenameGenerationMode() const
91 {
92     return FilenameGeneration;
93 }
94 
95 
getFilenameExtension() const96 const OFString &DcmStorageSCP::getFilenameExtension() const
97 {
98     return FilenameExtension;
99 }
100 
101 
getDatasetStorageMode() const102 DcmStorageSCP::E_DatasetStorageMode DcmStorageSCP::getDatasetStorageMode() const
103 {
104     return DatasetStorage;
105 }
106 
107 
108 // set methods
109 
setOutputDirectory(const OFString & directory)110 OFCondition DcmStorageSCP::setOutputDirectory(const OFString &directory)
111 {
112     OFCondition status = EC_Normal;
113     if (directory.empty())
114     {
115         // empty directory refers to the current directory
116         if (OFStandard::isWriteable("."))
117             OutputDirectory.clear();
118         else
119             status = EC_DirectoryNotWritable;
120     } else {
121         // check whether given directory exists and is writable
122         if (OFStandard::dirExists(directory))
123         {
124             if (OFStandard::isWriteable(directory))
125                 OFStandard::normalizeDirName(OutputDirectory, directory);
126             else
127                 status = EC_DirectoryNotWritable;
128         } else
129             status = EC_DirectoryDoesNotExist;
130     }
131     return status;
132 }
133 
134 
setDirectoryGenerationMode(const E_DirectoryGenerationMode mode)135 void DcmStorageSCP::setDirectoryGenerationMode(const E_DirectoryGenerationMode mode)
136 {
137     DirectoryGeneration = mode;
138 }
139 
140 
setFilenameGenerationMode(const E_FilenameGenerationMode mode)141 void DcmStorageSCP::setFilenameGenerationMode(const E_FilenameGenerationMode mode)
142 {
143     FilenameGeneration = mode;
144 }
145 
146 
setFilenameExtension(const OFString & extension)147 void DcmStorageSCP::setFilenameExtension(const OFString &extension)
148 {
149     FilenameExtension = extension;
150 }
151 
152 
setDatasetStorageMode(const E_DatasetStorageMode mode)153 void DcmStorageSCP::setDatasetStorageMode(const E_DatasetStorageMode mode)
154 {
155     DatasetStorage = mode;
156 }
157 
158 
159 // further public methods
160 
loadAssociationConfiguration(const OFString & filename,const OFString & profile)161 OFCondition DcmStorageSCP::loadAssociationConfiguration(const OFString &filename,
162                                                         const OFString &profile)
163 {
164     // first, try to load the configuration file
165     OFCondition status = loadAssociationCfgFile(filename);
166     // and then, try to select the desired profile
167     if (status.good())
168         status = setAndCheckAssociationProfile(profile);
169     return status;
170 }
171 
172 
173 // protected methods
174 
handleIncomingCommand(T_DIMSE_Message * incomingMsg,const DcmPresentationContextInfo & presInfo)175 OFCondition DcmStorageSCP::handleIncomingCommand(T_DIMSE_Message *incomingMsg,
176                                                  const DcmPresentationContextInfo &presInfo)
177 {
178     OFCondition status = EC_IllegalParameter;
179     if (incomingMsg != NULL)
180     {
181         // check whether we've received a supported command
182         if (incomingMsg->CommandField == DIMSE_C_ECHO_RQ)
183         {
184             // handle incoming C-ECHO request
185             status = handleECHORequest(incomingMsg->msg.CEchoRQ, presInfo.presentationContextID);
186         }
187         else if (incomingMsg->CommandField == DIMSE_C_STORE_RQ)
188         {
189             // handle incoming C-STORE request
190             T_DIMSE_C_StoreRQ &storeReq = incomingMsg->msg.CStoreRQ;
191             Uint16 rspStatusCode = STATUS_STORE_Error_CannotUnderstand;
192             // special case: bit preserving mode
193             if (DatasetStorage == DGM_StoreBitPreserving)
194             {
195                 OFString filename;
196                 // generate filename with full path (and create subdirectories if needed)
197                 status = generateSTORERequestFilename(storeReq, filename);
198                 if (status.good())
199                 {
200                     if (OFStandard::fileExists(filename))
201                         DCMNET_WARN("file already exists, overwriting: " << filename);
202                     // receive dataset directly to file
203                     status = receiveSTORERequest(storeReq, presInfo.presentationContextID, filename);
204                     if (status.good())
205                     {
206                         // call the notification handler (default implementation outputs to the logger)
207                         notifyInstanceStored(filename, storeReq.AffectedSOPClassUID, storeReq.AffectedSOPInstanceUID);
208                         rspStatusCode = STATUS_Success;
209                     }
210                 }
211             } else {
212                 DcmFileFormat fileformat;
213                 DcmDataset *reqDataset = fileformat.getDataset();
214                 // receive dataset in memory
215                 status = receiveSTORERequest(storeReq, presInfo.presentationContextID, reqDataset);
216                 if (status.good())
217                 {
218                     // do we need to store the received dataset at all?
219                     if (DatasetStorage == DSM_Ignore)
220                     {
221                         // output debug message that dataset is not stored
222                         DCMNET_DEBUG("received dataset is not stored since the storage mode is set to 'ignore'");
223                         rspStatusCode = STATUS_Success;
224                     } else {
225                         // check and process C-STORE request
226                         rspStatusCode = checkAndProcessSTORERequest(storeReq, fileformat);
227                     }
228                 }
229             }
230             // send C-STORE response (with DIMSE status code)
231             if (status.good())
232                 status = sendSTOREResponse(presInfo.presentationContextID, storeReq, rspStatusCode);
233             else if (status == DIMSE_OUTOFRESOURCES)
234             {
235                 // do not overwrite the previous error status
236                 sendSTOREResponse(presInfo.presentationContextID, storeReq, STATUS_STORE_Refused_OutOfResources);
237             }
238         } else {
239             // unsupported command
240             OFString tempStr;
241             DCMNET_ERROR("cannot handle this kind of DIMSE command (0x"
242                 << STD_NAMESPACE hex << STD_NAMESPACE setfill('0') << STD_NAMESPACE setw(4)
243                 << OFstatic_cast(unsigned int, incomingMsg->CommandField)
244                 << "), we are a Storage SCP only");
245             DCMNET_DEBUG(DIMSE_dumpMessage(tempStr, *incomingMsg, DIMSE_INCOMING));
246             // TODO: provide more information on this error?
247             status = DIMSE_BADCOMMANDTYPE;
248         }
249     }
250     return status;
251 }
252 
253 
checkAndProcessSTORERequest(const T_DIMSE_C_StoreRQ & reqMessage,DcmFileFormat & fileformat)254 Uint16 DcmStorageSCP::checkAndProcessSTORERequest(const T_DIMSE_C_StoreRQ &reqMessage,
255                                                   DcmFileFormat &fileformat)
256 {
257     DCMNET_DEBUG("checking and processing C-STORE request");
258     Uint16 statusCode = STATUS_STORE_Error_CannotUnderstand;
259     DcmDataset *dataset = fileformat.getDataset();
260     // perform some basic checks on the request dataset
261     if ((dataset != NULL) && !dataset->isEmpty())
262     {
263         OFString filename;
264         OFString directoryName;
265         OFString sopClassUID = reqMessage.AffectedSOPClassUID;
266         OFString sopInstanceUID = reqMessage.AffectedSOPInstanceUID;
267         // generate filename with full path
268         OFCondition status = generateDirAndFilename(filename, directoryName, sopClassUID, sopInstanceUID, dataset);
269         if (status.good())
270         {
271             DCMNET_DEBUG("generated filename for received object: " << filename);
272             // create the output directory (if needed)
273             status = OFStandard::createDirectory(directoryName, OutputDirectory /* rootDir */);
274             if (status.good())
275             {
276                 if (OFStandard::fileExists(filename))
277                     DCMNET_WARN("file already exists, overwriting: " << filename);
278                 // store the received dataset to file (with default settings)
279                 status = fileformat.saveFile(filename);
280                 if (status.good())
281                 {
282                     // call the notification handler (default implementation outputs to the logger)
283                     notifyInstanceStored(filename, sopClassUID, sopInstanceUID, dataset);
284                     statusCode = STATUS_Success;
285                 } else {
286                     DCMNET_ERROR("cannot store received object: " << filename << ": " << status.text());
287                     statusCode = STATUS_STORE_Refused_OutOfResources;
288 
289                     // delete incomplete file
290                     OFStandard::deleteFile(filename);
291                 }
292             } else {
293                 DCMNET_ERROR("cannot create directory for received object: " << directoryName << ": " << status.text());
294                 statusCode = STATUS_STORE_Refused_OutOfResources;
295             }
296         } else
297             DCMNET_ERROR("cannot generate directory or file name for received object: " << status.text());
298     }
299     return statusCode;
300 }
301 
302 
generateSTORERequestFilename(const T_DIMSE_C_StoreRQ & reqMessage,OFString & filename)303 OFCondition DcmStorageSCP::generateSTORERequestFilename(const T_DIMSE_C_StoreRQ &reqMessage,
304                                                         OFString &filename)
305 {
306     OFString directoryName;
307     OFString sopClassUID = reqMessage.AffectedSOPClassUID;
308     OFString sopInstanceUID = reqMessage.AffectedSOPInstanceUID;
309     // generate filename (with full path)
310     OFCondition status = generateDirAndFilename(filename, directoryName, sopClassUID, sopInstanceUID);
311     if (status.good())
312     {
313         DCMNET_DEBUG("generated filename for object to be received: " << filename);
314         // create the output directory (if needed)
315         status = OFStandard::createDirectory(directoryName, OutputDirectory /* rootDir */);
316         if (status.bad())
317             DCMNET_ERROR("cannot create directory for object to be received: " << directoryName << ": " << status.text());
318     } else
319         DCMNET_ERROR("cannot generate directory or file name for object to be received: " << status.text());
320     return status;
321 }
322 
323 
notifyInstanceStored(const OFString & filename,const OFString &,const OFString &,DcmDataset *) const324 void DcmStorageSCP::notifyInstanceStored(const OFString &filename,
325                                          const OFString & /*sopClassUID*/,
326                                          const OFString & /*sopInstanceUID*/,
327                                          DcmDataset * /*dataset*/) const
328 {
329     // by default, output some useful information
330     DCMNET_INFO("Stored received object to file: " << filename);
331 }
332 
333 
generateDirAndFilename(OFString & filename,OFString & directoryName,OFString & sopClassUID,OFString & sopInstanceUID,DcmDataset * dataset)334 OFCondition DcmStorageSCP::generateDirAndFilename(OFString &filename,
335                                                   OFString &directoryName,
336                                                   OFString &sopClassUID,
337                                                   OFString &sopInstanceUID,
338                                                   DcmDataset *dataset)
339 {
340     OFCondition status = EC_Normal;
341     // get SOP class and instance UID (if not yet known from the command set)
342     if (dataset != NULL)
343     {
344         if (sopClassUID.empty())
345             dataset->findAndGetOFString(DCM_SOPClassUID, sopClassUID);
346         if (sopInstanceUID.empty())
347             dataset->findAndGetOFString(DCM_SOPInstanceUID, sopInstanceUID);
348     }
349     // generate directory name
350     OFString generatedDirName;
351     switch (DirectoryGeneration)
352     {
353         case DGM_NoSubdirectory:
354             // do nothing (default)
355             break;
356         // use series date (if available) for subdirectory structure
357         case DGM_SeriesDate:
358             if (dataset != NULL)
359             {
360                 OFString seriesDate;
361                 DcmElement *element = NULL;
362                 // try to get the series date from the dataset
363                 if (dataset->findAndGetElement(DCM_SeriesDate, element).good() && (element->ident() == EVR_DA))
364                 {
365                     OFString dateValue;
366                     DcmDate *dateElement = OFstatic_cast(DcmDate *, element);
367                     // output ISO format is: YYYY-MM-DD
368                     if (dateElement->getISOFormattedDate(dateValue).good() && (dateValue.length() == 10))
369                     {
370                         OFOStringStream stream;
371                         stream << StandardSubdirectory << PATH_SEPARATOR
372                             << dateValue.substr(0, 4) << PATH_SEPARATOR
373                             << dateValue.substr(5 ,2) << PATH_SEPARATOR
374                             << dateValue.substr(8, 2) << OFStringStream_ends;
375                         OFSTRINGSTREAM_GETSTR(stream, tmpString)
376                         generatedDirName = tmpString;
377                         OFSTRINGSTREAM_FREESTR(tmpString);
378                     }
379                 }
380                 // alternatively, if that fails, use the current system date
381                 if (generatedDirName.empty())
382                 {
383                     OFString currentDate;
384                     status = DcmDate::getCurrentDate(currentDate);
385                     if (status.good())
386                     {
387                         OFOStringStream stream;
388                         stream << UndefinedSubdirectory << PATH_SEPARATOR
389                             << currentDate << OFStringStream_ends;
390                         OFSTRINGSTREAM_GETSTR(stream, tmpString)
391                         generatedDirName = tmpString;
392                         OFSTRINGSTREAM_FREESTR(tmpString);
393                     }
394                 }
395             } else {
396                 DCMNET_DEBUG("received dataset is not available in order to determine the SeriesDate "
397                     << DCM_SeriesDate << ", are you using the bit preserving mode?");
398                 // no DICOM dataset given, so we cannot determine the series date
399                 status = EC_CouldNotGenerateDirectoryName;
400             }
401             break;
402     }
403     if (status.good())
404     {
405         // combine the generated directory name with the output directory
406         OFStandard::combineDirAndFilename(directoryName, OutputDirectory, generatedDirName);
407         // generate filename
408         OFString generatedFileName;
409         switch (FilenameGeneration)
410         {
411             // use modality prefix and SOP instance UID (default)
412             case FGM_SOPInstanceUID:
413             {
414                 if (sopClassUID.empty())
415                     status = NET_EC_InvalidSOPClassUID;
416                 else if (sopInstanceUID.empty())
417                     status = NET_EC_InvalidSOPInstanceUID;
418                 else {
419                     OFOStringStream stream;
420                     stream << dcmSOPClassUIDToModality(sopClassUID.c_str(), "UNKNOWN")
421                            << '.' << sopInstanceUID << FilenameExtension << OFStringStream_ends;
422                     OFSTRINGSTREAM_GETSTR(stream, tmpString)
423                     generatedFileName = tmpString;
424                     OFSTRINGSTREAM_FREESTR(tmpString);
425                     // combine the generated file name with the directory name
426                     OFStandard::combineDirAndFilename(filename, directoryName, generatedFileName);
427                 }
428                 break;
429             }
430             // unique filename based on modality prefix and newly generated UID
431             case FGM_UniqueFromNewUID:
432             {
433                 char uidBuffer[70];
434                 dcmGenerateUniqueIdentifier(uidBuffer);
435                 OFOStringStream stream;
436                 stream << dcmSOPClassUIDToModality(sopClassUID.c_str(), "UNKNOWN")
437                        << ".X." << uidBuffer << FilenameExtension << OFStringStream_ends;
438                 OFSTRINGSTREAM_GETSTR(stream, tmpString)
439                 generatedFileName = tmpString;
440                 OFSTRINGSTREAM_FREESTR(tmpString);
441                 // combine the generated file name with the directory name
442                 OFStandard::combineDirAndFilename(filename, directoryName, generatedFileName);
443                 break;
444             }
445             // unique pseudo-random filename (also checks for existing files, so we need some special handling)
446             case FGM_ShortUniquePseudoRandom:
447             {
448                 OFString prefix = dcmSOPClassUIDToModality(sopClassUID.c_str(), "XX");
449                 prefix += '_';
450                 // TODO: we might want to use a more appropriate seed value
451                 unsigned int seed = OFstatic_cast(unsigned int, time(NULL));
452                 if (!FilenameCreator.makeFilename(seed, directoryName.c_str(), prefix.c_str(), FilenameExtension.c_str(), filename))
453                     status = EC_CouldNotGenerateFilename;
454                 break;
455             }
456             // use current system time and modality suffix for filename
457             case FGM_CurrentSystemTime:
458             {
459                 OFString timeStamp;
460                 // get the date/time as: YYYYMMDDHHMMSS.FFFFFF
461                 if (DcmDateTime::getCurrentDateTime(timeStamp, OFTrue /* seconds */, OFTrue /* fraction */).good())
462                 {
463                     OFOStringStream stream;
464                     stream << timeStamp << '.' << dcmSOPClassUIDToModality(sopClassUID.c_str(), "UNKNOWN")
465                         << FilenameExtension << OFStringStream_ends;
466                     OFSTRINGSTREAM_GETSTR(stream, tmpString)
467                     generatedFileName = tmpString;
468                     OFSTRINGSTREAM_FREESTR(tmpString);
469                     // combine the generated file name
470                     OFStandard::combineDirAndFilename(filename, directoryName, generatedFileName);
471                 } else
472                     status = EC_CouldNotGenerateFilename;
473                 break;
474             }
475 
476         }
477     }
478     return status;
479 }
480