Selaa lähdekoodia

Added file upload validation to saveImpl of ctkXnatFile

New class ctkXnatResourceCatalogXmlParser can parse the catalog xml of a
resource and returns the md5 hashes on the server. It is implemented as a
SAX parser.
The ctkXnatAPI's parseXmlResponse uses the new parser in order to process
the reply.
If the file validation fails an exception is thrown and the file is deleted.
Andreas Fetzer 10 vuotta sitten
vanhempi
commit
8fbab7cdb3

+ 1 - 0
Libs/XNAT/Core/CMakeLists.txt

@@ -21,6 +21,7 @@ set(KIT_SRCS
   ctkXnatReconstruction.cpp
   ctkXnatReconstructionFolder.cpp
   ctkXnatResource.cpp
+  ctkXnatResourceCatalogXmlParser.cpp
   ctkXnatScan.cpp
   ctkXnatScanFolder.cpp
   ctkXnatAssessor.cpp

+ 20 - 1
Libs/XNAT/Core/ctkXnatAPI.cpp

@@ -21,13 +21,20 @@
 
 // ctkXnatAPI includes
 #include "ctkXnatAPI_p.h"
+
+#include "ctkXnatResourceCatalogXmlParser.h"
+
 #include "qRestResult.h"
+
 #include <QNetworkReply>
 #include <QRegExp>
 #include <QUrl>
 #if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
 #include <QUrlQuery>
 #endif
+#include <QXmlDefaultHandler>
+#include <QXmlInputSource>
+#include <QXmlSimpleReader>
 
 // --------------------------------------------------------------------------
 // ctkXnatAPI methods
@@ -108,9 +115,21 @@ void ctkXnatAPI::parseResponse(qRestResult* restResult, const QByteArray& respon
 }
 
 // --------------------------------------------------------------------------
-QList<QVariantMap> ctkXnatAPI::parseXmlResponse(qRestResult* /*restResult*/, const QByteArray& /*response*/)
+QList<QVariantMap> ctkXnatAPI::parseXmlResponse(qRestResult* /*restResult*/, const QByteArray& response)
 {
+  QXmlInputSource xmlInput;
+  xmlInput.setData(response);
+  QXmlSimpleReader xmlReader;
+
   QList<QVariantMap> result;
+  // In this case a resource catalog xml was requested
+  if (response.contains("<cat:Catalog"))
+  {
+    ctkXnatResourceCatalogXmlParser parser;
+    xmlReader.setContentHandler(&parser);
+    xmlReader.parse(&xmlInput, true);
+    result = parser.md5Hashes();
+  }
   return result;
 }
 

+ 50 - 12
Libs/XNAT/Core/ctkXnatFile.cpp

@@ -25,6 +25,8 @@
 #include "ctkXnatObjectPrivate.h"
 #include "ctkXnatSession.h"
 
+#include <QCryptographicHash>
+#include <QDebug>
 #include <QFile>
 
 const QString ctkXnatFile::FILE_NAME = "Name";
@@ -138,6 +140,7 @@ void ctkXnatFile::reset()
 //----------------------------------------------------------------------------
 void ctkXnatFile::fetchImpl()
 {
+  // Does not make sense to fetch a file
 }
 
 //----------------------------------------------------------------------------
@@ -150,6 +153,7 @@ void ctkXnatFile::downloadImpl(const QString& filename)
 //----------------------------------------------------------------------------
 void ctkXnatFile::saveImpl()
 {
+  Q_D(ctkXnatFile);
   QString query = this->resourceUri();
   QString filename = this->localFilePath();
 
@@ -182,24 +186,58 @@ void ctkXnatFile::saveImpl()
   query.append(QString("&%1=%2").arg("content", this->fileContent()));
   query.append(QString("&%1=%2").arg("tags", this->fileTags()));
 
+  // TODO May be flag for setting overwrite and not doing this automatically
   if (this->exists())
     query.append(QString("&%1=%2").arg("overwrite", true));
 
+  // Flag needed for file upload
   query.append(QString("&%1=%2").arg("inbody", "true"));
 
   this->session()->upload(filename, query);
-  qint64 localFileSize = file.size();
-  QUuid queryId = this->session()->httpHead(this->resourceUri());
-  QMap<QByteArray, QByteArray> header = this->session()->httpHeadSync(queryId);
-  QVariant sizeOnServer = header.value("Content-Length");
-  qint64 remoteFileSize = sizeOnServer.toLongLong();
-
-  // Retrieving the md5 checksum on the server is not always possible
-  // At least we can check whether the file size is the same
-  if (remoteFileSize != localFileSize)
+
+  // Validating the file upload by requesting the catalog XML
+  // of the parent resource. Unfortunately for XNAT versions <= 1.6.4
+  // this is the only way to get the file's MD5 hash form the server.
+  QString md5Query = this->parent()->resourceUri();
+  QUuid md5ID = this->session()->httpGet(md5Query);
+  QList<QVariantMap> result = this->session()->httpSync(md5ID);
+
+  QString md5ChecksumRemote ("0");
+  // Newly added files are usually at the end of the catalog
+  // and hence at the end of the result list. So iterating backwards
+  // is for performance reasons.
+  QList<QVariantMap>::const_iterator it = result.constEnd()-1;
+  while (it != result.constBegin())
+  {
+    QVariantMap::const_iterator it2 = (*it).find(this->name());
+    if (it2 != (*it).constEnd())
+    {
+      md5ChecksumRemote = it2.value().toString();
+      break;
+    }
+    --it;
+  }
+
+  if (file.open(QFile::ReadOnly) && md5ChecksumRemote != "0")
+  {
+    QCryptographicHash hash(QCryptographicHash::Md5);
+    // TODO Do this in case of Qt5
+    //if (hash.addData(&file))
+    hash.addData(file.readAll());
+    QString md5ChecksumLocal(hash.result().toHex());
+    // Retrieving the md5 checksum on the server and comparing
+    // if with the local file md5 sum
+    if (md5ChecksumLocal != md5ChecksumRemote)
+    {
+      // Remove corrupted file from server
+      this->erase();
+      throw ctkXnatException("Upload failed! An error occurred during file upload.");
+    }
+  }
+  else
   {
-    // Remove corrupted file from server
-    this->erase();
-    throw ctkXnatException("Upload failed! An error occurred during file upload.");
+    qWarning()<<"Could not validate file upload!";
   }
+  // End file validation
+
 }

+ 86 - 0
Libs/XNAT/Core/ctkXnatResourceCatalogXmlParser.cpp

@@ -0,0 +1,86 @@
+/*=============================================================================
+
+  Library: XNAT/Core
+
+  Copyright (c) German Cancer Research Center,
+    Division of Medical and Biological Informatics
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=============================================================================*/
+
+#include "ctkXnatResourceCatalogXmlParser.h"
+
+#include <QDebug>
+
+//----------------------------------------------------------------------------
+class ctkXnatResourceCatalogXmlParserPrivate
+{
+public:
+
+  ctkXnatResourceCatalogXmlParserPrivate()
+  {
+  }
+
+  QList<QVariantMap> result;
+};
+
+ctkXnatResourceCatalogXmlParser::ctkXnatResourceCatalogXmlParser()
+  : d_ptr (new ctkXnatResourceCatalogXmlParserPrivate())
+{
+}
+ctkXnatResourceCatalogXmlParser::~ctkXnatResourceCatalogXmlParser()
+{
+  delete d_ptr;
+}
+
+bool ctkXnatResourceCatalogXmlParser::startElement(const QString &/*namespaceURI*/, const QString &localName,
+                                                   const QString &qName, const QXmlAttributes &atts)
+{
+  Q_D(ctkXnatResourceCatalogXmlParser);
+  if (qName == "cat:entry")
+  {
+    QString name("");
+    QString md5("");
+    QVariantMap map;
+    for( int i=0; i<atts.count(); i++ )
+    {
+      // TODO ID would be better
+      if( atts.localName(i) == "name")
+        name = atts.value(i);
+
+      if( atts.localName( i ) == "digest" )
+      {
+        md5 = atts.value(i);
+      }
+    }
+
+    if (name.length() == 0 || md5.length() == 0)
+    {
+      qWarning()<<"Error parsing XNAT resource catalog xml!";
+      return false;
+    }
+    else
+    {
+      map[name] = QVariant(md5);
+      d->result.append(map);
+    }
+  }
+  return true;
+}
+
+const QList<QVariantMap>& ctkXnatResourceCatalogXmlParser::md5Hashes()
+{
+  Q_D(ctkXnatResourceCatalogXmlParser);
+  return d->result;
+}

+ 73 - 0
Libs/XNAT/Core/ctkXnatResourceCatalogXmlParser.h

@@ -0,0 +1,73 @@
+/*=============================================================================
+
+  Library: XNAT/Core
+
+  Copyright (c) German Cancer Research Center,
+    Division of Medical and Biological Informatics
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=============================================================================*/
+
+#ifndef CTKXNATRESOURCECATALOGXMLPARSER_H
+#define CTKXNATRESOURCECATALOGXMLPARSER_H
+
+#include "ctkXNATCoreExport.h"
+
+#include <QXmlDefaultHandler>
+
+class ctkXnatResourceCatalogXmlParserPrivate;
+
+/**
+ * @brief Parses the xml response when requesting the catalog xml of a resource
+ *
+ * When sending the following request to XNAT:
+ * <xnat-url>/projects/myProject/resources/<resource-id>
+ * you get the catalog xml of the resource folder as response
+ *
+ * This class parses this response and returns the MD5 hashes of all the resource files,
+ * which are contained by the resource folder.
+ *
+ * @ingroup XNAT_Core
+ */
+class CTK_XNAT_CORE_EXPORT ctkXnatResourceCatalogXmlParser : public QXmlDefaultHandler
+{
+
+public:
+  ctkXnatResourceCatalogXmlParser();
+  ~ctkXnatResourceCatalogXmlParser();
+
+  /**
+   * Overwrites QXmlDefaultHandler::startElement()
+   */
+  bool startElement(const QString &namespaceURI, const QString &localName,
+                    const QString &qName, const QXmlAttributes &atts);
+
+  /**
+   * @brief Returns the md5 hashes of the resource files
+   * @return A list of QVariantMaps, which contain the md5 hashes
+   */
+  const QList<QVariantMap>& md5Hashes();
+
+private:
+
+  ctkXnatResourceCatalogXmlParser(const ctkXnatResourceCatalogXmlParser& );
+  ctkXnatResourceCatalogXmlParser& operator=(const ctkXnatResourceCatalogXmlParser& );
+
+  /// The private implementation part of the object.
+  ctkXnatResourceCatalogXmlParserPrivate* d_ptr;
+
+  Q_DECLARE_PRIVATE(ctkXnatResourceCatalogXmlParser)
+};
+
+#endif //CTKXNATRESOURCECATALOGXMLPARSER_H