filemanager.cpp Example File

torrent/filemanager.cpp

  /****************************************************************************
  **
  ** Copyright (C) 2016 The Qt Company Ltd.
  ** Contact: https://www.qt.io/licensing/
  **
  ** This file is part of the examples of the Qt Toolkit.
  **
  ** $QT_BEGIN_LICENSE:BSD$
  ** Commercial License Usage
  ** Licensees holding valid commercial Qt licenses may use this file in
  ** accordance with the commercial license agreement provided with the
  ** Software or, alternatively, in accordance with the terms contained in
  ** a written agreement between you and The Qt Company. For licensing terms
  ** and conditions see https://www.qt.io/terms-conditions. For further
  ** information use the contact form at https://www.qt.io/contact-us.
  **
  ** BSD License Usage
  ** Alternatively, you may use this file under the terms of the BSD license
  ** as follows:
  **
  ** "Redistribution and use in source and binary forms, with or without
  ** modification, are permitted provided that the following conditions are
  ** met:
  **   * Redistributions of source code must retain the above copyright
  **     notice, this list of conditions and the following disclaimer.
  **   * Redistributions in binary form must reproduce the above copyright
  **     notice, this list of conditions and the following disclaimer in
  **     the documentation and/or other materials provided with the
  **     distribution.
  **   * Neither the name of The Qt Company Ltd nor the names of its
  **     contributors may be used to endorse or promote products derived
  **     from this software without specific prior written permission.
  **
  **
  ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
  **
  ** $QT_END_LICENSE$
  **
  ****************************************************************************/

  #include "filemanager.h"
  #include "metainfo.h"

  #include <QByteArray>
  #include <QDir>
  #include <QFile>
  #include <QTimer>
  #include <QTimerEvent>
  #include <QCryptographicHash>

  FileManager::FileManager(QObject *parent)
      : QThread(parent)
  {
      quit = false;
      totalLength = 0;
      readId = 0;
      startVerification = false;
      wokeUp = false;
      newFile = false;
      numPieces = 0;
      verifiedPieces.fill(false);
  }

  FileManager::~FileManager()
  {
      quit = true;
      cond.wakeOne();
      wait();

      foreach (QFile *file, files) {
          file->close();
          delete file;
      }
  }

  int FileManager::read(int pieceIndex, int offset, int length)
  {
      ReadRequest request;
      request.pieceIndex = pieceIndex;
      request.offset = offset;
      request.length = length;

      QMutexLocker locker(&mutex);
      request.id = readId++;
      readRequests << request;

      if (!wokeUp) {
          wokeUp = true;
          QMetaObject::invokeMethod(this, "wakeUp", Qt::QueuedConnection);
      }

      return request.id;
  }

  void FileManager::write(int pieceIndex, int offset, const QByteArray &data)
  {
      WriteRequest request;
      request.pieceIndex = pieceIndex;
      request.offset = offset;
      request.data = data;

      QMutexLocker locker(&mutex);
      writeRequests << request;

      if (!wokeUp) {
          wokeUp = true;
          QMetaObject::invokeMethod(this, "wakeUp", Qt::QueuedConnection);
      }
  }

  void FileManager::verifyPiece(int pieceIndex)
  {
      QMutexLocker locker(&mutex);
      pendingVerificationRequests << pieceIndex;
      startVerification = true;

      if (!wokeUp) {
          wokeUp = true;
          QMetaObject::invokeMethod(this, "wakeUp", Qt::QueuedConnection);
      }
  }

  int FileManager::pieceLengthAt(int pieceIndex) const
  {
      QMutexLocker locker(&mutex);
      return (sha1s.size() == pieceIndex + 1)
          ? (totalLength % pieceLength) : pieceLength;
  }

  QBitArray FileManager::completedPieces() const
  {
      QMutexLocker locker(&mutex);
      return verifiedPieces;
  }

  void FileManager::setCompletedPieces(const QBitArray &pieces)
  {
      QMutexLocker locker(&mutex);
      verifiedPieces = pieces;
  }

  QString FileManager::errorString() const
  {
      return errString;
  }

  void FileManager::run()
  {
      if (!generateFiles())
          return;

      do {
          {
              // Go to sleep if there's nothing to do.
              QMutexLocker locker(&mutex);
              if (!quit && readRequests.isEmpty() && writeRequests.isEmpty() && !startVerification)
                  cond.wait(&mutex);
          }

          // Read pending read requests
          mutex.lock();
          QList<ReadRequest> newReadRequests = readRequests;
          readRequests.clear();
          mutex.unlock();
          while (!newReadRequests.isEmpty()) {
              ReadRequest request = newReadRequests.takeFirst();
              QByteArray block = readBlock(request.pieceIndex, request.offset, request.length);
              emit dataRead(request.id, request.pieceIndex, request.offset, block);
          }

          // Write pending write requests
          mutex.lock();
          QList<WriteRequest> newWriteRequests = writeRequests;
          writeRequests.clear();
          while (!quit && !newWriteRequests.isEmpty()) {
              WriteRequest request = newWriteRequests.takeFirst();
              writeBlock(request.pieceIndex, request.offset, request.data);
          }

          // Process pending verification requests
          if (startVerification) {
              newPendingVerificationRequests = pendingVerificationRequests;
              pendingVerificationRequests.clear();
              verifyFileContents();
              startVerification = false;
          }
          mutex.unlock();
          newPendingVerificationRequests.clear();

      } while (!quit);

      // Write pending write requests
      mutex.lock();
      QList<WriteRequest> newWriteRequests = writeRequests;
      writeRequests.clear();
      mutex.unlock();
      while (!newWriteRequests.isEmpty()) {
          WriteRequest request = newWriteRequests.takeFirst();
          writeBlock(request.pieceIndex, request.offset, request.data);
      }
  }

  void FileManager::startDataVerification()
  {
      QMutexLocker locker(&mutex);
      startVerification = true;
      cond.wakeOne();
  }

  bool FileManager::generateFiles()
  {
      numPieces = -1;

      // Set up the thread local data
      if (metaInfo.fileForm() == MetaInfo::SingleFileForm) {
          QMutexLocker locker(&mutex);
          MetaInfoSingleFile singleFile = metaInfo.singleFile();

          QString prefix;
          if (!destinationPath.isEmpty()) {
              prefix = destinationPath;
              if (!prefix.endsWith('/'))
                  prefix += '/';
              QDir dir;
              if (!dir.mkpath(prefix)) {
                  errString = tr("Failed to create directory %1").arg(prefix);
                  emit error();
                  return false;
              }
          }
          QFile *file = new QFile(prefix + singleFile.name);
          if (!file->open(QFile::ReadWrite)) {
              errString = tr("Failed to open/create file %1: %2")
                          .arg(file->fileName()).arg(file->errorString());
              emit error();
              delete file;
              return false;
          }

          if (file->size() != singleFile.length) {
              newFile = true;
              if (!file->resize(singleFile.length)) {
                  errString = tr("Failed to resize file %1: %2")
                              .arg(file->fileName()).arg(file->errorString());
                  delete file;
                  emit error();
                  return false;
              }
          }
          fileSizes << file->size();
          files << file;
          file->close();

          pieceLength = singleFile.pieceLength;
          totalLength = singleFile.length;
          sha1s = singleFile.sha1Sums;
      } else {
          QMutexLocker locker(&mutex);
          QDir dir;
          QString prefix;

          if (!destinationPath.isEmpty()) {
              prefix = destinationPath;
              if (!prefix.endsWith('/'))
                  prefix += '/';
          }
          if (!metaInfo.name().isEmpty()) {
              prefix += metaInfo.name();
              if (!prefix.endsWith('/'))
                  prefix += '/';
          }
          if (!dir.mkpath(prefix)) {
              errString = tr("Failed to create directory %1").arg(prefix);
              emit error();
              return false;
          }

          foreach (const MetaInfoMultiFile &entry, metaInfo.multiFiles()) {
              QString filePath = QFileInfo(prefix + entry.path).path();
              if (!QFile::exists(filePath)) {
                  if (!dir.mkpath(filePath)) {
                      errString = tr("Failed to create directory %1").arg(filePath);
                      emit error();
                      return false;
                  }
              }

              QFile *file = new QFile(prefix + entry.path);
              if (!file->open(QFile::ReadWrite)) {
                  errString = tr("Failed to open/create file %1: %2")
                              .arg(file->fileName()).arg(file->errorString());
                  emit error();
                  delete file;
                  return false;
              }

              if (file->size() != entry.length) {
                  newFile = true;
                  if (!file->resize(entry.length)) {
                      errString = tr("Failed to resize file %1: %2")
                                  .arg(file->fileName()).arg(file->errorString());
                      emit error();
                      delete file;
                      return false;
                  }
              }
              fileSizes << file->size();
              files << file;
              file->close();

              totalLength += entry.length;
          }

          sha1s = metaInfo.sha1Sums();
          pieceLength = metaInfo.pieceLength();
      }
      numPieces = sha1s.size();
      return true;
  }

  QByteArray FileManager::readBlock(int pieceIndex, int offset, int length)
  {
      QByteArray block;
      qint64 startReadIndex = (quint64(pieceIndex) * pieceLength) + offset;
      qint64 currentIndex = 0;

      for (int i = 0; !quit && i < files.size() && length > 0; ++i) {
          QFile *file = files[i];
          qint64 currentFileSize = fileSizes.at(i);
          if ((currentIndex + currentFileSize) > startReadIndex) {
              if (!file->isOpen()) {
                  if (!file->open(QFile::ReadWrite)) {
                      errString = tr("Failed to read from file %1: %2")
                          .arg(file->fileName()).arg(file->errorString());
                      emit error();
                      break;
                  }
              }

              file->seek(startReadIndex - currentIndex);
              QByteArray chunk = file->read(qMin<qint64>(length, currentFileSize - file->pos()));
              file->close();

              block += chunk;
              length -= chunk.size();
              startReadIndex += chunk.size();
              if (length < 0) {
                  errString = tr("Failed to read from file %1 (read %3 bytes): %2")
                              .arg(file->fileName()).arg(file->errorString()).arg(length);
                  emit error();
                  break;
              }
          }
          currentIndex += currentFileSize;
      }
      return block;
  }

  bool FileManager::writeBlock(int pieceIndex, int offset, const QByteArray &data)
  {
      qint64 startWriteIndex = (qint64(pieceIndex) * pieceLength) + offset;
      qint64 currentIndex = 0;
      int bytesToWrite = data.size();
      int written = 0;

      for (int i = 0; !quit && i < files.size(); ++i) {
          QFile *file = files[i];
          qint64 currentFileSize = fileSizes.at(i);

          if ((currentIndex + currentFileSize) > startWriteIndex) {
              if (!file->isOpen()) {
                  if (!file->open(QFile::ReadWrite)) {
                      errString = tr("Failed to write to file %1: %2")
                          .arg(file->fileName()).arg(file->errorString());
                      emit error();
                      break;
                  }
              }

              file->seek(startWriteIndex - currentIndex);
              qint64 bytesWritten = file->write(data.constData() + written,
                                                qMin<qint64>(bytesToWrite, currentFileSize - file->pos()));
              file->close();

              if (bytesWritten <= 0) {
                  errString = tr("Failed to write to file %1: %2")
                              .arg(file->fileName()).arg(file->errorString());
                  emit error();
                  return false;
              }

              written += bytesWritten;
              startWriteIndex += bytesWritten;
              bytesToWrite -= bytesWritten;
              if (bytesToWrite == 0)
                  break;
          }
          currentIndex += currentFileSize;
      }
      return true;
  }

  void FileManager::verifyFileContents()
  {
      // Verify all pieces the first time
      if (newPendingVerificationRequests.isEmpty()) {
          if (verifiedPieces.count(true) == 0) {
              verifiedPieces.resize(sha1s.size());

              int oldPercent = 0;
              if (!newFile) {
                  int numPieces = sha1s.size();

                  for (int index = 0; index < numPieces; ++index) {
                      verifySinglePiece(index);

                      int percent = ((index + 1) * 100) / numPieces;
                      if (oldPercent != percent) {
                          emit verificationProgress(percent);
                          oldPercent = percent;
                      }
                  }
              }
          }
          emit verificationDone();
          return;
      }

      // Verify all pending pieces
      foreach (int index, newPendingVerificationRequests)
          emit pieceVerified(index, verifySinglePiece(index));
  }

  bool FileManager::verifySinglePiece(int pieceIndex)
  {
      QByteArray block = readBlock(pieceIndex, 0, pieceLength);
      QByteArray sha1Sum = QCryptographicHash::hash(block, QCryptographicHash::Sha1);

      if (sha1Sum != sha1s.at(pieceIndex))
          return false;
      verifiedPieces.setBit(pieceIndex);
      return true;
  }

  void FileManager::wakeUp()
  {
      QMutexLocker locker(&mutex);
      wokeUp = false;
      cond.wakeOne();
  }