code cleanup
[case] / src / operationthread.cpp
diff --git a/src/operationthread.cpp b/src/operationthread.cpp
new file mode 100644 (file)
index 0000000..d0cf7e1
--- /dev/null
@@ -0,0 +1,601 @@
+// case - file manager for N900
+// Copyright (C) 2010 Lukas Hrazky <lukkash@email.cz>
+// 
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+// 
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+// 
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+#include "operationthread.h"
+
+#include <errno.h>
+#include <math.h>
+
+#include "utils.h"
+
+
+#define BLOCK_SIZE (256 * 1024)
+
+
+#define PAUSE()                                                                             \
+    if (pause) {                                                                            \
+        emit operationPaused();                                                             \
+        waitOnCond();                                                                       \
+    }
+
+
+#define SHOW_ERROR_PROMPT(promptString, fileName)                                           \
+    response = NONE;                                                                        \
+    if (ignoreAll[errno]) {                                                                 \
+        response = IGNORE;                                                                  \
+    } else {                                                                                \
+        char buf[255];                                                                      \
+        char *realBuf = buf;                                                                \
+        if (errno == 255) {                                                                 \
+            strcpy(buf, tr("File is sequential").toStdString().c_str());                    \
+        } else {                                                                            \
+            realBuf = strerror_r(errno, buf, 255);                                          \
+        }                                                                                   \
+        emit showErrorPrompt(this, promptString + " " + realBuf + ".", fileName, errno);    \
+        waitOnCond();                                                                       \
+    }
+
+
+#define ERROR_PROMPT(operation, promptString, fileName)                                     \
+{                                                                                           \
+    response = NONE;                                                                        \
+    while (!abort && operation) {                                                           \
+        SHOW_ERROR_PROMPT(promptString, fileName)                                           \
+        if (response == IGNORE) {                                                           \
+            break;                                                                          \
+        }                                                                                   \
+        PAUSE()                                                                             \
+    }                                                                                       \
+}
+
+
+#define SPECIAL_COPY_ERROR_PROMPT(operation, promptString, fileName)                        \
+{                                                                                           \
+    ERROR_PROMPT(operation, promptString, fileName)                                         \
+    if (abort || response == IGNORE) {                                                      \
+        if (!abort) {                                                                       \
+            updateProgress(fileSizeMap[path]);                                              \
+            removeExcludeFiles.insert(path);                                                \
+        }                                                                                   \
+        return;                                                                             \
+    }                                                                                       \
+}
+
+
+#define OVERWRITE_PROMPT(file, newFile)                                                     \
+{                                                                                           \
+    response = NONE;                                                                        \
+                                                                                            \
+    while (!abort && response == NONE && newFile.exists()) {                                \
+        if (overwriteAll != NONE) {                                                         \
+            response = overwriteAll;                                                        \
+        } else {                                                                            \
+            emit showOverwritePrompt(this, newFile.absoluteFilePath(),                      \
+                newFile.isDir() && file.isDir());                                           \
+            waitOnCond();                                                                   \
+                                                                                            \
+            PAUSE()                                                                         \
+            else if (response == NONE) {                                                    \
+                emit showInputFilenamePrompt(this, newFile, file.isDir());                  \
+                waitOnCond();                                                               \
+                if (newNameFromDialog.size()) {                                             \
+                    newFile.setFile(newNameFromDialog);                                     \
+                }                                                                           \
+            }                                                                               \
+        }                                                                                   \
+    }                                                                                       \
+    if (response == ASK) response = NONE;                                                   \
+}
+
+
+OperationThread::OperationThread(const QFileInfoList files, QDir dest) :
+    abort(false),
+    pause(false),
+    files(files),
+    dest(dest),
+    response(NONE),
+    overwriteAll(NONE),
+    totalSize(0),
+    totalValue(0),
+    fileValue(0)
+{
+    memset(ignoreAll, false, sizeof(ignoreAll));
+}
+
+
+void OperationThread::setResponse(
+    const Response response,
+    const bool applyToAll,
+    const int err)
+{
+    mutex.lock();
+
+    this->response = response;
+
+    if (applyToAll) {
+        if (response == KEEP
+            || response == OVERWRITE
+            || response == NONE)
+        {
+            overwriteAll = response;
+        }
+
+        if (response == IGNORE) {
+            ignoreAll[err] = true;
+        }
+    }
+
+    if (response == ABORT) abort = true;
+
+    mutex.unlock();
+    waitCond.wakeAll();
+}
+
+
+void OperationThread::processFiles(const QFileInfoList &files) {
+    for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
+        PAUSE();
+        if (abort) break;
+        perform(*it);
+    }
+}
+
+
+bool OperationThread::remove(QString &fileName, const bool doUpdates) {
+    return remove(QFileInfo(fileName), doUpdates);
+}
+
+
+bool OperationThread::remove(const QFileInfoList &files, const bool doUpdates) {
+    bool res = true;
+    for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
+        if (!remove(*it, doUpdates)) res = false;
+        PAUSE();
+        if (abort) break;
+    }
+    return res;
+}
+
+
+bool OperationThread::remove(const QFileInfo &file, const bool doUpdates) {
+    QString path = file.absoluteFilePath();
+
+    if (removeExcludeFiles.contains(path)) {
+        if (doUpdates) updateProgress(1);
+        return false;
+    }
+
+    QFSFileEngine engine(path);
+
+    if (doUpdates) updateFile(path);
+
+    if (file.isDir()) {
+        if (!remove(listDirFiles(path), doUpdates)) {
+            if (doUpdates) updateProgress(1);
+            return false;
+        }
+
+        if (!listDirFiles(path).size()) {
+            ERROR_PROMPT(!engine.rmdir(path, false), tr("Error deleting directory %1."), path)
+        }
+    } else {
+        ERROR_PROMPT(!engine.remove(), tr("Error deleting file %1."), path)
+    }
+
+    if (!abort && doUpdates) updateProgress(1);
+
+    PAUSE();
+    if (abort || response == IGNORE) return false;
+    return true;
+}
+
+
+void OperationThread::copy(const QFileInfo &file) {
+    QString path(file.absoluteFilePath());
+    QFSFileEngine engine(path);
+    QFileInfo newFile(dest.absolutePath() + "/" + file.fileName());
+
+    updateFile(path);
+
+    // hack to prevent asking about the same file if we already asked in the rename(...) function
+    if (overwriteAll == DONT_ASK_ONCE) {
+        overwriteAll = NONE;
+    } else {
+        OVERWRITE_PROMPT(file, newFile)
+    }
+
+    QString newPath(newFile.absoluteFilePath());
+    QFSFileEngine newEngine(newPath);
+
+    PAUSE();
+    if (abort) return;
+
+    if (file.isDir()) {
+        // save the overwrite response, because the response variable will get ovewritten in remove(...)
+        Response overwriteResponse = response;
+
+        if (newFile.exists() && !newFile.isDir()) {
+            // overwriting a file, so check for KEEP and handle it
+            if (response == KEEP) {
+                updateProgress(fileSizeMap[path]);
+                removeExcludeFiles.insert(path);
+                return;
+            }
+
+            // if it should not be kept, remove it and return on failure
+            if(!remove(newPath)) {
+                updateProgress(fileSizeMap[path]);
+                return;
+            }
+            // create new info since we deleted the file - is it needed?
+            newFile = QFileInfo(newPath);
+        } else {
+            // overwriting a directory - response KEEP means to keep the files inside,
+            // SKIP_DIR means to skip the dir completely
+            if (response == SKIP_DIR) {
+                updateProgress(fileSizeMap[path]);
+                removeExcludeFiles.insert(path);
+                return;
+            }
+        }
+
+        if (!newFile.exists()) {
+            SPECIAL_COPY_ERROR_PROMPT(!engine.mkdir(newPath, false),
+                tr("Error creating directory %1."), newPath)
+        }
+
+        // we've done the job with the dir, so update progress and recurse into the dir
+        updateProgress(1);
+        
+        // change the dest for the recursion
+        QDir destBackup = dest;
+        dest = newPath;
+
+        // and set overwriteAll to the response we got a while ago
+        // because it applies to the files inside the dir
+        Response tmpResp = overwriteAll;
+        overwriteAll = overwriteResponse;
+
+        processFiles(listDirFiles(path));
+
+        overwriteAll = tmpResp;
+
+        ERROR_PROMPT(!newEngine.setPermissions(file.permissions()),
+            tr("Error setting permissions for directory %1."), newPath)
+
+        PAUSE();
+        if (abort) return;
+
+        dest = destBackup;
+    } else {
+        if (response == KEEP) {
+            updateProgress(fileSizeMap[path]);
+            removeExcludeFiles.insert(path);
+            return;
+        }
+
+        SPECIAL_COPY_ERROR_PROMPT(checkSequentialFile(engine), tr("Cannot copy file %1."), path)
+
+        if (newFile.exists() && newFile.isDir()) {
+            SPECIAL_COPY_ERROR_PROMPT(!remove(newPath),
+                tr("Cannot replace directory %1 due to previous errors."), newPath)
+        }
+
+        SPECIAL_COPY_ERROR_PROMPT(!engine.open(QIODevice::ReadOnly), tr("Error reading file %1."), path)
+
+        bool ignore = false, newFileWritten = false;
+        while (!abort && !ignore) {
+            engine.seek(0);
+            fileValue = 0;
+
+            ERROR_PROMPT(!newEngine.open(QIODevice::WriteOnly | QIODevice::Truncate),
+                tr("Error writing file %1."), newPath)
+
+            if (abort || response == IGNORE) {
+                if (response == IGNORE) {
+                    updateProgress(fileSizeMap[path]);
+                    removeExcludeFiles.insert(path);
+                    ignore = true;
+                }
+                break;
+            }
+
+            newFileWritten = true;
+
+            bool error = false;
+            char block[BLOCK_SIZE];
+            qint64 bytes;
+
+            while ((bytes = engine.read(block, sizeof(block))) > 0) {
+                if (bytes == -1 || bytes != newEngine.write(block, bytes)) {
+                    if (bytes == -1) {
+                        SHOW_ERROR_PROMPT(tr("Error while reading from file %1."), path);
+                    } else {
+                        SHOW_ERROR_PROMPT(tr("Error while writing to file %1."), newPath);
+                    }
+
+                    if (!abort) {
+                        if (response == IGNORE) {
+                            updateProgress(fileSizeMap[path] - fileValue);
+                            removeExcludeFiles.insert(path);
+                            ignore = true;
+                        } else {
+                            updateProgress(-fileValue);
+                        }
+                    }
+                    error = true;
+                    break;
+                }
+
+                PAUSE();
+                if (abort) break;
+
+                updateProgress(1);
+            }
+
+            if (!error) break;
+            PAUSE();
+        }
+
+        engine.close();
+        newEngine.close();
+
+        PAUSE();
+        if (abort || ignore) {
+            if (newFileWritten) {
+                newEngine.remove();
+            }
+        } else {
+            ERROR_PROMPT(!newEngine.setPermissions(file.permissions()),
+                tr("Error setting permissions for file %1."), newPath)
+        }
+    }
+}
+
+
+unsigned int OperationThread::calculateFileSize(const QFileInfoList &files,
+    const bool count,
+    const bool addSize)
+{
+    unsigned int res = 0;
+
+    for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
+        unsigned int size = 0;
+
+        PAUSE();
+        if (abort) break;
+
+        if (it->isDir()) {
+            size += calculateFileSize(listDirFiles(it->absoluteFilePath()), count, addSize);
+        }
+
+        if (addSize) {
+            if (it->isDir()) {
+                ++size;
+            } else {
+                size += ceil(static_cast<float>(it->size()) / BLOCK_SIZE);
+            }
+            fileSizeMap[it->absoluteFilePath()] = size;
+        }
+
+        if (count) {
+            ++size;
+        }
+
+        res += size;
+    }
+
+    return res;
+}
+
+
+QFileInfoList OperationThread::listDirFiles(const QString &dirPath) {
+    QDir dir = dirPath;
+    return dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden);
+}
+
+
+void OperationThread::setTotalSize(unsigned int size) {
+    totalSize = size;
+    emit totalSizeChanged(size);
+}
+
+
+void OperationThread::updateProgress(int value) {
+    totalValue += value;
+    fileValue += value;
+    emit progressUpdate(value);
+}
+
+
+void OperationThread::updateFile(const QString &name) {
+    fileValue = 0;
+    emit fileNameUpdated(shortenPath(name));
+}
+
+
+void OperationThread::waitOnCond() {
+    time_t waitTime = time(0);
+    waitCond.wait(&mutex);
+    emit operationResumed(time(0) - waitTime);
+}
+
+
+bool OperationThread::checkSequentialFile(const QFSFileEngine &engine) {
+    errno = 0;
+    if (engine.isSequential()) {
+        if (!errno) errno = 255;
+        return true;
+    }
+
+    return false;
+}
+
+
+void OperationThread::wake() {
+    pause = false;
+    waitCond.wakeAll();
+}
+
+
+void DeleteThread::run() {
+    mutex.lock();
+
+    setTotalSize(calculateFileSize(files, true));
+    emit operationStarted(time(0));
+
+    processFiles(files);
+
+    sleep(0.5);
+    emit finished(this);
+}
+
+
+void DeleteThread::perform(const QFileInfo &file) {
+    remove(file, true);
+}
+
+
+void CopyThread::run() {
+    mutex.lock();
+
+    setTotalSize(calculateFileSize(files, false, true));
+    emit operationStarted(time(0));
+
+    processFiles(files);
+
+    sleep(0.5);
+    emit finished(this);
+}
+
+
+void CopyThread::perform(const QFileInfo &file) {
+    copy(file);
+}
+
+
+void MoveThread::run() {
+    mutex.lock();
+
+    rename(files, dest);
+
+    sleep(0.5);
+    emit finished(this);
+}
+
+
+void MoveThread::rename(const QFileInfoList &files, const QDir &dest) {
+    setTotalSize(totalSize + files.size());
+    emit operationStarted(time(0));
+
+    for (int i = 0; i < files.size(); ++i) {
+        QString path = files[i].absoluteFilePath();
+        QFSFileEngine engine(path);
+        QFileInfo newFile(dest.absolutePath() + "/" + files[i].fileName());
+
+        updateFile(path);
+
+        OVERWRITE_PROMPT(files[i], newFile)
+
+        // if we are owerwriting dir over a dir, we will get SKIP_DIR
+        // as a response from OVERWRITE_PROMT meaning we should skip it
+        // (KEEP would mean to keep the files inside)
+        if (files[i].isDir() && newFile.exists() && newFile.isDir()) {
+            if (response == SKIP_DIR) {
+                PAUSE();
+                if (abort) break;
+                updateProgress(1);
+                removeExcludeFiles.insert(path);
+                continue;
+            }
+        } else {
+            if (response == KEEP) {
+                PAUSE();
+                if (abort) break;
+                updateProgress(1);
+                removeExcludeFiles.insert(path);
+                continue;
+            }
+        }
+
+        QString newPath(newFile.absoluteFilePath());
+        QFSFileEngine newEngine(newPath);
+
+        bool done = false;
+
+        while (!abort && !engine.rename(newPath)) {
+            // source and target are on different partitions
+            // this should happen on the first file, unless some are skipped by overwrite prompt
+            // we calculate the actual file sizes, because from now on copy & remove takes over
+            if (errno == EXDEV) {
+                overwriteAll = response;
+                // hack: we already checked the first file we are sending to processFiles(...)
+                // so we don't want to ask about this one again
+                if (overwriteAll == NONE) overwriteAll = DONT_ASK_ONCE;
+
+                QFileInfoList remainingFiles = files.mid(i);
+
+                setTotalSize(totalValue + calculateFileSize(remainingFiles, true, true));
+
+                processFiles(remainingFiles);
+
+                emit removeAfterCopy();
+
+                remove(remainingFiles, true);
+
+                done = true;
+                break;
+            // the target is nonempty dir. lets call this recursively and rename the contents one by one
+            } else if (errno == ENOTEMPTY || errno == EEXIST) {
+                Response tmpResp = overwriteAll;
+                overwriteAll = response;
+
+                rename(listDirFiles(path), QDir(newPath));
+                PAUSE();
+                if (abort) break;
+
+                overwriteAll = tmpResp;
+
+                remove(files[i]);
+
+                break;
+            // source and target are nonmatching types(file and dir)
+            // remove the target and let it loop once again
+            } else if (errno == ENOTDIR || errno == EISDIR) {
+                if (!remove(newPath)) break;
+            } else {
+                SHOW_ERROR_PROMPT(tr("Error moving %1."), path)
+
+                if (response == IGNORE) {
+                    break;
+                }
+            }
+            PAUSE();
+        }
+
+        if (done) break;
+
+        PAUSE();
+        if (abort) break;
+        updateProgress(1);
+    }
+}
+
+
+void MoveThread::perform(const QFileInfo &file) {
+    copy(file);
+}