1 // case - file manager for N900
2 // Copyright (C) 2010 Lukas Hrazky <lukkash@email.cz>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
18 #include "fileoperator.h"
22 #include <QMessageBox>
23 #include <QHBoxLayout>
33 #define BLOCK_SIZE 524288
36 #define SHOW_ERROR_PROMPT(promptString, fileName) \
37 response = FileOperator::NONE; \
38 if (ignoreAll[errno]) { \
39 response = FileOperator::IGNORE; \
42 char *realBuf = strerror_r(errno, buf, 255); \
43 emit showErrorPrompt(this, promptString + " " + realBuf + ".", fileName, errno); \
44 waitCond.wait(&mutex); \
48 #define ERROR_PROMPT(operation, promptString, fileName) \
50 response = FileOperator::NONE; \
51 while (!abort && operation) { \
52 SHOW_ERROR_PROMPT(promptString, fileName) \
53 if (response == FileOperator::IGNORE) { \
60 #define SPECIAL_COPY_ERROR_PROMPT(operation, promptString, fileName) \
62 ERROR_PROMPT(operation, promptString, fileName) \
63 if (abort || response == FileOperator::IGNORE) { \
65 updateProgress(fileSizeMap[path]); \
66 removeExcludeFiles.insert(path); \
73 #define OVERWRITE_PROMPT(file, newFile) \
75 response = FileOperator::NONE; \
77 while (response == FileOperator::NONE && newFile.exists()) { \
78 if (overwriteAll != FileOperator::NONE) { \
79 response = overwriteAll; \
81 emit showOverwritePrompt(this, newFile.absoluteFilePath(), \
82 newFile.isDir() && file.isDir()); \
83 waitCond.wait(&mutex); \
85 if (response == FileOperator::NONE) { \
86 emit showInputFilenamePrompt(this, newFile, file.isDir()); \
87 waitCond.wait(&mutex); \
88 if (newNameFromDialog.size()) { \
89 newFile.setFile(newNameFromDialog); \
94 if (response == FileOperator::ASK) response = FileOperator::NONE; \
98 FileOperator::FileOperator(QWidget *parent) : QWidget(parent) {
99 QHBoxLayout *layout = new QHBoxLayout;
100 layout->setContentsMargins(0, 0, 0, 0);
101 layout->setSpacing(0);
103 qRegisterMetaType<QFileInfo>("QFileInfo");
107 QString FileOperator::shortenPath(const QString &path) {
108 QString homePath = QFSFileEngine::homePath();
110 if (path.indexOf(homePath, 0) == 0) {
111 QString result = path;
113 result.replace(0, homePath.size(), "~");
121 QString FileOperator::unwindPath(const QString &path) {
122 QString result = path;
123 // if ~ is the first character and / or nothing follows it, replace with home dir
124 if (path == "~" || path.indexOf("~/", 0) == 0) {
125 QString homePath = QFSFileEngine::homePath();
126 result.replace(0, 1, homePath);
127 // in case someone wants to enter a dir called ~ in the current dir, he can escape it with \~
128 } else if (path == "\\~" || path.indexOf("\\~/", 0) == 0) {
129 result.replace(0, 2, "~");
136 void FileOperator::deleteFiles(const QFileInfoList &files) {
138 if (files.size() == 1) {
139 title = tr("Delete file");
140 desc = tr("Are you sure you want to delete %1?")
141 .arg(FileOperator::shortenPath(files[0].absoluteFilePath()));
143 title = tr("Delete files");
144 desc = tr("You are about to delete %1 files. Are you sure you want to continue?").arg(files.size());
147 int confirm = QMessageBox::warning(
155 if(confirm == QMessageBox::Yes) {
156 caterNewThread(new DeleteThread(files));
161 void FileOperator::copyFiles(const QFileInfoList &files, QDir &destination) {
163 if (files.size() == 1) {
164 title = tr("Copy file");
165 desc = tr("Are you sure you want to copy %1 to %2?")
166 .arg(FileOperator::shortenPath(files[0].absoluteFilePath()))
167 .arg(FileOperator::shortenPath(destination.absolutePath()));
169 title = tr("Copy files");
170 desc = tr("You are about to copy %1 files to %2. Are you sure you want to continue?")
171 .arg(files.size()).arg(FileOperator::shortenPath(destination.absolutePath()));
174 int confirm = QMessageBox::warning(
182 if(confirm == QMessageBox::Yes) {
183 caterNewThread(new CopyThread(files, destination));
188 void FileOperator::moveFiles(const QFileInfoList &files, QDir &destination) {
189 // for move we don't wanna move to the same dir
190 if (files[0].absolutePath() == destination.absolutePath()) return;
193 if (files.size() == 1) {
194 title = tr("Move file");
195 desc = tr("Are you sure you want to move %1 to %2?")
196 .arg(FileOperator::shortenPath(files[0].absoluteFilePath()))
197 .arg(FileOperator::shortenPath(destination.absolutePath()));
199 title = tr("Move files");
200 desc = tr("You are about to move %1 files to %2. Are you sure you want to continue?")
201 .arg(files.size()).arg(FileOperator::shortenPath(destination.absolutePath()));
204 int confirm = QMessageBox::warning(
212 if(confirm == QMessageBox::Yes) {
213 caterNewThread(new MoveThread(files, destination));
218 void FileOperator::showErrorPrompt(FileManipulatorThread* manipulator,
219 const QString &message,
220 const QString &fileName,
224 msgBox.addButton(QMessageBox::Cancel);
225 QAbstractButton *abortButton = msgBox.addButton(tr("Abort"), QMessageBox::DestructiveRole);
226 QAbstractButton *retryButton = msgBox.addButton(QMessageBox::Retry);
227 QAbstractButton *ignoreButton = msgBox.addButton(QMessageBox::Ignore);
228 QAbstractButton *ignoreAllButton = msgBox.addButton(tr("Ignore All"), QMessageBox::AcceptRole);
229 msgBox.setText(message.arg(FileOperator::shortenPath(fileName)));
233 if (msgBox.clickedButton() == abortButton) {
234 manipulator->setResponse(ABORT);
235 } else if (msgBox.clickedButton() == retryButton) {
236 manipulator->setResponse(RETRY);
237 } else if (msgBox.clickedButton() == ignoreButton) {
238 manipulator->setResponse(IGNORE);
239 } else if (msgBox.clickedButton() == ignoreAllButton) {
240 manipulator->setResponse(IGNORE, true, err);
245 void FileOperator::showOverwritePrompt(
246 FileManipulatorThread* manipulator,
247 const QString &fileName,
248 const bool dirOverDir)
251 msgBox.addButtonFirst(QDialogButtonBox::Cancel);
252 QAbstractButton *yesButton = msgBox.addButtonFirst(QDialogButtonBox::Yes);
253 QAbstractButton *yesToAllButton = msgBox.addButtonFirst(QDialogButtonBox::YesToAll);
254 QAbstractButton *noButton = msgBox.addButtonSecond(QDialogButtonBox::No);
255 QAbstractButton *noToAllButton = msgBox.addButtonSecond(QDialogButtonBox::NoToAll);
256 QAbstractButton *abortButton = msgBox.addButtonSecond(tr("Abort"), QDialogButtonBox::DestructiveRole);
257 QAbstractButton *newNameButton = msgBox.addButtonFirst(tr("New Name"), QDialogButtonBox::AcceptRole);
258 QAbstractButton *askButton = 0;
259 QAbstractButton *skipDirButton = 0;
262 msgBox.setText(tr("Directory %1 already exists. Overwrite the files inside?")
263 .arg(FileOperator::shortenPath(fileName)));
264 askButton = msgBox.addButtonFirst(tr("Ask"), QDialogButtonBox::AcceptRole);
265 skipDirButton = msgBox.addButtonSecond(tr("Skip"), QDialogButtonBox::NoRole);
267 msgBox.setText(tr("File %1 already exists. Overwrite?").arg(FileOperator::shortenPath(fileName)));
272 if (msgBox.clickedButton == abortButton) {
273 manipulator->setResponse(ABORT);
274 } else if (msgBox.clickedButton == yesButton) {
275 manipulator->setResponse(OVERWRITE);
276 } else if (msgBox.clickedButton == yesToAllButton) {
277 manipulator->setResponse(OVERWRITE, true);
278 } else if (msgBox.clickedButton == noButton) {
279 manipulator->setResponse(KEEP);
280 } else if (msgBox.clickedButton == noToAllButton) {
281 manipulator->setResponse(KEEP, true);
282 } else if (msgBox.clickedButton == askButton) {
283 manipulator->setResponse(ASK);
284 } else if (msgBox.clickedButton == newNameButton) {
285 manipulator->setResponse(NONE);
286 } else if (msgBox.clickedButton == skipDirButton) {
287 manipulator->setResponse(SKIP_DIR);
292 void FileOperator::showInputFilenamePrompt(FileManipulatorThread* manipulator,
293 const QFileInfo &file,
297 QString prompt, error;
300 prompt = tr("Enter the new directory name.");
302 prompt = tr("Enter the new file name.");
305 manipulator->mutex.lock();
307 manipulator->newNameFromDialog = "";
308 QString text = file.fileName();
312 text = QInputDialog::getText(this, QString(), prompt + error, QLineEdit::Normal, text, &ok);
317 if (text.contains(QRegExp("[\"*/:<>?\\\\|]"))) {
318 error = "<small><br/><font color = 'red'>" + tr("The name cannot contain any of the following characters: ") +
319 "\"*/:<>?\\|</font></small>";
320 } else if (ok && !text.isEmpty()) {
321 QFileInfo info(file.path() + "/" + text);
322 manipulator->newNameFromDialog = info.absoluteFilePath();
327 manipulator->mutex.unlock();
328 manipulator->waitCond.wakeAll();
332 void FileOperator::remove(FileManipulatorThread* manipulator) {
334 layout()->removeWidget(manipulator->progressBar);
335 manipulatorList.removeAll(manipulator);
340 void FileOperator::setBarSize(FileManipulatorThread* manipulator, unsigned int size) {
341 if (!manipulator->progressBar->maximum()) {
342 manipulator->startTime = time(0);
344 manipulator->progressBar->setMinimum(0);
345 manipulator->progressBar->setMaximum(size);
349 void FileOperator::updateProgress(FileManipulatorThread* manipulator, int value) {
350 manipulator->setText(value);
354 void FileOperator::caterNewThread(FileManipulatorThread *thread) {
355 manipulatorList.append(thread);
357 connect(thread, SIGNAL(showErrorPrompt(FileManipulatorThread*, const QString&, const QString&, const int)),
358 this, SLOT(showErrorPrompt(FileManipulatorThread*, const QString&, const QString&, const int)));
359 connect(thread, SIGNAL(showOverwritePrompt(FileManipulatorThread*, const QString&, bool)),
360 this, SLOT(showOverwritePrompt(FileManipulatorThread*, const QString&, bool)));
361 connect(thread, SIGNAL(showInputFilenamePrompt(FileManipulatorThread*, const QFileInfo&, bool)),
362 this, SLOT(showInputFilenamePrompt(FileManipulatorThread*, const QFileInfo&, bool)));
363 connect(thread, SIGNAL(finished(FileManipulatorThread*)),
364 this, SLOT(remove(FileManipulatorThread*)));
365 connect(thread, SIGNAL(setBarSize(FileManipulatorThread*, unsigned int)),
366 this, SLOT(setBarSize(FileManipulatorThread*, unsigned int)));
367 connect(thread, SIGNAL(updateProgress(FileManipulatorThread*, int)),
368 this, SLOT(updateProgress(FileManipulatorThread*, int)));
370 layout()->addWidget(thread->progressBar);
371 thread->start(QThread::LowestPriority);
375 FileManipulatorThread::FileManipulatorThread(const QFileInfoList files, QDir dest) :
376 progressBar(new QProgressBar()),
380 response(FileOperator::NONE),
381 overwriteAll(FileOperator::NONE),
389 memset(ignoreAll, false, sizeof(ignoreAll));
390 progressBar->setMaximum(1);
391 progressBar->setValue(0);
392 progressBar->setMinimum(0);
393 QFont barFont = progressBar->font();
394 barFont.setPointSize(12);
395 progressBar->setFont(barFont);
396 progressBar->setFormat(tr("Gathering information..."));
397 progressBar->setMinimumHeight(44);
398 progressBar->setStyle(new QPlastiqueStyle);
399 //progressBar->setStyle(new QMotifStyle);
403 FileManipulatorThread::~FileManipulatorThread() {
404 if (!abort && progressBar->value() < progressBar->maximum()) {
405 std::cout << "WARNING: deleting a progressbar which's value " << progressBar->value() <<
406 " has not reached maximum of " << progressBar->maximum() << std::endl;
412 void FileManipulatorThread::setResponse(
413 const FileOperator::Response response,
414 const bool applyToAll,
419 this->response = response;
422 if (response == FileOperator::KEEP
423 || response == FileOperator::OVERWRITE
424 || response == FileOperator::NONE)
426 overwriteAll = response;
429 if (response == FileOperator::IGNORE) {
430 ignoreAll[err] = true;
434 if (response == FileOperator::ABORT) abort = true;
441 void FileManipulatorThread::processFiles(const QFileInfoList &files) {
442 for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
449 bool FileManipulatorThread::remove(QString &fileName, const bool doUpdates) {
450 return remove(QFileInfo(fileName), doUpdates);
454 bool FileManipulatorThread::remove(const QFileInfoList &files, const bool doUpdates) {
456 for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
457 if (!remove(*it, doUpdates)) res = false;
464 bool FileManipulatorThread::remove(const QFileInfo &file, const bool doUpdates) {
465 std::cout << "DELETING " << file.absoluteFilePath().toStdString() << std::endl;
467 QString path = file.absoluteFilePath();
469 if (removeExcludeFiles.contains(path)) {
470 if (doUpdates) updateProgress(1);
474 QFSFileEngine engine(path);
476 if (doUpdates) updateFile(path);
479 if (!remove(listDirFiles(path), doUpdates)) {
480 if (doUpdates) updateProgress(1);
484 if (!listDirFiles(path).size()) {
485 ERROR_PROMPT(!engine.rmdir(path, false), tr("Error deleting directory %1."), path)
488 ERROR_PROMPT(!engine.remove(), tr("Error deleting file %1."), path)
491 if (!abort && doUpdates) updateProgress(1);
493 if (abort || response == FileOperator::IGNORE) return false;
498 void FileManipulatorThread::copy(const QFileInfo &file) {
499 std::cout << "COPYING " << file.absoluteFilePath().toStdString()
500 << " to " << dest.absolutePath().toStdString() << std::endl;
502 QString path(file.absoluteFilePath());
503 QFSFileEngine engine(path);
504 QFileInfo newFile(dest.absolutePath() + "/" + file.fileName());
508 // hack to prevent asking about the same file if we already asked in the rename(...) function
509 if (overwriteAll == FileOperator::DONT_ASK_ONCE) {
510 overwriteAll = FileOperator::NONE;
512 OVERWRITE_PROMPT(file, newFile)
515 QString newPath(newFile.absoluteFilePath());
516 QFSFileEngine newEngine(newPath);
521 // save the overwrite response, because the response variable will get ovewritten in remove(...)
522 FileOperator::Response overwriteResponse = response;
524 if (newFile.exists() && !newFile.isDir()) {
525 // overwriting a file, so check for KEEP and handle it
526 if (response == FileOperator::KEEP) {
527 updateProgress(fileSizeMap[path]);
528 removeExcludeFiles.insert(path);
532 // if it should not be kept, remove it and return on failure
533 if(!remove(newPath)) {
534 updateProgress(fileSizeMap[path]);
537 // create new info since we deleted the file - is it needed?
538 newFile = QFileInfo(newPath);
540 // overwriting a directory - response KEEP means to keep the files inside,
541 // SKIP_DIR means to skip the dir completely
542 if (response == FileOperator::SKIP_DIR) {
543 updateProgress(fileSizeMap[path]);
544 removeExcludeFiles.insert(path);
549 if (!newFile.exists()) {
550 SPECIAL_COPY_ERROR_PROMPT(!engine.mkdir(newPath, false),
551 tr("Error creating directory %1."), newPath)
554 // we've done the job with the dir, so update progress and recurse into the dir
557 // change the dest for the recursion
558 QDir destBackup = dest;
561 // and set overwriteAll to the response we got a while ago
562 // because it applies to the files inside the dir
563 FileOperator::Response tmpResp = overwriteAll;
564 overwriteAll = overwriteResponse;
566 processFiles(listDirFiles(path));
568 overwriteAll = tmpResp;
570 ERROR_PROMPT(!newEngine.setPermissions(file.permissions()),
571 tr("Error setting permissions for directory %1."), newPath)
577 if (response == FileOperator::KEEP) {
578 updateProgress(fileSizeMap[path]);
579 removeExcludeFiles.insert(path);
583 SPECIAL_COPY_ERROR_PROMPT(engine.isSequential(), tr("Cannot copy sequential file %1."), path)
585 if (newFile.exists() && newFile.isDir()) {
586 SPECIAL_COPY_ERROR_PROMPT(!remove(newPath),
587 tr("Cannot replace directory %1 due to previous errors."), newPath)
590 SPECIAL_COPY_ERROR_PROMPT(!engine.open(QIODevice::ReadOnly), tr("Error reading file %1."), path)
592 bool ignore = false, newFileWritten = false;
593 while (!abort && !ignore) {
597 ERROR_PROMPT(!newEngine.open(QIODevice::WriteOnly | QIODevice::Truncate),
598 tr("Error writing file %1."), newPath)
600 if (abort || response == FileOperator::IGNORE) {
601 if (response == FileOperator::IGNORE) {
602 updateProgress(fileSizeMap[path]);
603 removeExcludeFiles.insert(path);
609 newFileWritten = true;
612 char block[BLOCK_SIZE];
614 while ((bytes = engine.read(block, sizeof(block))) > 0) {
615 if (bytes == -1 || bytes != newEngine.write(block, bytes)) {
617 SHOW_ERROR_PROMPT(tr("Error while reading from file %1."), path);
619 SHOW_ERROR_PROMPT(tr("Error while writing to file %1."), newPath);
623 if (response == FileOperator::IGNORE) {
624 updateProgress(fileSizeMap[path] - fileValue);
625 removeExcludeFiles.insert(path);
628 updateProgress(-fileValue);
644 if (abort || ignore) {
645 if (newFileWritten) {
649 ERROR_PROMPT(!newEngine.setPermissions(file.permissions()),
650 tr("Error setting permissions for file %1."), newPath)
656 unsigned int FileManipulatorThread::calculateFileSize(const QFileInfoList &files,
660 unsigned int res = 0;
662 for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
663 unsigned int size = 0;
666 size += calculateFileSize(listDirFiles(it->absoluteFilePath()), count, addSize);
673 size += ceil(static_cast<float>(it->size()) / BLOCK_SIZE);
675 fileSizeMap[it->absoluteFilePath()] = size;
689 QFileInfoList FileManipulatorThread::listDirFiles(const QString &dirPath) {
691 return dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden);
695 void FileManipulatorThread::setBarSize(unsigned int size) {
697 emit setBarSize(this, size);
701 void FileManipulatorThread::updateProgress(int value) {
704 emit updateProgress(this, value);
708 void FileManipulatorThread::updateFile(const QString &name) {
710 fileName = FileOperator::shortenPath(name);
711 emit updateProgress(this, 0);
715 void FileManipulatorThread::setText(int value) {
716 if (progressBar->value() + value > progressBar->maximum()) {
717 std::cout << "WARNING: exceeding progressbar maximum (" << progressBar->maximum()
718 << ") by " << value << std::endl;
721 time_t now = time(0);
722 if (lastTimeUpdate < now) {
723 lastTimeUpdate = now;
725 time_t elapsed = now - startTime;
726 time_t remaining = (time_t) ((float) elapsed / barValue * (barSize - barValue));
727 struct tm *ts = gmtime(&remaining);
729 if (remaining < 60) {
730 strftime(timeBuf, sizeof(timeBuf), "%Ss", ts);
731 } else if (remaining < 3600) {
732 strftime(timeBuf, sizeof(timeBuf), "%M:%S", ts);
734 strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", ts);
738 progressBar->setFormat(barText.arg(fileName) + "\n%p% ETA " + timeBuf);
739 progressBar->setValue(progressBar->value() + value);
743 DeleteThread::DeleteThread(const QFileInfoList &files) : FileManipulatorThread(files) {
744 barText = tr("deleting %1");
748 void DeleteThread::run() {
751 setBarSize(calculateFileSize(files, true));
760 void DeleteThread::perform(const QFileInfo &file) {
765 CopyThread::CopyThread(const QFileInfoList &files, QDir &dest) : FileManipulatorThread(files, dest) {
766 barText = tr("copying %1");
770 void CopyThread::run() {
773 setBarSize(calculateFileSize(files, false, true));
782 void CopyThread::perform(const QFileInfo &file) {
787 MoveThread::MoveThread(const QFileInfoList &files, QDir &dest) : FileManipulatorThread(files, dest) {
788 barText = tr("moving %1");
792 void MoveThread::run() {
802 void MoveThread::rename(const QFileInfoList &files, const QDir &dest) {
803 setBarSize(barSize + files.size());
805 for (int i = 0; i < files.size(); ++i) {
806 QString path = files[i].absoluteFilePath();
807 QFSFileEngine engine(path);
808 QFileInfo newFile(dest.absolutePath() + "/" + files[i].fileName());
812 OVERWRITE_PROMPT(files[i], newFile)
814 // if we are owerwriting dir over a dir, we will get SKIP_DIR
815 // as a response from OVERWRITE_PROMT meaning we should skip it
816 // (KEEP would mean to keep the files inside)
817 if (files[i].isDir() && newFile.exists() && newFile.isDir()) {
818 if (response == FileOperator::SKIP_DIR) {
821 removeExcludeFiles.insert(path);
825 if (response == FileOperator::KEEP) {
828 removeExcludeFiles.insert(path);
833 QString newPath(newFile.absoluteFilePath());
834 QFSFileEngine newEngine(newPath);
836 while (!abort && !engine.rename(newPath)) {
837 // source and target are on different partitions
838 // this should happen on the first file, unless some are skipped by overwrite prompt
839 // we calculate the actual file sizes, because from now on copy & remove takes over
840 if (errno == EXDEV) {
841 overwriteAll = response;
842 // hack: we already checked the first file we are sending to processFiles(...)
843 // so we don't want to ask about this one again
844 if (overwriteAll == FileOperator::NONE) overwriteAll = FileOperator::DONT_ASK_ONCE;
846 QFileInfoList remainingFiles = files.mid(i);
848 setBarSize(barValue + calculateFileSize(remainingFiles, true, true));
850 processFiles(remainingFiles);
852 barText = tr("deleting %1");
854 remove(remainingFiles, true);
856 // just to quit the loops, we are done
858 // the target is nonempty dir. lets call this recursively and rename the contents one by one
859 } else if (errno == ENOTEMPTY || errno == EEXIST) {
860 FileOperator::Response tmpResp = overwriteAll;
861 overwriteAll = response;
863 rename(listDirFiles(path), QDir(newPath));
866 overwriteAll = tmpResp;
871 // source and target are nonmatching types(file and dir)
872 // remove the target and let it loop once again
873 } else if (errno == ENOTDIR || errno == EISDIR) {
874 if (!remove(newPath)) break;
876 SHOW_ERROR_PROMPT(tr("Error moving %1."), path)
878 if (response == FileOperator::IGNORE) {
890 void MoveThread::perform(const QFileInfo &file) {