v0.3.0
[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 | QIODevice::Unbuffered),
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) {
325                     SHOW_ERROR_PROMPT(tr("Error while reading from file %1."), path);
326                 
327                     if (!abort) {
328                         if (response == IGNORE) {
329                             updateProgress(fileSizeMap[path] - fileValue);
330                             removeExcludeFiles.insert(path);
331                             ignore = true;
332                         } else {
333                             updateProgress(-fileValue);
334                         }
335                     }
336                     error = true;
337                     break;
338                 } else {
339                     qint64 written = 0;
340                     char *blockPointer = block;
341                     while (bytes != (written = newEngine.write(blockPointer, bytes))) {
342                         SHOW_ERROR_PROMPT(tr("Error while writing to file %1."), newPath);
343
344                         if (response == IGNORE) {
345                             updateProgress(fileSizeMap[path] - fileValue);
346                             removeExcludeFiles.insert(path);
347                             ignore = true;
348                         }
349
350                         if (abort || ignore) break;
351
352                         if (written == -1) written = 0;
353                         bytes -= written;
354                         blockPointer += written;
355
356                         PAUSE();
357                     }
358                 }
359
360                 PAUSE();
361                 if (abort || ignore) break;
362
363                 updateProgress(1);
364             }
365
366             if (!error) break;
367             PAUSE();
368         }
369
370         engine.close();
371         newEngine.close();
372
373         PAUSE();
374         if (abort || ignore) {
375             if (newFileWritten) {
376                 newEngine.remove();
377             }
378         } else {
379             ERROR_PROMPT(!newEngine.setPermissions(file.permissions()),
380                 tr("Error setting permissions for file %1."), newPath)
381         }
382     }
383 }
384
385
386 unsigned int OperationThread::calculateFileSize(const QFileInfoList &files,
387     const bool count,
388     const bool addSize)
389 {
390     unsigned int res = 0;
391
392     for (QFileInfoList::const_iterator it = files.begin(); it != files.end(); ++it) {
393         unsigned int size = 0;
394
395         PAUSE();
396         if (abort) break;
397
398         if (it->isDir()) {
399             size += calculateFileSize(listDirFiles(it->absoluteFilePath()), count, addSize);
400         }
401
402         if (addSize) {
403             if (it->isDir()) {
404                 ++size;
405             } else {
406                 size += ceil(static_cast<float>(it->size()) / BLOCK_SIZE);
407             }
408             fileSizeMap[it->absoluteFilePath()] = size;
409         }
410
411         if (count) {
412             ++size;
413         }
414
415         res += size;
416     }
417
418     return res;
419 }
420
421
422 QFileInfoList OperationThread::listDirFiles(const QString &dirPath) {
423     QDir dir = dirPath;
424     return dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden);
425 }
426
427
428 void OperationThread::setTotalSize(unsigned int size) {
429     totalSize = size;
430     emit totalSizeChanged(size);
431 }
432
433
434 void OperationThread::updateProgress(int value) {
435     totalValue += value;
436     fileValue += value;
437     emit progressUpdate(value);
438 }
439
440
441 void OperationThread::updateFile(const QString &name) {
442     fileValue = 0;
443     emit fileNameUpdated(shortenPath(name));
444 }
445
446
447 void OperationThread::waitOnCond() {
448     time_t waitTime = time(0);
449     waitCond.wait(&mutex);
450     emit operationResumed(time(0) - waitTime);
451 }
452
453
454 bool OperationThread::checkSequentialFile(const QFSFileEngine &engine) {
455     errno = 0;
456     if (engine.isSequential()) {
457         if (!errno) errno = 255;
458         return true;
459     }
460
461     return false;
462 }
463
464
465 void OperationThread::wake() {
466     pause = false;
467     waitCond.wakeAll();
468 }
469
470
471 void DeleteThread::run() {
472     mutex.lock();
473
474     setTotalSize(calculateFileSize(files, true));
475     emit operationStarted(time(0));
476
477     processFiles(files);
478
479     sleep(0.5);
480     emit finished(this);
481 }
482
483
484 void DeleteThread::perform(const QFileInfo &file) {
485     remove(file, true);
486 }
487
488
489 void CopyThread::run() {
490     mutex.lock();
491
492     setTotalSize(calculateFileSize(files, false, true));
493     emit operationStarted(time(0));
494
495     processFiles(files);
496
497     sleep(0.5);
498     emit finished(this);
499 }
500
501
502 void CopyThread::perform(const QFileInfo &file) {
503     copy(file);
504 }
505
506
507 void MoveThread::run() {
508     mutex.lock();
509
510     rename(files, dest);
511
512     sleep(0.5);
513     emit finished(this);
514 }
515
516
517 void MoveThread::rename(const QFileInfoList &files, const QDir &dest) {
518     setTotalSize(totalSize + files.size());
519     emit operationStarted(time(0));
520
521     for (int i = 0; i < files.size(); ++i) {
522         QString path = files[i].absoluteFilePath();
523         QFSFileEngine engine(path);
524         QFileInfo newFile(dest.absolutePath() + "/" + files[i].fileName());
525
526         updateFile(path);
527
528         OVERWRITE_PROMPT(files[i], newFile)
529
530         // if we are owerwriting dir over a dir, we will get SKIP_DIR
531         // as a response from OVERWRITE_PROMT meaning we should skip it
532         // (KEEP would mean to keep the files inside)
533         if (files[i].isDir() && newFile.exists() && newFile.isDir()) {
534             if (response == SKIP_DIR) {
535                 PAUSE();
536                 if (abort) break;
537                 updateProgress(1);
538                 removeExcludeFiles.insert(path);
539                 continue;
540             }
541         } else {
542             if (response == KEEP) {
543                 PAUSE();
544                 if (abort) break;
545                 updateProgress(1);
546                 removeExcludeFiles.insert(path);
547                 continue;
548             }
549         }
550
551         QString newPath(newFile.absoluteFilePath());
552         QFSFileEngine newEngine(newPath);
553
554         bool done = false;
555
556         while (!abort && !engine.rename(newPath)) {
557             // source and target are on different partitions
558             // this should happen on the first file, unless some are skipped by overwrite prompt
559             // we calculate the actual file sizes, because from now on copy & remove takes over
560             if (errno == EXDEV) {
561                 overwriteAll = response;
562                 // hack: we already checked the first file we are sending to processFiles(...)
563                 // so we don't want to ask about this one again
564                 if (overwriteAll == NONE) overwriteAll = DONT_ASK_ONCE;
565
566                 QFileInfoList remainingFiles = files.mid(i);
567
568                 setTotalSize(totalValue + calculateFileSize(remainingFiles, true, true));
569
570                 processFiles(remainingFiles);
571
572                 emit removeAfterCopy();
573
574                 remove(remainingFiles, true);
575
576                 done = true;
577                 break;
578             // the target is nonempty dir. lets call this recursively and rename the contents one by one
579             } else if (errno == ENOTEMPTY || errno == EEXIST) {
580                 Response tmpResp = overwriteAll;
581                 overwriteAll = response;
582
583                 rename(listDirFiles(path), QDir(newPath));
584                 PAUSE();
585                 if (abort) break;
586
587                 overwriteAll = tmpResp;
588
589                 remove(files[i]);
590
591                 break;
592             // source and target are nonmatching types(file and dir)
593             // remove the target and let it loop once again
594             } else if (errno == ENOTDIR || errno == EISDIR) {
595                 if (!remove(newPath)) break;
596             } else {
597                 SHOW_ERROR_PROMPT(tr("Error moving %1."), path)
598
599                 if (response == IGNORE) {
600                     break;
601                 }
602             }
603             PAUSE();
604         }
605
606         if (done) break;
607
608         PAUSE();
609         if (abort) break;
610         updateProgress(1);
611     }
612 }
613
614
615 void MoveThread::perform(const QFileInfo &file) {
616     copy(file);
617 }