2aa82656fd005dae1eb6b1eee03a57e736cfdeca
[case] / src / fileoperator.cpp
1 // case - file manager for N900
2 // Copyright (C) 2010 Lukas Hrazky
3 // 
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.
8 // 
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.
13 // 
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/>.
16
17
18 #include "fileoperator.h"
19
20 #include <QtGui>
21 #include <QDir>
22 #include <QMessageBox>
23 #include <QHBoxLayout>
24 #include <QChar>
25
26 #include <math.h>
27 #include <errno.h>
28 #include <iostream>
29
30
31 #define SHOW_ERROR_PROMPT(promptString)                                                 \
32     response = FileOperator::NONE;                                                      \
33     if (ignoreAll[errno]) {                                                             \
34         response = FileOperator::IGNORE;                                                \
35     } else {                                                                            \
36         char buf[255];                                                                  \
37         char *realBuf = strerror_r(errno, buf, 255);                                    \
38         emit showErrorPrompt(this, promptString + ". " + realBuf + ".", errno);         \
39         waitCond.wait(&mutex);                                                          \
40     }
41
42
43 #define ERROR_PROMPT(operation, promptString)                                           \
44 {                                                                                       \
45     response = FileOperator::NONE;                                                      \
46     while (!abort && operation) {                                                       \
47         SHOW_ERROR_PROMPT(promptString)                                                 \
48         if (response == FileOperator::IGNORE) {                                         \
49             break;                                                                      \
50         }                                                                               \
51     }                                                                                   \
52 }
53
54
55 #define ERROR_PROMPT_XP(operation, promptString, onIgnore, quitCmd)                     \
56 {                                                                                       \
57     ERROR_PROMPT(operation, promptString)                                               \
58     if (abort || response == FileOperator::IGNORE) {                                    \
59         if (!abort) onIgnore;                                                           \
60         quitCmd;                                                                        \
61     }                                                                                   \
62 }
63
64
65 #define OVERWRITE_PROMPT(file, newFile)                                                 \
66 {                                                                                       \
67     response = FileOperator::NONE;                                                      \
68                                                                                         \
69     if (newFile.exists()) {                                                             \
70         if (overwriteAll != FileOperator::NONE) {                                       \
71             response = overwriteAll;                                                    \
72         } else {                                                                        \
73             bool dirOverDir = false;                                                    \
74             if (newFile.isDir() && file.isDir()) dirOverDir = true;                     \
75             emit showOverwritePrompt(this, newFile.absoluteFilePath(), dirOverDir);     \
76             waitCond.wait(&mutex);                                                      \
77         }                                                                               \
78     }                                                                                   \
79 }
80
81
82 FileOperator::FileOperator(QWidget *parent) : QWidget(parent) {
83     QHBoxLayout *layout = new QHBoxLayout;
84     layout->setContentsMargins(0, 0, 0, 0);
85     layout->setSpacing(0);
86     setLayout(layout);
87 }
88
89
90 void FileOperator::deleteFiles(const QFileInfoList &files) {
91     QString title, desc;
92     if (files.size() == 1) {
93         title = tr("Delete file");
94         desc = tr("Are you sure you want to delete %1?").arg(files[0].absoluteFilePath());
95     } else {
96         title = tr("Delete files");
97         desc = tr("You are about to delete %1 files. Are you sure you want to continue?").arg(files.size());
98     }
99
100     int confirm = QMessageBox::warning(
101         0,
102         title,
103         desc,
104         QMessageBox::Yes,
105         QMessageBox::No
106     );
107
108     if(confirm == QMessageBox::Yes) {
109         caterNewThread(new DeleteThread(files));
110     }
111 }
112
113
114 void FileOperator::copyFiles(const QFileInfoList &files, QDir &destination) {
115     QString title, desc;
116     if (files.size() == 1) {
117         title = tr("Copy file");
118         desc = tr("Are you sure you want to copy %1 to %2?").arg(files[0].absoluteFilePath())
119             .arg(destination.absolutePath());
120     } else {
121         title = tr("Copy files");
122         desc = tr("You are about to copy %1 files to %2. Are you sure you want to continue?")
123             .arg(files.size()).arg(destination.absolutePath());
124     }
125
126     int confirm = QMessageBox::warning(
127         0,
128         title,
129         desc,
130         QMessageBox::Yes,
131         QMessageBox::No
132     );
133
134     if(confirm == QMessageBox::Yes) {
135         caterNewThread(new CopyThread(files, destination));
136     }
137 }
138
139
140 void FileOperator::moveFiles(const QFileInfoList &files, QDir &destination) {
141     // for move we don't wanna move to the same dir
142     if (files[0].absolutePath() == destination.absolutePath()) return;
143
144     QString title, desc;
145     if (files.size() == 1) {
146         title = tr("Move file");
147         desc = tr("Are you sure you want to move %1 to %2?").arg(files[0].absoluteFilePath())
148             .arg(destination.absolutePath());
149     } else {
150         title = tr("Move files");
151         desc = tr("You are about to move %1 files to %2. Are you sure you want to continue?")
152             .arg(files.size()).arg(destination.absolutePath());
153     }
154
155     int confirm = QMessageBox::warning(
156         0,
157         title,
158         desc,
159         QMessageBox::Yes,
160         QMessageBox::No
161     );
162
163     if(confirm == QMessageBox::Yes) {
164         caterNewThread(new MoveThread(files, destination));
165     }
166 }
167
168
169 void FileOperator::showErrorPrompt(FileManipulatorThread* manipulator, const QString &message, const int err) {
170     QMessageBox msgBox;
171     msgBox.addButton(QMessageBox::Cancel);
172     QAbstractButton *abortButton = msgBox.addButton(tr("Abort"), QMessageBox::DestructiveRole);
173     QAbstractButton *retryButton = msgBox.addButton(QMessageBox::Retry);
174     QAbstractButton *ignoreButton = msgBox.addButton(QMessageBox::Ignore);
175     QAbstractButton *ignoreAllButton = msgBox.addButton(tr("Ignore All"), QMessageBox::AcceptRole);
176     msgBox.setText(message);
177
178     msgBox.exec();
179
180     if (msgBox.clickedButton() == abortButton) {
181         manipulator->setResponse(ABORT);
182     } else if (msgBox.clickedButton() == retryButton) {
183         manipulator->setResponse(RETRY);
184     } else if (msgBox.clickedButton() == ignoreButton) {
185         manipulator->setResponse(IGNORE);
186     } else if (msgBox.clickedButton() == ignoreAllButton) {
187         manipulator->setResponse(IGNORE, true, err);
188     }
189 }
190
191
192 void FileOperator::showOverwritePrompt(
193     FileManipulatorThread* manipulator,
194     const QString &fileName,
195     const bool dirOverDir)
196 {
197     QMessageBox msgBox;
198     msgBox.addButton(QMessageBox::Cancel);
199     QAbstractButton *yesButton = msgBox.addButton(QMessageBox::Yes);
200     QAbstractButton *yesToAllButton = msgBox.addButton(QMessageBox::YesToAll);
201     QAbstractButton *noButton = msgBox.addButton(QMessageBox::No);
202     QAbstractButton *noToAllButton = msgBox.addButton(QMessageBox::NoToAll);
203     QAbstractButton *abortButton = msgBox.addButton(tr("Abort"), QMessageBox::DestructiveRole);
204     QAbstractButton *askButton = 0;
205
206     if (dirOverDir) {
207         msgBox.setText(tr("Directory %1 already exists. Overwrite the files inside?").arg(fileName));
208         askButton = msgBox.addButton(tr("Ask"), QMessageBox::AcceptRole);
209     } else {
210         msgBox.setText(tr("File %1 already exists. Overwrite?").arg(fileName));
211     }
212
213     msgBox.exec();
214
215     if (msgBox.clickedButton() == abortButton) {
216         manipulator->setResponse(ABORT);
217     } else if (msgBox.clickedButton() == yesButton) {
218         manipulator->setResponse(OVERWRITE);
219     } else if (msgBox.clickedButton() == yesToAllButton) {
220         manipulator->setResponse(OVERWRITE, true);
221     } else if (msgBox.clickedButton() == noButton) {
222         manipulator->setResponse(KEEP);
223     } else if (msgBox.clickedButton() == noToAllButton) {
224         manipulator->setResponse(KEEP, true);
225     } else if (msgBox.clickedButton() == askButton) {
226         manipulator->setResponse(NONE, true);
227     }
228 }
229
230
231 void FileOperator::remove(FileManipulatorThread* manipulator) {
232     layout()->removeWidget(manipulator->widget);
233     manipulatorList.removeAll(manipulator);
234     delete manipulator;
235 }
236
237
238 void FileOperator::setBarSize(FileManipulatorThread* manipulator, unsigned int size) {
239     manipulator->widget->setMinimum(0);
240     manipulator->widget->setMaximum(size);
241 }
242
243
244 void FileOperator::updateProgress(FileManipulatorThread* manipulator, int value) {
245     if (manipulator->widget->value() + value > manipulator->widget->maximum()) {
246         std::cout << "WARNING, EXCEEDING MAXIMUM BY " << value << std::endl;
247     }
248     manipulator->widget->setValue(manipulator->widget->value() + value);
249 }
250
251
252 void FileOperator::caterNewThread(FileManipulatorThread *thread) {
253     manipulatorList.append(thread);
254
255     connect(thread, SIGNAL(showErrorPrompt(FileManipulatorThread*, const QString&, const int)),
256         this, SLOT(showErrorPrompt(FileManipulatorThread*, const QString&, const int)));
257     connect(thread, SIGNAL(showOverwritePrompt(FileManipulatorThread*, const QString&, bool)),
258         this, SLOT(showOverwritePrompt(FileManipulatorThread*, const QString&, bool)));
259     connect(thread, SIGNAL(finished(FileManipulatorThread*)),
260         this, SLOT(remove(FileManipulatorThread*)));
261     connect(thread, SIGNAL(setBarSize(FileManipulatorThread*, unsigned int)),
262         this, SLOT(setBarSize(FileManipulatorThread*, unsigned int)));
263     connect(thread, SIGNAL(updateProgress(FileManipulatorThread*, int)),
264         this, SLOT(updateProgress(FileManipulatorThread*, int)));
265
266     thread->widget->setValue(0);
267
268     layout()->addWidget(thread->widget);
269     thread->start();
270 }
271
272
273 FileManipulatorThread::FileManipulatorThread(const QFileInfoList files, QDir dest) :
274     widget(new QProgressBar()),
275     files(files),
276     dest(dest),
277     response(FileOperator::NONE),
278     overwriteAll(FileOperator::NONE),
279     abort(false),
280     barSize(0),
281     barValue(0),
282     fileSize(0),
283     fileValue(0)
284 {
285     memset(ignoreAll, false, sizeof(ignoreAll));
286     //widget->setStyle(new QPlastiqueStyle);
287 }
288
289
290 void FileManipulatorThread::setResponse(
291     const FileOperator::Response response,
292     const bool applyToAll,
293     const int err)
294 {
295     mutex.lock();
296
297     this->response = response;
298
299     if (applyToAll) {
300         if (response == FileOperator::KEEP
301             || response == FileOperator::OVERWRITE
302             || response == FileOperator::NONE)
303         {
304             overwriteAll = response;
305         }
306
307         if (response == FileOperator::IGNORE) {
308             ignoreAll[err] = true;
309         }
310     }
311
312     if (response == FileOperator::ABORT) abort = true;
313
314     mutex.unlock();
315     waitCond.wakeAll();
316 }
317
318
319 void FileManipulatorThread::processFiles(const QFileInfoList &files) {
320     for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
321         perform(*it);
322         if (abort) break;
323     }
324 }
325
326
327 bool FileManipulatorThread::remove(QString &fileName, const bool ignoreDirNotEmpty) {
328     return remove(QFileInfo(fileName), ignoreDirNotEmpty);
329 }
330
331
332 bool FileManipulatorThread::remove(const QFileInfoList &files, const bool ignoreDirNotEmpty) {
333     bool res = true;
334     for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
335         if (!remove(*it, ignoreDirNotEmpty)) res = false;
336         if (abort) break;
337     }
338     return res;
339 }
340
341
342 bool FileManipulatorThread::remove(const QFileInfo &file, const bool ignoreDirNotEmpty) {
343     QString path = file.absoluteFilePath();
344     QFSFileEngine engine(path);
345
346     if (file.isDir()) {
347         QFileInfoList list = listDirFiles(path);
348
349         if (ignoreDirNotEmpty && list.size()) return true;
350
351         if (!remove(list, ignoreDirNotEmpty)) return false;
352
353         ERROR_PROMPT(!engine.rmdir(path, false),
354             tr("Error deleting directory %1.").arg(path))
355     } else {
356         ERROR_PROMPT(!engine.remove(),
357             tr("Error deleting file %1.").arg(path))
358     }
359
360     if (abort || response == FileOperator::IGNORE) return false;
361     return true;
362 }
363
364
365 void FileManipulatorThread::copy(const QFileInfo &file, const bool removeAfterCopy) {
366     std::cout << (removeAfterCopy ? "MOVING " : "COPYING ") << file.absoluteFilePath().toStdString()
367         << " to " << dest.absolutePath().toStdString() << std::endl;
368
369     QString path(file.absoluteFilePath());
370     QString newPath(dest.absolutePath() + "/" + file.fileName());
371     QFSFileEngine engine(path);
372     QFSFileEngine newEngine(newPath);
373     QFileInfo newFile(newPath);
374
375     updateFile(path);
376
377     // hack to prevent asking about the same file if we already asked in the rename(...) function
378     if (overwriteAll == FileOperator::DONT_ASK_ONCE) {
379         overwriteAll = FileOperator::NONE;
380     } else {
381         OVERWRITE_PROMPT(file, newFile)
382     }
383
384     if (abort) return;
385
386     // this loop is here only to allow easily breaking out to the end (and remove the source file/dir)
387     while (1) {
388         if (response == FileOperator::KEEP) {
389             updateProgress(fileSizeMap[path]);
390             break;
391         }
392
393         FileOperator::Response overwriteResponse = response;
394
395         if (file.isDir()) {
396             if (newFile.exists() && !newFile.isDir()) {
397                 if(!remove(newPath)) {
398                     updateProgress(fileSizeMap[path]);
399                     break;
400                 }
401                 newFile = QFileInfo(newPath);
402             }
403
404             if (!newFile.exists()) {
405                 ERROR_PROMPT_XP(!engine.mkdir(newPath, false),
406                     tr("Error creating directory %1.").arg(newPath),
407                     updateProgress(fileSizeMap[path]),
408                     break)
409             }
410
411             updateProgress(1);
412             
413             QDir destBackup = dest;
414             dest = newPath;
415
416             FileOperator::Response tmpResp = overwriteAll;
417             overwriteAll = overwriteResponse;
418
419             processFiles(listDirFiles(path));
420
421             overwriteAll = tmpResp;
422
423             ERROR_PROMPT(!newEngine.setPermissions(file.permissions()),
424                 tr("Error setting permissions for directory %1.").arg(newPath))
425
426             if (abort) return;
427
428             dest = destBackup;
429         } else {
430             ERROR_PROMPT_XP(engine.isSequential(),
431                 tr("Cannot copy sequential file %1.").arg(path),
432                 updateProgress(fileSizeMap[path]),
433                 break)
434
435             if (newFile.exists() && newFile.isDir()) {
436                 ERROR_PROMPT_XP(!remove(newPath),
437                     tr("Cannot replace directory %1 due to previous errors.").arg(newPath),
438                     updateProgress(fileSizeMap[path]),
439                     break)
440             }
441
442             ERROR_PROMPT_XP(!engine.open(QIODevice::ReadOnly),
443                 tr("Error reading file %1.").arg(path),
444                 updateProgress(fileSizeMap[path]),
445                 break)
446
447             bool ignore = false;
448             while (!abort && !ignore) {
449                 engine.seek(0);
450
451                 ERROR_PROMPT(!newEngine.open(QIODevice::WriteOnly | QIODevice::Truncate),
452                     tr("Error writing file %1.").arg(newPath))
453
454                 if (abort || response == FileOperator::IGNORE) {
455                     if (response == FileOperator::IGNORE) {
456                         updateProgress(fileSizeMap[path] - fileValue);
457                         ignore = true;
458                     }
459                     break;
460                 }
461
462                 bool error = false;
463                 char block[4096];
464                 qint64 bytes;
465                 while ((bytes = engine.read(block, sizeof(block))) > 0) {
466                     if (bytes == -1 || bytes != newEngine.write(block, bytes)) {
467                         if (bytes == -1) {
468                             SHOW_ERROR_PROMPT(tr("Error while reading from file %1.").arg(path));
469                         } else {
470                             SHOW_ERROR_PROMPT(tr("Error while writing to file %1.").arg(newPath));
471                         }
472
473                         if (!abort) {
474                             if (response == FileOperator::IGNORE) {
475                                 updateProgress(fileSizeMap[path] - fileValue);
476                                 ignore = true;
477                             } else {
478                                 updateProgress(-fileValue);
479                             }
480                         }
481                         error = true;
482                         break;
483                     }
484
485                     updateProgress(1);
486                 }
487
488                 if (!error) break;
489             }
490
491             engine.close();
492             newEngine.close();
493
494             if (abort || ignore) {
495                 newEngine.remove();
496             } else {
497                 ERROR_PROMPT(!newEngine.setPermissions(file.permissions()),
498                     tr("Error setting permissions for file %1.").arg(newPath))
499             }
500         }
501
502         break;
503     }
504
505     if (removeAfterCopy && !abort) remove(path, true);
506 }
507
508
509 unsigned int FileManipulatorThread::countFiles(const QFileInfoList &files) {
510     unsigned int res = 0;
511
512     for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
513         unsigned int size = 1;
514
515         if (it->isDir()) {
516             size += countFiles(listDirFiles(it->absoluteFilePath()));
517         }
518
519         res += size;
520         fileSizeMap[it->absoluteFilePath()] = size;
521     }
522
523     return res;
524 }
525
526
527 unsigned int FileManipulatorThread::calculateFileSize(const QFileInfoList &files) {
528     unsigned int res = 0;
529
530     for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
531         unsigned int size = 1;
532
533         if (it->isDir()) {
534             size += calculateFileSize(listDirFiles(it->absoluteFilePath()));
535         } else {
536             size = ceil(static_cast<float>(it->size()) / 4096);
537         }
538
539         res += size;
540         fileSizeMap[it->absoluteFilePath()] = size;
541     }
542
543     return res;
544 }
545
546
547 QFileInfoList FileManipulatorThread::listDirFiles(const QString &dirPath) {
548     QDir dir = dirPath;
549     return dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden);
550 }
551
552
553 void FileManipulatorThread::setBarSize(unsigned int size) {
554     barSize = size;
555     emit setBarSize(this, size);
556 }
557
558
559 void FileManipulatorThread::updateProgress(int value) {
560     barValue += value;
561     fileValue += value;
562     emit updateProgress(this, value);
563 }
564
565
566 void FileManipulatorThread::updateFile(const QString &fileName) {
567     fileValue = 0;
568     emit updateFile(this, fileName);
569 }
570
571
572 void DeleteThread::run() {
573     mutex.lock();
574
575     setBarSize(countFiles(files));
576
577     processFiles(files);
578
579     sleep(0.5);
580     emit finished(this);
581 }
582
583
584 void DeleteThread::perform(const QFileInfo &file) {
585     std::cout << "DELETING " << file.absoluteFilePath().toStdString() << std::endl;
586
587     QString path = file.absoluteFilePath();
588     QFSFileEngine engine(path);
589
590     if (file.isDir()) {
591         processFiles(listDirFiles(path));
592
593         if (!listDirFiles(path).size()) {
594             ERROR_PROMPT(!engine.rmdir(path, false),
595                 tr("Error deleting directory %1.").arg(path))
596         }
597     } else {
598         ERROR_PROMPT(!engine.remove(),
599             tr("Error deleting file %1.").arg(path))
600     }
601
602     if (!abort) updateProgress(1);
603 }
604
605
606 void CopyThread::run() {
607     mutex.lock();
608
609     setBarSize(calculateFileSize(files));
610
611     processFiles(files);
612
613     sleep(0.5);
614     emit finished(this);
615 }
616
617
618 void CopyThread::perform(const QFileInfo &file) {
619     copy(file, false);
620 }
621
622
623 void MoveThread::run() {
624     mutex.lock();
625
626     rename(files, dest);
627
628     sleep(0.5);
629     emit finished(this);
630 }
631
632
633 void MoveThread::rename(const QFileInfoList &files, const QDir &dest) {
634     setBarSize(barSize + files.size());
635
636     for (int i = 0; i < files.size(); ++i) {
637         QString path = files[i].absoluteFilePath();
638         QFSFileEngine engine(path);
639         QString newPath = dest.absolutePath() + "/" + files[i].fileName();
640
641         updateFile(path);
642
643         OVERWRITE_PROMPT(files[i], QFileInfo(newPath))
644
645         if (response == FileOperator::KEEP) {
646             remove(path);
647             if (abort) break;
648             updateProgress(1);
649             continue;
650         }
651
652         while (!abort && !engine.rename(newPath)) {
653             // source and target are on different partitions
654             // this should happen on the first file, unless some are skipped by overwrite prompt
655             // we calculate the actual file sizes, because from now on copy & remove takes over
656             if (errno == EXDEV) {
657                 setBarSize(barValue + calculateFileSize(files));
658
659                 FileOperator::Response tmpResp = overwriteAll;
660                 overwriteAll = response;
661                 // hack: we already checked the first file we are sending to processFiles(...)
662                 // so we don't want to ask about this one again
663                 if (overwriteAll == FileOperator::NONE) overwriteAll = FileOperator::DONT_ASK_ONCE;
664
665                 processFiles(files.mid(i));
666
667                 overwriteAll = tmpResp;
668
669                 // just to quit the loops, we are done
670                 abort = true;
671             // the target is nonempty dir. lets call this recursively and rename the contents one by one
672             } else if (errno == ENOTEMPTY || errno == EEXIST) {
673                 FileOperator::Response tmpResp = overwriteAll;
674                 overwriteAll = response;
675
676                 rename(listDirFiles(path), QDir(newPath));
677                 if (abort) break;
678
679                 overwriteAll = tmpResp;
680
681                 ERROR_PROMPT(!engine.rmdir(path, false), tr("Error deleting directory %1.").arg(path))
682
683                 break;
684             // source and target are nonmatching types(file and dir)
685             // remove the target and let it loop once again
686             } else if (errno == ENOTDIR || errno == EISDIR) {
687                 if (!remove(newPath)) break;
688             } else {
689                 SHOW_ERROR_PROMPT(tr("Error moving %1.").arg(path))
690
691                 if (response == FileOperator::IGNORE) {
692                     break;
693                 }
694             }
695         }
696             
697         if (abort) break;
698         updateProgress(1);
699     }
700 }
701
702
703 void MoveThread::perform(const QFileInfo &file) {
704     copy(file, true);
705 }