d0cf7e15104970a08dc49c72012bcfd4b6eea3cf
[case] / src / operationthread.cpp
1 // case - file manager for N900
2 // Copyright (C) 2010 Lukas Hrazky <lukkash@email.cz>
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 "operationthread.h"
19
20 #include <errno.h>
21 #include <math.h>
22
23 #include "utils.h"
24
25
26 #define BLOCK_SIZE (256 * 1024)
27
28
29 #define PAUSE()                                                                             \
30     if (pause) {                                                                            \
31         emit operationPaused();                                                             \
32         waitOnCond();                                                                       \
33     }
34
35
36 #define SHOW_ERROR_PROMPT(promptString, fileName)                                           \
37     response = NONE;                                                                        \
38     if (ignoreAll[errno]) {                                                                 \
39         response = IGNORE;                                                                  \
40     } else {                                                                                \
41         char buf[255];                                                                      \
42         char *realBuf = buf;                                                                \
43         if (errno == 255) {                                                                 \
44             strcpy(buf, tr("File is sequential").toStdString().c_str());                    \
45         } else {                                                                            \
46             realBuf = strerror_r(errno, buf, 255);                                          \
47         }                                                                                   \
48         emit showErrorPrompt(this, promptString + " " + realBuf + ".", fileName, errno);    \
49         waitOnCond();                                                                       \
50     }
51
52
53 #define ERROR_PROMPT(operation, promptString, fileName)                                     \
54 {                                                                                           \
55     response = NONE;                                                                        \
56     while (!abort && operation) {                                                           \
57         SHOW_ERROR_PROMPT(promptString, fileName)                                           \
58         if (response == IGNORE) {                                                           \
59             break;                                                                          \
60         }                                                                                   \
61         PAUSE()                                                                             \
62     }                                                                                       \
63 }
64
65
66 #define SPECIAL_COPY_ERROR_PROMPT(operation, promptString, fileName)                        \
67 {                                                                                           \
68     ERROR_PROMPT(operation, promptString, fileName)                                         \
69     if (abort || response == IGNORE) {                                                      \
70         if (!abort) {                                                                       \
71             updateProgress(fileSizeMap[path]);                                              \
72             removeExcludeFiles.insert(path);                                                \
73         }                                                                                   \
74         return;                                                                             \
75     }                                                                                       \
76 }
77
78
79 #define OVERWRITE_PROMPT(file, newFile)                                                     \
80 {                                                                                           \
81     response = NONE;                                                                        \
82                                                                                             \
83     while (!abort && response == NONE && newFile.exists()) {                                \
84         if (overwriteAll != NONE) {                                                         \
85             response = overwriteAll;                                                        \
86         } else {                                                                            \
87             emit showOverwritePrompt(this, newFile.absoluteFilePath(),                      \
88                 newFile.isDir() && file.isDir());                                           \
89             waitOnCond();                                                                   \
90                                                                                             \
91             PAUSE()                                                                         \
92             else if (response == NONE) {                                                    \
93                 emit showInputFilenamePrompt(this, newFile, file.isDir());                  \
94                 waitOnCond();                                                               \
95                 if (newNameFromDialog.size()) {                                             \
96                     newFile.setFile(newNameFromDialog);                                     \
97                 }                                                                           \
98             }                                                                               \
99         }                                                                                   \
100     }                                                                                       \
101     if (response == ASK) response = NONE;                                                   \
102 }
103
104
105 OperationThread::OperationThread(const QFileInfoList files, QDir dest) :
106     abort(false),
107     pause(false),
108     files(files),
109     dest(dest),
110     response(NONE),
111     overwriteAll(NONE),
112     totalSize(0),
113     totalValue(0),
114     fileValue(0)
115 {
116     memset(ignoreAll, false, sizeof(ignoreAll));
117 }
118
119
120 void OperationThread::setResponse(
121     const Response response,
122     const bool applyToAll,
123     const int err)
124 {
125     mutex.lock();
126
127     this->response = response;
128
129     if (applyToAll) {
130         if (response == KEEP
131             || response == OVERWRITE
132             || response == NONE)
133         {
134             overwriteAll = response;
135         }
136
137         if (response == IGNORE) {
138             ignoreAll[err] = true;
139         }
140     }
141
142     if (response == ABORT) abort = true;
143
144     mutex.unlock();
145     waitCond.wakeAll();
146 }
147
148
149 void OperationThread::processFiles(const QFileInfoList &files) {
150     for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
151         PAUSE();
152         if (abort) break;
153         perform(*it);
154     }
155 }
156
157
158 bool OperationThread::remove(QString &fileName, const bool doUpdates) {
159     return remove(QFileInfo(fileName), doUpdates);
160 }
161
162
163 bool OperationThread::remove(const QFileInfoList &files, const bool doUpdates) {
164     bool res = true;
165     for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
166         if (!remove(*it, doUpdates)) res = false;
167         PAUSE();
168         if (abort) break;
169     }
170     return res;
171 }
172
173
174 bool OperationThread::remove(const QFileInfo &file, const bool doUpdates) {
175     QString path = file.absoluteFilePath();
176
177     if (removeExcludeFiles.contains(path)) {
178         if (doUpdates) updateProgress(1);
179         return false;
180     }
181
182     QFSFileEngine engine(path);
183
184     if (doUpdates) updateFile(path);
185
186     if (file.isDir()) {
187         if (!remove(listDirFiles(path), doUpdates)) {
188             if (doUpdates) updateProgress(1);
189             return false;
190         }
191
192         if (!listDirFiles(path).size()) {
193             ERROR_PROMPT(!engine.rmdir(path, false), tr("Error deleting directory %1."), path)
194         }
195     } else {
196         ERROR_PROMPT(!engine.remove(), tr("Error deleting file %1."), path)
197     }
198
199     if (!abort && doUpdates) updateProgress(1);
200
201     PAUSE();
202     if (abort || response == IGNORE) return false;
203     return true;
204 }
205
206
207 void OperationThread::copy(const QFileInfo &file) {
208     QString path(file.absoluteFilePath());
209     QFSFileEngine engine(path);
210     QFileInfo newFile(dest.absolutePath() + "/" + file.fileName());
211
212     updateFile(path);
213
214     // hack to prevent asking about the same file if we already asked in the rename(...) function
215     if (overwriteAll == DONT_ASK_ONCE) {
216         overwriteAll = NONE;
217     } else {
218         OVERWRITE_PROMPT(file, newFile)
219     }
220
221     QString newPath(newFile.absoluteFilePath());
222     QFSFileEngine newEngine(newPath);
223
224     PAUSE();
225     if (abort) return;
226
227     if (file.isDir()) {
228         // save the overwrite response, because the response variable will get ovewritten in remove(...)
229         Response overwriteResponse = response;
230
231         if (newFile.exists() && !newFile.isDir()) {
232             // overwriting a file, so check for KEEP and handle it
233             if (response == KEEP) {
234                 updateProgress(fileSizeMap[path]);
235                 removeExcludeFiles.insert(path);
236                 return;
237             }
238
239             // if it should not be kept, remove it and return on failure
240             if(!remove(newPath)) {
241                 updateProgress(fileSizeMap[path]);
242                 return;
243             }
244             // create new info since we deleted the file - is it needed?
245             newFile = QFileInfo(newPath);
246         } else {
247             // overwriting a directory - response KEEP means to keep the files inside,
248             // SKIP_DIR means to skip the dir completely
249             if (response == SKIP_DIR) {
250                 updateProgress(fileSizeMap[path]);
251                 removeExcludeFiles.insert(path);
252                 return;
253             }
254         }
255
256         if (!newFile.exists()) {
257             SPECIAL_COPY_ERROR_PROMPT(!engine.mkdir(newPath, false),
258                 tr("Error creating directory %1."), newPath)
259         }
260
261         // we've done the job with the dir, so update progress and recurse into the dir
262         updateProgress(1);
263         
264         // change the dest for the recursion
265         QDir destBackup = dest;
266         dest = newPath;
267
268         // and set overwriteAll to the response we got a while ago
269         // because it applies to the files inside the dir
270         Response tmpResp = overwriteAll;
271         overwriteAll = overwriteResponse;
272
273         processFiles(listDirFiles(path));
274
275         overwriteAll = tmpResp;
276
277         ERROR_PROMPT(!newEngine.setPermissions(file.permissions()),
278             tr("Error setting permissions for directory %1."), newPath)
279
280         PAUSE();
281         if (abort) return;
282
283         dest = destBackup;
284     } else {
285         if (response == KEEP) {
286             updateProgress(fileSizeMap[path]);
287             removeExcludeFiles.insert(path);
288             return;
289         }
290
291         SPECIAL_COPY_ERROR_PROMPT(checkSequentialFile(engine), tr("Cannot copy file %1."), path)
292
293         if (newFile.exists() && newFile.isDir()) {
294             SPECIAL_COPY_ERROR_PROMPT(!remove(newPath),
295                 tr("Cannot replace directory %1 due to previous errors."), newPath)
296         }
297
298         SPECIAL_COPY_ERROR_PROMPT(!engine.open(QIODevice::ReadOnly), tr("Error reading file %1."), path)
299
300         bool ignore = false, newFileWritten = false;
301         while (!abort && !ignore) {
302             engine.seek(0);
303             fileValue = 0;
304
305             ERROR_PROMPT(!newEngine.open(QIODevice::WriteOnly | QIODevice::Truncate),
306                 tr("Error writing file %1."), newPath)
307
308             if (abort || response == IGNORE) {
309                 if (response == IGNORE) {
310                     updateProgress(fileSizeMap[path]);
311                     removeExcludeFiles.insert(path);
312                     ignore = true;
313                 }
314                 break;
315             }
316
317             newFileWritten = true;
318
319             bool error = false;
320             char block[BLOCK_SIZE];
321             qint64 bytes;
322
323             while ((bytes = engine.read(block, sizeof(block))) > 0) {
324                 if (bytes == -1 || bytes != newEngine.write(block, bytes)) {
325                     if (bytes == -1) {
326                         SHOW_ERROR_PROMPT(tr("Error while reading from file %1."), path);
327                     } else {
328                         SHOW_ERROR_PROMPT(tr("Error while writing to file %1."), newPath);
329                     }
330
331                     if (!abort) {
332                         if (response == IGNORE) {
333                             updateProgress(fileSizeMap[path] - fileValue);
334                             removeExcludeFiles.insert(path);
335                             ignore = true;
336                         } else {
337                             updateProgress(-fileValue);
338                         }
339                     }
340                     error = true;
341                     break;
342                 }
343
344                 PAUSE();
345                 if (abort) break;
346
347                 updateProgress(1);
348             }
349
350             if (!error) break;
351             PAUSE();
352         }
353
354         engine.close();
355         newEngine.close();
356
357         PAUSE();
358         if (abort || ignore) {
359             if (newFileWritten) {
360                 newEngine.remove();
361             }
362         } else {
363             ERROR_PROMPT(!newEngine.setPermissions(file.permissions()),
364                 tr("Error setting permissions for file %1."), newPath)
365         }
366     }
367 }
368
369
370 unsigned int OperationThread::calculateFileSize(const QFileInfoList &files,
371     const bool count,
372     const bool addSize)
373 {
374     unsigned int res = 0;
375
376     for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
377         unsigned int size = 0;
378
379         PAUSE();
380         if (abort) break;
381
382         if (it->isDir()) {
383             size += calculateFileSize(listDirFiles(it->absoluteFilePath()), count, addSize);
384         }
385
386         if (addSize) {
387             if (it->isDir()) {
388                 ++size;
389             } else {
390                 size += ceil(static_cast<float>(it->size()) / BLOCK_SIZE);
391             }
392             fileSizeMap[it->absoluteFilePath()] = size;
393         }
394
395         if (count) {
396             ++size;
397         }
398
399         res += size;
400     }
401
402     return res;
403 }
404
405
406 QFileInfoList OperationThread::listDirFiles(const QString &dirPath) {
407     QDir dir = dirPath;
408     return dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden);
409 }
410
411
412 void OperationThread::setTotalSize(unsigned int size) {
413     totalSize = size;
414     emit totalSizeChanged(size);
415 }
416
417
418 void OperationThread::updateProgress(int value) {
419     totalValue += value;
420     fileValue += value;
421     emit progressUpdate(value);
422 }
423
424
425 void OperationThread::updateFile(const QString &name) {
426     fileValue = 0;
427     emit fileNameUpdated(shortenPath(name));
428 }
429
430
431 void OperationThread::waitOnCond() {
432     time_t waitTime = time(0);
433     waitCond.wait(&mutex);
434     emit operationResumed(time(0) - waitTime);
435 }
436
437
438 bool OperationThread::checkSequentialFile(const QFSFileEngine &engine) {
439     errno = 0;
440     if (engine.isSequential()) {
441         if (!errno) errno = 255;
442         return true;
443     }
444
445     return false;
446 }
447
448
449 void OperationThread::wake() {
450     pause = false;
451     waitCond.wakeAll();
452 }
453
454
455 void DeleteThread::run() {
456     mutex.lock();
457
458     setTotalSize(calculateFileSize(files, true));
459     emit operationStarted(time(0));
460
461     processFiles(files);
462
463     sleep(0.5);
464     emit finished(this);
465 }
466
467
468 void DeleteThread::perform(const QFileInfo &file) {
469     remove(file, true);
470 }
471
472
473 void CopyThread::run() {
474     mutex.lock();
475
476     setTotalSize(calculateFileSize(files, false, true));
477     emit operationStarted(time(0));
478
479     processFiles(files);
480
481     sleep(0.5);
482     emit finished(this);
483 }
484
485
486 void CopyThread::perform(const QFileInfo &file) {
487     copy(file);
488 }
489
490
491 void MoveThread::run() {
492     mutex.lock();
493
494     rename(files, dest);
495
496     sleep(0.5);
497     emit finished(this);
498 }
499
500
501 void MoveThread::rename(const QFileInfoList &files, const QDir &dest) {
502     setTotalSize(totalSize + files.size());
503     emit operationStarted(time(0));
504
505     for (int i = 0; i < files.size(); ++i) {
506         QString path = files[i].absoluteFilePath();
507         QFSFileEngine engine(path);
508         QFileInfo newFile(dest.absolutePath() + "/" + files[i].fileName());
509
510         updateFile(path);
511
512         OVERWRITE_PROMPT(files[i], newFile)
513
514         // if we are owerwriting dir over a dir, we will get SKIP_DIR
515         // as a response from OVERWRITE_PROMT meaning we should skip it
516         // (KEEP would mean to keep the files inside)
517         if (files[i].isDir() && newFile.exists() && newFile.isDir()) {
518             if (response == SKIP_DIR) {
519                 PAUSE();
520                 if (abort) break;
521                 updateProgress(1);
522                 removeExcludeFiles.insert(path);
523                 continue;
524             }
525         } else {
526             if (response == KEEP) {
527                 PAUSE();
528                 if (abort) break;
529                 updateProgress(1);
530                 removeExcludeFiles.insert(path);
531                 continue;
532             }
533         }
534
535         QString newPath(newFile.absoluteFilePath());
536         QFSFileEngine newEngine(newPath);
537
538         bool done = false;
539
540         while (!abort && !engine.rename(newPath)) {
541             // source and target are on different partitions
542             // this should happen on the first file, unless some are skipped by overwrite prompt
543             // we calculate the actual file sizes, because from now on copy & remove takes over
544             if (errno == EXDEV) {
545                 overwriteAll = response;
546                 // hack: we already checked the first file we are sending to processFiles(...)
547                 // so we don't want to ask about this one again
548                 if (overwriteAll == NONE) overwriteAll = DONT_ASK_ONCE;
549
550                 QFileInfoList remainingFiles = files.mid(i);
551
552                 setTotalSize(totalValue + calculateFileSize(remainingFiles, true, true));
553
554                 processFiles(remainingFiles);
555
556                 emit removeAfterCopy();
557
558                 remove(remainingFiles, true);
559
560                 done = true;
561                 break;
562             // the target is nonempty dir. lets call this recursively and rename the contents one by one
563             } else if (errno == ENOTEMPTY || errno == EEXIST) {
564                 Response tmpResp = overwriteAll;
565                 overwriteAll = response;
566
567                 rename(listDirFiles(path), QDir(newPath));
568                 PAUSE();
569                 if (abort) break;
570
571                 overwriteAll = tmpResp;
572
573                 remove(files[i]);
574
575                 break;
576             // source and target are nonmatching types(file and dir)
577             // remove the target and let it loop once again
578             } else if (errno == ENOTDIR || errno == EISDIR) {
579                 if (!remove(newPath)) break;
580             } else {
581                 SHOW_ERROR_PROMPT(tr("Error moving %1."), path)
582
583                 if (response == IGNORE) {
584                     break;
585                 }
586             }
587             PAUSE();
588         }
589
590         if (done) break;
591
592         PAUSE();
593         if (abort) break;
594         updateProgress(1);
595     }
596 }
597
598
599 void MoveThread::perform(const QFileInfo &file) {
600     copy(file);
601 }