backupjob.cpp
00001 /* Copyright 2009 Klarälvdalens Datakonsult AB 00002 00003 This program is free software; you can redistribute it and/or 00004 modify it under the terms of the GNU General Public License as 00005 published by the Free Software Foundation; either version 2 of 00006 the License or (at your option) version 3 or any later version 00007 accepted by the membership of KDE e.V. (or its successor approved 00008 by the membership of KDE e.V.), which shall act as a proxy 00009 defined in Section 14 of version 3 of the license. 00010 00011 This program is distributed in the hope that it will be useful, 00012 but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00014 GNU General Public License for more details. 00015 00016 You should have received a copy of the GNU General Public License 00017 along with this program. If not, see <http://www.gnu.org/licenses/>. 00018 */ 00019 #include "backupjob.h" 00020 00021 #include "kmmsgdict.h" 00022 #include "kmfolder.h" 00023 #include "kmfoldercachedimap.h" 00024 #include "kmfolderdir.h" 00025 #include "folderutil.h" 00026 00027 #include "progressmanager.h" 00028 00029 #include "kzip.h" 00030 #include "ktar.h" 00031 #include "tdemessagebox.h" 00032 00033 #include "tqfile.h" 00034 #include "tqfileinfo.h" 00035 #include "tqstringlist.h" 00036 00037 using namespace KMail; 00038 00039 BackupJob::BackupJob( TQWidget *parent ) 00040 : TQObject( parent ), 00041 mArchiveType( Zip ), 00042 mRootFolder( 0 ), 00043 mArchive( 0 ), 00044 mParentWidget( parent ), 00045 mCurrentFolderOpen( false ), 00046 mArchivedMessages( 0 ), 00047 mArchivedSize( 0 ), 00048 mProgressItem( 0 ), 00049 mAborted( false ), 00050 mDeleteFoldersAfterCompletion( false ), 00051 mCurrentFolder( 0 ), 00052 mCurrentMessage( 0 ), 00053 mCurrentJob( 0 ) 00054 { 00055 } 00056 00057 BackupJob::~BackupJob() 00058 { 00059 mPendingFolders.clear(); 00060 if ( mArchive ) { 00061 delete mArchive; 00062 mArchive = 0; 00063 } 00064 } 00065 00066 void BackupJob::setRootFolder( KMFolder *rootFolder ) 00067 { 00068 mRootFolder = rootFolder; 00069 } 00070 00071 void BackupJob::setSaveLocation( const KURL &savePath ) 00072 { 00073 mMailArchivePath = savePath; 00074 } 00075 00076 void BackupJob::setArchiveType( ArchiveType type ) 00077 { 00078 mArchiveType = type; 00079 } 00080 00081 void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem ) 00082 { 00083 mDeleteFoldersAfterCompletion = deleteThem; 00084 } 00085 00086 TQString BackupJob::stripRootPath( const TQString &path ) const 00087 { 00088 TQString ret = path; 00089 ret = ret.remove( mRootFolder->path() ); 00090 if ( ret.startsWith( "/" ) ) 00091 ret = ret.right( ret.length() - 1 ); 00092 return ret; 00093 } 00094 00095 void BackupJob::queueFolders( KMFolder *root ) 00096 { 00097 mPendingFolders.append( root ); 00098 KMFolderDir *dir = root->child(); 00099 if ( dir ) { 00100 for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) { 00101 if ( node->isDir() ) 00102 continue; 00103 KMFolder *folder = static_cast<KMFolder*>( node ); 00104 queueFolders( folder ); 00105 } 00106 } 00107 } 00108 00109 bool BackupJob::hasChildren( KMFolder *folder ) const 00110 { 00111 KMFolderDir *dir = folder->child(); 00112 if ( dir ) { 00113 for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) { 00114 if ( !node->isDir() ) 00115 return true; 00116 } 00117 } 00118 return false; 00119 } 00120 00121 void BackupJob::cancelJob() 00122 { 00123 abort( i18n( "The operation was canceled by the user." ) ); 00124 } 00125 00126 void BackupJob::abort( const TQString &errorMessage ) 00127 { 00128 // We could be called this twice, since killing the current job below will cause the job to fail, 00129 // and that will call abort() 00130 if ( mAborted ) 00131 return; 00132 00133 mAborted = true; 00134 if ( mCurrentFolderOpen && mCurrentFolder ) { 00135 mCurrentFolder->close( "BackupJob" ); 00136 mCurrentFolder = 0; 00137 } 00138 if ( mArchive && mArchive->isOpened() ) { 00139 mArchive->close(); 00140 } 00141 if ( mCurrentJob ) { 00142 mCurrentJob->kill(); 00143 mCurrentJob = 0; 00144 } 00145 if ( mProgressItem ) { 00146 mProgressItem->setComplete(); 00147 mProgressItem = 0; 00148 // The progressmanager will delete it 00149 } 00150 00151 TQString text = i18n( "Failed to archive the folder '%1'." ).arg( mRootFolder->name() ); 00152 text += "\n" + errorMessage; 00153 KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed." ) ); 00154 deleteLater(); 00155 // Clean up archive file here? 00156 } 00157 00158 void BackupJob::finish() 00159 { 00160 if ( mArchive->isOpened() ) { 00161 mArchive->close(); 00162 if ( !mArchive->closeSucceeded() ) { 00163 abort( i18n( "Unable to finalize the archive file." ) ); 00164 return; 00165 } 00166 } 00167 00168 mProgressItem->setStatus( i18n( "Archiving finished" ) ); 00169 mProgressItem->setComplete(); 00170 mProgressItem = 0; 00171 00172 TQFileInfo archiveFileInfo( mMailArchivePath.path() ); 00173 TQString text = i18n( "Archiving folder '%1' successfully completed. " 00174 "The archive was written to the file '%2'." ) 00175 .arg( mRootFolder->name() ).arg( mMailArchivePath.path() ); 00176 text += "\n" + i18n( "1 message of size %1 was archived.", 00177 "%n messages with the total size of %1 were archived.", mArchivedMessages ) 00178 .arg( TDEIO::convertSize( mArchivedSize ) ); 00179 text += "\n" + i18n( "The archive file has a size of %1." ) 00180 .arg( TDEIO::convertSize( archiveFileInfo.size() ) ); 00181 KMessageBox::information( mParentWidget, text, i18n( "Archiving finished." ) ); 00182 00183 if ( mDeleteFoldersAfterCompletion ) { 00184 // Some saftey checks first... 00185 if ( archiveFileInfo.size() > 0 && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) { 00186 // Sorry for any data loss! 00187 FolderUtil::deleteFolder( mRootFolder, mParentWidget ); 00188 } 00189 } 00190 00191 deleteLater(); 00192 } 00193 00194 void BackupJob::archiveNextMessage() 00195 { 00196 if ( mAborted ) 00197 return; 00198 00199 mCurrentMessage = 0; 00200 if ( mPendingMessages.isEmpty() ) { 00201 kdDebug(5006) << "===> All messages done in folder " << mCurrentFolder->name() << endl; 00202 mCurrentFolder->close( "BackupJob" ); 00203 mCurrentFolderOpen = false; 00204 archiveNextFolder(); 00205 return; 00206 } 00207 00208 unsigned long serNum = mPendingMessages.front(); 00209 mPendingMessages.pop_front(); 00210 00211 KMFolder *folder; 00212 mMessageIndex = -1; 00213 KMMsgDict::instance()->getLocation( serNum, &folder, &mMessageIndex ); 00214 if ( mMessageIndex == -1 ) { 00215 kdWarning(5006) << "Failed to get message location for sernum " << serNum << endl; 00216 abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) ); 00217 return; 00218 } 00219 00220 Q_ASSERT( folder == mCurrentFolder ); 00221 const KMMsgBase *base = mCurrentFolder->getMsgBase( mMessageIndex ); 00222 mUnget = base && !base->isMessage(); 00223 KMMessage *message = mCurrentFolder->getMsg( mMessageIndex ); 00224 if ( !message ) { 00225 kdWarning(5006) << "Failed to retrieve message with index " << mMessageIndex << endl; 00226 abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) ); 00227 return; 00228 } 00229 00230 kdDebug(5006) << "Going to get next message with subject " << message->subject() << ", " 00231 << mPendingMessages.size() << " messages left in the folder." << endl; 00232 00233 if ( message->isComplete() ) { 00234 // Use a singleshot timer, or otherwise we risk ending up in a very big recursion 00235 // for folders that have many messages 00236 mCurrentMessage = message; 00237 TQTimer::singleShot( 0, this, TQT_SLOT( processCurrentMessage() ) ); 00238 } 00239 else if ( message->parent() ) { 00240 mCurrentJob = message->parent()->createJob( message ); 00241 mCurrentJob->setCancellable( false ); 00242 connect( mCurrentJob, TQT_SIGNAL( messageRetrieved( KMMessage* ) ), 00243 this, TQT_SLOT( messageRetrieved( KMMessage* ) ) ); 00244 connect( mCurrentJob, TQT_SIGNAL( result( KMail::FolderJob* ) ), 00245 this, TQT_SLOT( folderJobFinished( KMail::FolderJob* ) ) ); 00246 mCurrentJob->start(); 00247 } 00248 else { 00249 kdWarning(5006) << "Message with subject " << mCurrentMessage->subject() 00250 << " is neither complete nor has a parent!" << endl; 00251 abort( i18n( "Internal error while trying to retrieve a message from folder '%1'." ) 00252 .arg( mCurrentFolder->name() ) ); 00253 } 00254 00255 mProgressItem->setProgress( ( mProgressItem->progress() + 5 ) ); 00256 } 00257 00258 static int fileInfoToUnixPermissions( const TQFileInfo &fileInfo ) 00259 { 00260 int perm = 0; 00261 if ( fileInfo.permission( TQFileInfo::ExeOther ) ) perm += S_IXOTH; 00262 if ( fileInfo.permission( TQFileInfo::WriteOther ) ) perm += S_IWOTH; 00263 if ( fileInfo.permission( TQFileInfo::ReadOther ) ) perm += S_IROTH; 00264 if ( fileInfo.permission( TQFileInfo::ExeGroup ) ) perm += S_IXGRP; 00265 if ( fileInfo.permission( TQFileInfo::WriteGroup ) ) perm += S_IWGRP; 00266 if ( fileInfo.permission( TQFileInfo::ReadGroup ) ) perm += S_IRGRP; 00267 if ( fileInfo.permission( TQFileInfo::ExeOwner ) ) perm += S_IXUSR; 00268 if ( fileInfo.permission( TQFileInfo::WriteOwner ) ) perm += S_IWUSR; 00269 if ( fileInfo.permission( TQFileInfo::ReadOwner ) ) perm += S_IRUSR; 00270 return perm; 00271 } 00272 00273 void BackupJob::processCurrentMessage() 00274 { 00275 if ( mAborted ) 00276 return; 00277 00278 if ( mCurrentMessage ) { 00279 kdDebug(5006) << "Processing message with subject " << mCurrentMessage->subject() << endl; 00280 const DwString &messageDWString = mCurrentMessage->asDwString(); 00281 const uint messageSize = messageDWString.size(); 00282 const char *messageString = mCurrentMessage->asDwString().c_str(); 00283 TQString messageName; 00284 TQFileInfo fileInfo; 00285 if ( messageName.isEmpty() ) { 00286 messageName = TQString::number( mCurrentMessage->getMsgSerNum() ); // IMAP doesn't have filenames 00287 if ( mCurrentMessage->storage() ) { 00288 fileInfo.setFile( mCurrentMessage->storage()->location() ); 00289 // TODO: what permissions etc to take when there is no storage file? 00290 } 00291 } 00292 else { 00293 // TODO: What if the message is not in the "cur" directory? 00294 fileInfo.setFile( mCurrentFolder->location() + "/cur/" + mCurrentMessage->fileName() ); 00295 messageName = mCurrentMessage->fileName(); 00296 } 00297 00298 const TQString fileName = stripRootPath( mCurrentFolder->location() ) + 00299 "/cur/" + messageName; 00300 00301 TQString user; 00302 TQString group; 00303 mode_t permissions = 0700; 00304 time_t creationTime = time( 0 ); 00305 time_t modificationTime = time( 0 ); 00306 time_t accessTime = time( 0 ); 00307 if ( !fileInfo.fileName().isEmpty() ) { 00308 user = fileInfo.owner(); 00309 group = fileInfo.group(); 00310 permissions = fileInfoToUnixPermissions( fileInfo ); 00311 creationTime = fileInfo.created().toTime_t(); 00312 modificationTime = fileInfo.lastModified().toTime_t(); 00313 accessTime = fileInfo.lastRead().toTime_t(); 00314 } 00315 else { 00316 kdWarning(5006) << "Unable to find file for message " << fileName << endl; 00317 } 00318 00319 if ( !mArchive->writeFile( fileName, user, group, messageSize, permissions, accessTime, 00320 modificationTime, creationTime, messageString ) ) { 00321 abort( i18n( "Failed to write a message into the archive folder '%1'." ).arg( mCurrentFolder->name() ) ); 00322 return; 00323 } 00324 00325 if ( mUnget ) { 00326 Q_ASSERT( mMessageIndex >= 0 ); 00327 mCurrentFolder->unGetMsg( mMessageIndex ); 00328 } 00329 00330 mArchivedMessages++; 00331 mArchivedSize += messageSize; 00332 } 00333 else { 00334 // No message? According to ImapJob::slotGetMessageResult(), that means the message is no 00335 // longer on the server. So ignore this one. 00336 kdWarning(5006) << "Unable to download a message for folder " << mCurrentFolder->name() << endl; 00337 } 00338 archiveNextMessage(); 00339 } 00340 00341 void BackupJob::messageRetrieved( KMMessage *message ) 00342 { 00343 mCurrentMessage = message; 00344 processCurrentMessage(); 00345 } 00346 00347 void BackupJob::folderJobFinished( KMail::FolderJob *job ) 00348 { 00349 if ( mAborted ) 00350 return; 00351 00352 // The job might finish after it has emitted messageRetrieved(), in which case we have already 00353 // started a new job. Don't set the current job to 0 in that case. 00354 if ( job == mCurrentJob ) { 00355 mCurrentJob = 0; 00356 } 00357 00358 if ( job->error() ) { 00359 if ( mCurrentFolder ) 00360 abort( i18n( "Downloading a message in folder '%1' failed." ).arg( mCurrentFolder->name() ) ); 00361 else 00362 abort( i18n( "Downloading a message in the current folder failed." ) ); 00363 } 00364 } 00365 00366 bool BackupJob::writeDirHelper( const TQString &directoryPath, const TQString &permissionPath ) 00367 { 00368 TQFileInfo fileInfo( permissionPath ); 00369 TQString user = fileInfo.owner(); 00370 TQString group = fileInfo.group(); 00371 mode_t permissions = fileInfoToUnixPermissions( fileInfo ); 00372 time_t creationTime = fileInfo.created().toTime_t(); 00373 time_t modificationTime = fileInfo.lastModified().toTime_t(); 00374 time_t accessTime = fileInfo.lastRead().toTime_t(); 00375 return mArchive->writeDir( stripRootPath( directoryPath ), user, group, permissions, accessTime, 00376 modificationTime, creationTime ); 00377 } 00378 00379 void BackupJob::archiveNextFolder() 00380 { 00381 if ( mAborted ) 00382 return; 00383 00384 if ( mPendingFolders.isEmpty() ) { 00385 finish(); 00386 return; 00387 } 00388 00389 mCurrentFolder = mPendingFolders.take( 0 ); 00390 kdDebug(5006) << "===> Archiving next folder: " << mCurrentFolder->name() << endl; 00391 mProgressItem->setStatus( i18n( "Archiving folder %1" ).arg( mCurrentFolder->name() ) ); 00392 if ( mCurrentFolder->open( "BackupJob" ) != 0 ) { 00393 abort( i18n( "Unable to open folder '%1'.").arg( mCurrentFolder->name() ) ); 00394 return; 00395 } 00396 mCurrentFolderOpen = true; 00397 00398 const TQString folderName = mCurrentFolder->name(); 00399 bool success = true; 00400 if ( hasChildren( mCurrentFolder ) ) { 00401 if ( !writeDirHelper( mCurrentFolder->subdirLocation(), mCurrentFolder->subdirLocation() ) ) 00402 success = false; 00403 } 00404 if ( !writeDirHelper( mCurrentFolder->location(), mCurrentFolder->location() ) ) 00405 success = false; 00406 if ( !writeDirHelper( mCurrentFolder->location() + "/cur", mCurrentFolder->location() ) ) 00407 success = false; 00408 if ( !writeDirHelper( mCurrentFolder->location() + "/new", mCurrentFolder->location() ) ) 00409 success = false; 00410 if ( !writeDirHelper( mCurrentFolder->location() + "/tmp", mCurrentFolder->location() ) ) 00411 success = false; 00412 if ( !success ) { 00413 abort( i18n( "Unable to create folder structure for folder '%1' within archive file." ) 00414 .arg( mCurrentFolder->name() ) ); 00415 return; 00416 } 00417 00418 for ( int i = 0; i < mCurrentFolder->count( false /* no cache */ ); i++ ) { 00419 unsigned long serNum = KMMsgDict::instance()->getMsgSerNum( mCurrentFolder, i ); 00420 if ( serNum == 0 ) { 00421 // Uh oh 00422 kdWarning(5006) << "Got serial number zero in " << mCurrentFolder->name() 00423 << " at index " << i << "!" << endl; 00424 // TODO: handle error in a nicer way. this is _very_ bad 00425 abort( i18n( "Unable to backup messages in folder '%1', the index file is corrupted." ) 00426 .arg( mCurrentFolder->name() ) ); 00427 return; 00428 } 00429 else 00430 mPendingMessages.append( serNum ); 00431 } 00432 archiveNextMessage(); 00433 } 00434 00435 // TODO 00436 // - error handling 00437 // - import 00438 // - connect to progressmanager, especially abort 00439 // - messagebox when finished (?) 00440 // - ui dialog 00441 // - use correct permissions 00442 // - save index and serial number? 00443 // - guarded pointers for folders 00444 // - online IMAP: check mails first, so sernums are up-to-date? 00445 // - "ignore errors"-mode, with summary how many messages couldn't be archived? 00446 // - do something when the user quits KMail while the backup job is running 00447 // - run in a thread? 00448 // - delete source folder after completion. dangerous!!! 00449 // 00450 // BUGS 00451 // - Online IMAP: Test Mails -> Test%20Mails 00452 // - corrupted sernums indices stop backup job 00453 void BackupJob::start() 00454 { 00455 Q_ASSERT( !mMailArchivePath.isEmpty() ); 00456 Q_ASSERT( mRootFolder ); 00457 00458 queueFolders( mRootFolder ); 00459 00460 switch ( mArchiveType ) { 00461 case Zip: { 00462 KZip *zip = new KZip( mMailArchivePath.path() ); 00463 zip->setCompression( KZip::DeflateCompression ); 00464 mArchive = zip; 00465 break; 00466 } 00467 case Tar: { 00468 mArchive = new KTar( mMailArchivePath.path(), "application/x-tar" ); 00469 break; 00470 } 00471 case TarGz: { 00472 mArchive = new KTar( mMailArchivePath.path(), "application/x-gzip" ); 00473 break; 00474 } 00475 case TarBz2: { 00476 mArchive = new KTar( mMailArchivePath.path(), "application/x-bzip2" ); 00477 break; 00478 } 00479 } 00480 00481 kdDebug(5006) << "Starting backup." << endl; 00482 if ( !mArchive->open( IO_WriteOnly ) ) { 00483 abort( i18n( "Unable to open archive for writing." ) ); 00484 return; 00485 } 00486 00487 mProgressItem = KPIM::ProgressManager::createProgressItem( 00488 "BackupJob", 00489 i18n( "Archiving" ), 00490 TQString(), 00491 true ); 00492 mProgressItem->setUsesBusyIndicator( true ); 00493 connect( mProgressItem, TQT_SIGNAL(progressItemCanceled(KPIM::ProgressItem*)), 00494 this, TQT_SLOT(cancelJob()) ); 00495 00496 archiveNextFolder(); 00497 } 00498 00499 #include "backupjob.moc" 00500