2

I am having issues where the file is being uploaded twice to the server.

I am using the QFileSystemWatcher class from C++ Qt on Windows XP to send a file when the folder changes The files are small (1-12kb).

The application sends the files by scanning the folder whenever it changes (on directoryChanged signal), loop through the files and send the one that I need. The server responds with an xml file that is returned into the same folder for another application to processes it.

Apparently what’s happening is that on some systems there are 2 very fast directoryChanged signals at nearly the same time and there are two very fast file uploads happening.

The server is running Apache and PHP, and there’s a simple MUTEX in place on the PHP side, but I just wanted to get to the root of the problem which seems to be on the Qt side. I'm open to using another class, another library or straight C++.

Here is some code, I stripped all the irrelevant content:

this->w = new QFileSystemWatcher();
this->w->addPath("C:/POSERA/MaitreD/DATA/INT");

QStringList directoryList = w->directories();
Q_FOREACH(QString directory, directoryList)
{
    qDebug() << "Watching Main Directory name: " << directory << endl;
}

DirectoryWatcher* dw = new DirectoryWatcher;

QObject::connect( this->w, SIGNAL(directoryChanged(const QString&)),
                  dw, SLOT(directoryChanged(const QString&)));

and the DirectoryWatcher.cpp:

DirectoryWatcher::DirectoryWatcher(QWidget* parent) : QWidget(parent)
{
    lockSend = false;
}

void DirectoryWatcher::directoryChanged(const QString& str)
{
    directoryLastChanged = str;

    QByteArray byteArray = str.toUtf8();
    const char* cString = byteArray.constData();

    sendChangedFiles(cString);
}

void DirectoryWatcher::sendChangedFiles(const char* path)
{
    DIR *dir;
    struct dirent *ent;
    if ((dir = opendir (path)) != NULL)
    {
        QString str;

        while ((ent = readdir (dir)) != NULL)
        {
            str = QString("%1/%2").arg(path, ent->d_name);

            QFileInfo info(str);

            if (lockSend == false &&
               (info.completeSuffix() == "xml" || info.completeSuffix() == "XML") &&
               (info.baseName() != "") &&
               (!info.baseName().startsWith("REDM")) &&
               (!info.baseName().startsWith("REFT")))
            {
                // reset the counter.
                this->resendCounter = 0;

                sendFileAndAccept(str.toUtf8().constData());
            }
        }
        closedir (dir);
    }
    else
    {
        qDebug() << "Could not open directory" << endl;
    }
}

class QNetworkRequest;
class QNetworkReply;

void DirectoryWatcher::sendFileAndAccept(const char* path)
{
    // increment the resend counter
    this->resendCounter++;

    QFileInfo fileInfo(path);

    QNetworkAccessManager * mgr = new QNetworkAccessManager(this);
    connect(mgr,SIGNAL(finished(QNetworkReply*)),
            this,SLOT(saveResponse(QNetworkReply*)));
    connect(mgr,SIGNAL(finished(QNetworkReply*)),
            mgr,SLOT(deleteLater())); // @todo delete later

    QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);

    QHttpPart filePart;
    filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/xml")); // @todo test
    filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"someFile\"; filename=\"" + fileInfo.baseName() + ".xml\""));

    currentFileSent = fileInfo.baseName();

    QFile *file = new QFile(path);
    file->open(QIODevice::ReadOnly);
    filePart.setBodyDevice(file);
    file->setParent(multiPart); // we cannot delete the file now, so delete it with the multiPart

    multiPart->append(filePart);

    // POST request
    QNetworkReply *reply = mgr->post(QNetworkRequest(QUrl(XXXXXX)), multiPart);

    multiPart->setParent(reply); // delete the multiPart with the reply

    // lock
    lockSend = true;
}

void DirectoryWatcher::saveResponse(QNetworkReply *rep) {

    // get the response
    QByteArray bts = rep->readAll();
    QString str(bts);

    // compute new path
    QString partName = currentFileSent.mid(1, currentFileSent.length());
    QString newPath = QString("%1/A%2.xml").arg(directoryLastChanged, partName);

    qDebug() << "new path: " << newPath << endl;

    switch (rep->error()) {
        case QNetworkReply::NoError: {
            qDebug() << "NO ERROR" << endl;

            // save response to a file.

            QFile file(newPath);
            file.open(QIODevice::WriteOnly | QIODevice::Text);
            QTextStream out(&file);
            out << str;

            file.close();

            break;
        }
        default:

//        case QNetworkReply::TimeoutError :
//        case QNetworkReply::HostNotFoundError :
            qDebug() << "NETWORK REPLY ERROR" << endl;
            // resend the file if the counter is < 10
            if (this->resendCounter < 5) {

                // delay by n sec
                QTime dieTime = QTime::currentTime().addSecs(1);
                while( QTime::currentTime() < dieTime )
                    QCoreApplication::processEvents(QEventLoop::AllEvents, 100);

                sendFileAndAccept(this->lastPathSent.toStdString().c_str());
            } else {

                // after 10 attempts, we're probably sure that the network is down
                // save the file somewhere and generate a default one to prevent timeouts.

                qDebug() << "Saving file for later..." << endl;
                if (!saveFileForLater(lastPathSent.toStdString().c_str())) {
                    qDebug() << "ERROR SAVING FILE, CHECK IF FOLDER EXISTS AND THE PERMISSIONS." << endl;
                }

                // generate a default one to prevent timeouts.
                qDebug() << "Generate a default file..." << endl;
                // ...
            }

            break;
    }

    // unlock
    lockSend = false;

    rep->deleteLater(); // prevent memory leak
}

bool DirectoryWatcher::saveFileForLater(const char* pathToRequestFile) {

    QFile file(pathToRequestFile);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "readonly and text" << endl;
        return false;
    }

    QString path(pathToRequestFile);
    QFileInfo fileinfo(path);

    QString newPath = "C:\\data\\offline\\" + fileinfo.fileName();

    return file.copy(newPath);

}

Thanks for your help.

p.i.g.
  • 2,815
  • 2
  • 24
  • 41
alexg
  • 902
  • 11
  • 37
  • I doubt you will find a solution. I can confirm this behaviour. Though I had it with large files (>500mb). The filesystemwatcher fired several times during the copy process. I also had to implement some kind of 'mutex'. But I am still wondering a bit that you can see it with small files like yours. – Greenflow Aug 22 '13 at 22:47
  • I don't understand the question. Do you want just 1 directoryChanged signal for both files (implement a throttle on a known clock). Or do you want to handle each file separately? – rileyberton Aug 22 '13 at 23:57
  • thanks for your answers guys. @Greenflow I think it's the opposite, small files are more error prone in the file upload scenarios. – alexg Aug 23 '13 at 02:51
  • @rileyberton in the case I described there's only one file to be sent, but the app sends the same file 2 times. PHP interprets them as 2 separate threads that may sometimes enter the same critical section (without the mutex implemented). What I want is to send a file, then wait for a response (the lockSend should be responsible for that). I'm sure this is possible, if applications like dropbox do that without problems, then I don't think I should reinvent the wheel with some kind of DIY solution and I'm kind of disappointed a framework like Qt doesn't prevent this from happening – alexg Aug 23 '13 at 02:55
  • So you could keep a short lived set that tracks files in flight and not allow duplicates based on name. When the upload completes simply remove the file from the set. This prevents the mutex around sendChangedFiles. – rileyberton Aug 23 '13 at 11:52
  • I tried that a while ago. Didn't work because in some cases the filename was the same. The third party application is generating the request XML and is waiting for the answer XML. When the answer XML is created, it moves both, the request and the answer to another folder. So in some cases a different request with the same file name could be generated by the application. This is out of my control. – alexg Aug 23 '13 at 13:36
  • basically what I want is: 1) Grab the file changed 2) Send it 3) Wait for an answer, thus blocking all other sends 4) Receive an answer 5) Create a file in the folder with answer content. The problem with QFileSystemWatcher is that it sends a signal when the directory has changed and I need to parse the directory to find what files I do need. It would be so much easier if I could get the filename that changed and/or was created in the specified folder. – alexg Aug 23 '13 at 13:44
  • With regard to your last point Trevor, there is a signal on `QFileSystemWatcher` called `fileChanged()`. Have you tried that one instead of `directoryChanged()`? – RobbieE Oct 19 '13 at 10:37

1 Answers1

2

The most possible reason for 2 emits of directoryChanged is that normal editor when saving changes removes and writes the new version of the file to the disk. That's why there is one signal when file is removed and one when it's recreated.

user2087932
  • 128
  • 2
  • 9