--- /dev/null
+// 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);
+}