You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
501 lines
16 KiB
501 lines
16 KiB
/* Copyright 2009 Klarälvdalens Datakonsult AB
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of
|
|
the License or (at your option) version 3 or any later version
|
|
accepted by the membership of KDE e.V. (or its successor approved
|
|
by the membership of KDE e.V.), which shall act as a proxy
|
|
defined in Section 14 of version 3 of the license.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "backupjob.h"
|
|
|
|
#include "kmmsgdict.h"
|
|
#include "kmfolder.h"
|
|
#include "kmfoldercachedimap.h"
|
|
#include "kmfolderdir.h"
|
|
#include "folderutil.h"
|
|
|
|
#include "progressmanager.h"
|
|
|
|
#include "kzip.h"
|
|
#include "ktar.h"
|
|
#include "kmessagebox.h"
|
|
|
|
#include "tqfile.h"
|
|
#include "tqfileinfo.h"
|
|
#include "tqstringlist.h"
|
|
|
|
using namespace KMail;
|
|
|
|
BackupJob::BackupJob( TQWidget *parent )
|
|
: TQObject( parent ),
|
|
mArchiveType( Zip ),
|
|
mRootFolder( 0 ),
|
|
mArchive( 0 ),
|
|
mParentWidget( parent ),
|
|
mCurrentFolderOpen( false ),
|
|
mArchivedMessages( 0 ),
|
|
mArchivedSize( 0 ),
|
|
mProgressItem( 0 ),
|
|
mAborted( false ),
|
|
mDeleteFoldersAfterCompletion( false ),
|
|
mCurrentFolder( 0 ),
|
|
mCurrentMessage( 0 ),
|
|
mCurrentJob( 0 )
|
|
{
|
|
}
|
|
|
|
BackupJob::~BackupJob()
|
|
{
|
|
mPendingFolders.clear();
|
|
if ( mArchive ) {
|
|
delete mArchive;
|
|
mArchive = 0;
|
|
}
|
|
}
|
|
|
|
void BackupJob::setRootFolder( KMFolder *rootFolder )
|
|
{
|
|
mRootFolder = rootFolder;
|
|
}
|
|
|
|
void BackupJob::setSaveLocation( const KURL &savePath )
|
|
{
|
|
mMailArchivePath = savePath;
|
|
}
|
|
|
|
void BackupJob::setArchiveType( ArchiveType type )
|
|
{
|
|
mArchiveType = type;
|
|
}
|
|
|
|
void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem )
|
|
{
|
|
mDeleteFoldersAfterCompletion = deleteThem;
|
|
}
|
|
|
|
TQString BackupJob::stripRootPath( const TQString &path ) const
|
|
{
|
|
TQString ret = path;
|
|
ret = ret.remove( mRootFolder->path() );
|
|
if ( ret.startsWith( "/" ) )
|
|
ret = ret.right( ret.length() - 1 );
|
|
return ret;
|
|
}
|
|
|
|
void BackupJob::queueFolders( KMFolder *root )
|
|
{
|
|
mPendingFolders.append( root );
|
|
KMFolderDir *dir = root->child();
|
|
if ( dir ) {
|
|
for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
|
|
if ( node->isDir() )
|
|
continue;
|
|
KMFolder *folder = static_cast<KMFolder*>( node );
|
|
queueFolders( folder );
|
|
}
|
|
}
|
|
}
|
|
|
|
bool BackupJob::hasChildren( KMFolder *folder ) const
|
|
{
|
|
KMFolderDir *dir = folder->child();
|
|
if ( dir ) {
|
|
for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
|
|
if ( !node->isDir() )
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void BackupJob::cancelJob()
|
|
{
|
|
abort( i18n( "The operation was canceled by the user." ) );
|
|
}
|
|
|
|
void BackupJob::abort( const TQString &errorMessage )
|
|
{
|
|
// We could be called this twice, since killing the current job below will cause the job to fail,
|
|
// and that will call abort()
|
|
if ( mAborted )
|
|
return;
|
|
|
|
mAborted = true;
|
|
if ( mCurrentFolderOpen && mCurrentFolder ) {
|
|
mCurrentFolder->close( "BackupJob" );
|
|
mCurrentFolder = 0;
|
|
}
|
|
if ( mArchive && mArchive->isOpened() ) {
|
|
mArchive->close();
|
|
}
|
|
if ( mCurrentJob ) {
|
|
mCurrentJob->kill();
|
|
mCurrentJob = 0;
|
|
}
|
|
if ( mProgressItem ) {
|
|
mProgressItem->setComplete();
|
|
mProgressItem = 0;
|
|
// The progressmanager will delete it
|
|
}
|
|
|
|
TQString text = i18n( "Failed to archive the folder '%1'." ).arg( mRootFolder->name() );
|
|
text += "\n" + errorMessage;
|
|
KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed." ) );
|
|
deleteLater();
|
|
// Clean up archive file here?
|
|
}
|
|
|
|
void BackupJob::finish()
|
|
{
|
|
if ( mArchive->isOpened() ) {
|
|
mArchive->close();
|
|
if ( !mArchive->closeSucceeded() ) {
|
|
abort( i18n( "Unable to finalize the archive file." ) );
|
|
return;
|
|
}
|
|
}
|
|
|
|
mProgressItem->seStatus( i18n( "Archiving finished" ) );
|
|
mProgressItem->setComplete();
|
|
mProgressItem = 0;
|
|
|
|
TQFileInfo archiveFileInfo( mMailArchivePath.path() );
|
|
TQString text = i18n( "Archiving folder '%1' successfully completed. "
|
|
"The archive was written to the file '%2'." )
|
|
.arg( mRootFolder->name() ).arg( mMailArchivePath.path() );
|
|
text += "\n" + i18n( "1 message of size %1 was archived.",
|
|
"%n messages with the total size of %1 were archived.", mArchivedMessages )
|
|
.arg( KIO::convertSize( mArchivedSize ) );
|
|
text += "\n" + i18n( "The archive file has a size of %1." )
|
|
.arg( KIO::convertSize( archiveFileInfo.size() ) );
|
|
KMessageBox::information( mParentWidget, text, i18n( "Archiving finished." ) );
|
|
|
|
if ( mDeleteFoldersAfterCompletion ) {
|
|
// Some saftey checks first...
|
|
if ( archiveFileInfo.size() > 0 && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) {
|
|
// Sorry for any data loss!
|
|
FolderUtil::deleteFolder( mRootFolder, mParentWidget );
|
|
}
|
|
}
|
|
|
|
deleteLater();
|
|
}
|
|
|
|
void BackupJob::archiveNextMessage()
|
|
{
|
|
if ( mAborted )
|
|
return;
|
|
|
|
mCurrentMessage = 0;
|
|
if ( mPendingMessages.isEmpty() ) {
|
|
kdDebug(5006) << "===> All messages done in folder " << mCurrentFolder->name() << endl;
|
|
mCurrentFolder->close( "BackupJob" );
|
|
mCurrentFolderOpen = false;
|
|
archiveNextFolder();
|
|
return;
|
|
}
|
|
|
|
unsigned long serNum = mPendingMessages.front();
|
|
mPendingMessages.pop_front();
|
|
|
|
KMFolder *folder;
|
|
mMessageIndex = -1;
|
|
KMMsgDict::instance()->getLocation( serNum, &folder, &mMessageIndex );
|
|
if ( mMessageIndex == -1 ) {
|
|
kdWarning(5006) << "Failed to get message location for sernum " << serNum << endl;
|
|
abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
|
|
return;
|
|
}
|
|
|
|
Q_ASSERT( folder == mCurrentFolder );
|
|
const KMMsgBase *base = mCurrentFolder->getMsgBase( mMessageIndex );
|
|
mUnget = base && !base->isMessage();
|
|
KMMessage *message = mCurrentFolder->getMsg( mMessageIndex );
|
|
if ( !message ) {
|
|
kdWarning(5006) << "Failed to retrieve message with index " << mMessageIndex << endl;
|
|
abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
|
|
return;
|
|
}
|
|
|
|
kdDebug(5006) << "Going to get next message with subject " << message->subject() << ", "
|
|
<< mPendingMessages.size() << " messages left in the folder." << endl;
|
|
|
|
if ( message->isComplete() ) {
|
|
// Use a singleshot timer, or otherwise we risk ending up in a very big recursion
|
|
// for folders that have many messages
|
|
mCurrentMessage = message;
|
|
TQTimer::singleShot( 0, this, TQT_SLOT( processCurrentMessage() ) );
|
|
}
|
|
else if ( message->parent() ) {
|
|
mCurrentJob = message->parent()->createJob( message );
|
|
mCurrentJob->setCancellable( false );
|
|
connect( mCurrentJob, TQT_SIGNAL( messageRetrieved( KMMessage* ) ),
|
|
this, TQT_SLOT( messageRetrieved( KMMessage* ) ) );
|
|
connect( mCurrentJob, TQT_SIGNAL( result( KMail::FolderJob* ) ),
|
|
this, TQT_SLOT( folderJobFinished( KMail::FolderJob* ) ) );
|
|
mCurrentJob->start();
|
|
}
|
|
else {
|
|
kdWarning(5006) << "Message with subject " << mCurrentMessage->subject()
|
|
<< " is neither complete nor has a parent!" << endl;
|
|
abort( i18n( "Internal error while trying to retrieve a message from folder '%1'." )
|
|
.arg( mCurrentFolder->name() ) );
|
|
}
|
|
|
|
mProgressItem->setProgress( ( mProgressItem->progress() + 5 ) );
|
|
}
|
|
|
|
static int fileInfoToUnixPermissions( const TQFileInfo &fileInfo )
|
|
{
|
|
int perm = 0;
|
|
if ( fileInfo.permission( TQFileInfo::ExeOther ) ) perm += S_IXOTH;
|
|
if ( fileInfo.permission( TQFileInfo::WriteOther ) ) perm += S_IWOTH;
|
|
if ( fileInfo.permission( TQFileInfo::ReadOther ) ) perm += S_IROTH;
|
|
if ( fileInfo.permission( TQFileInfo::ExeGroup ) ) perm += S_IXGRP;
|
|
if ( fileInfo.permission( TQFileInfo::WriteGroup ) ) perm += S_IWGRP;
|
|
if ( fileInfo.permission( TQFileInfo::ReadGroup ) ) perm += S_IRGRP;
|
|
if ( fileInfo.permission( TQFileInfo::ExeOwner ) ) perm += S_IXUSR;
|
|
if ( fileInfo.permission( TQFileInfo::WriteOwner ) ) perm += S_IWUSR;
|
|
if ( fileInfo.permission( TQFileInfo::ReadOwner ) ) perm += S_IRUSR;
|
|
return perm;
|
|
}
|
|
|
|
void BackupJob::processCurrentMessage()
|
|
{
|
|
if ( mAborted )
|
|
return;
|
|
|
|
if ( mCurrentMessage ) {
|
|
kdDebug(5006) << "Processing message with subject " << mCurrentMessage->subject() << endl;
|
|
const DwString &messageDWString = mCurrentMessage->asDwString();
|
|
const uint messageSize = messageDWString.size();
|
|
const char *messageString = mCurrentMessage->asDwString().c_str();
|
|
TQString messageName;
|
|
TQFileInfo fileInfo;
|
|
if ( messageName.isEmpty() ) {
|
|
messageName = TQString::number( mCurrentMessage->getMsgSerNum() ); // IMAP doesn't have filenames
|
|
if ( mCurrentMessage->storage() ) {
|
|
fileInfo.setFile( mCurrentMessage->storage()->location() );
|
|
// TODO: what permissions etc to take when there is no storage file?
|
|
}
|
|
}
|
|
else {
|
|
// TODO: What if the message is not in the "cur" directory?
|
|
fileInfo.setFile( mCurrentFolder->location() + "/cur/" + mCurrentMessage->fileName() );
|
|
messageName = mCurrentMessage->fileName();
|
|
}
|
|
|
|
const TQString fileName = stripRootPath( mCurrentFolder->location() ) +
|
|
"/cur/" + messageName;
|
|
|
|
TQString user;
|
|
TQString group;
|
|
mode_t permissions = 0700;
|
|
time_t creationTime = time( 0 );
|
|
time_t modificationTime = time( 0 );
|
|
time_t accessTime = time( 0 );
|
|
if ( !fileInfo.fileName().isEmpty() ) {
|
|
user = fileInfo.owner();
|
|
group = fileInfo.group();
|
|
permissions = fileInfoToUnixPermissions( fileInfo );
|
|
creationTime = fileInfo.created().toTime_t();
|
|
modificationTime = fileInfo.lastModified().toTime_t();
|
|
accessTime = fileInfo.lastRead().toTime_t();
|
|
}
|
|
else {
|
|
kdWarning(5006) << "Unable to find file for message " << fileName << endl;
|
|
}
|
|
|
|
if ( !mArchive->writeFile( fileName, user, group, messageSize, permissions, accessTime,
|
|
modificationTime, creationTime, messageString ) ) {
|
|
abort( i18n( "Failed to write a message into the archive folder '%1'." ).arg( mCurrentFolder->name() ) );
|
|
return;
|
|
}
|
|
|
|
if ( mUnget ) {
|
|
Q_ASSERT( mMessageIndex >= 0 );
|
|
mCurrentFolder->unGetMsg( mMessageIndex );
|
|
}
|
|
|
|
mArchivedMessages++;
|
|
mArchivedSize += messageSize;
|
|
}
|
|
else {
|
|
// No message? According to ImapJob::slotGetMessageResult(), that means the message is no
|
|
// longer on the server. So ignore this one.
|
|
kdWarning(5006) << "Unable to download a message for folder " << mCurrentFolder->name() << endl;
|
|
}
|
|
archiveNextMessage();
|
|
}
|
|
|
|
void BackupJob::messageRetrieved( KMMessage *message )
|
|
{
|
|
mCurrentMessage = message;
|
|
processCurrentMessage();
|
|
}
|
|
|
|
void BackupJob::folderJobFinished( KMail::FolderJob *job )
|
|
{
|
|
if ( mAborted )
|
|
return;
|
|
|
|
// The job might finish after it has emitted messageRetrieved(), in which case we have already
|
|
// started a new job. Don't set the current job to 0 in that case.
|
|
if ( job == mCurrentJob ) {
|
|
mCurrentJob = 0;
|
|
}
|
|
|
|
if ( job->error() ) {
|
|
if ( mCurrentFolder )
|
|
abort( i18n( "Downloading a message in folder '%1' failed." ).arg( mCurrentFolder->name() ) );
|
|
else
|
|
abort( i18n( "Downloading a message in the current folder failed." ) );
|
|
}
|
|
}
|
|
|
|
bool BackupJob::writeDirHelper( const TQString &directoryPath, const TQString &permissionPath )
|
|
{
|
|
TQFileInfo fileInfo( permissionPath );
|
|
TQString user = fileInfo.owner();
|
|
TQString group = fileInfo.group();
|
|
mode_t permissions = fileInfoToUnixPermissions( fileInfo );
|
|
time_t creationTime = fileInfo.created().toTime_t();
|
|
time_t modificationTime = fileInfo.lastModified().toTime_t();
|
|
time_t accessTime = fileInfo.lastRead().toTime_t();
|
|
return mArchive->writeDir( stripRootPath( directoryPath ), user, group, permissions, accessTime,
|
|
modificationTime, creationTime );
|
|
}
|
|
|
|
void BackupJob::archiveNextFolder()
|
|
{
|
|
if ( mAborted )
|
|
return;
|
|
|
|
if ( mPendingFolders.isEmpty() ) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
mCurrentFolder = mPendingFolders.take( 0 );
|
|
kdDebug(5006) << "===> Archiving next folder: " << mCurrentFolder->name() << endl;
|
|
mProgressItem->seStatus( i18n( "Archiving folder %1" ).arg( mCurrentFolder->name() ) );
|
|
if ( mCurrentFolder->open( "BackupJob" ) != 0 ) {
|
|
abort( i18n( "Unable to open folder '%1'.").arg( mCurrentFolder->name() ) );
|
|
return;
|
|
}
|
|
mCurrentFolderOpen = true;
|
|
|
|
const TQString folderName = mCurrentFolder->name();
|
|
bool success = true;
|
|
if ( hasChildren( mCurrentFolder ) ) {
|
|
if ( !writeDirHelper( mCurrentFolder->subdirLocation(), mCurrentFolder->subdirLocation() ) )
|
|
success = false;
|
|
}
|
|
if ( !writeDirHelper( mCurrentFolder->location(), mCurrentFolder->location() ) )
|
|
success = false;
|
|
if ( !writeDirHelper( mCurrentFolder->location() + "/cur", mCurrentFolder->location() ) )
|
|
success = false;
|
|
if ( !writeDirHelper( mCurrentFolder->location() + "/new", mCurrentFolder->location() ) )
|
|
success = false;
|
|
if ( !writeDirHelper( mCurrentFolder->location() + "/tmp", mCurrentFolder->location() ) )
|
|
success = false;
|
|
if ( !success ) {
|
|
abort( i18n( "Unable to create folder structure for folder '%1' within archive file." )
|
|
.arg( mCurrentFolder->name() ) );
|
|
return;
|
|
}
|
|
|
|
for ( int i = 0; i < mCurrentFolder->count( false /* no cache */ ); i++ ) {
|
|
unsigned long serNum = KMMsgDict::instance()->getMsgSerNum( mCurrentFolder, i );
|
|
if ( serNum == 0 ) {
|
|
// Uh oh
|
|
kdWarning(5006) << "Got serial number zero in " << mCurrentFolder->name()
|
|
<< " at index " << i << "!" << endl;
|
|
// TODO: handle error in a nicer way. this is _very_ bad
|
|
abort( i18n( "Unable to backup messages in folder '%1', the index file is corrupted." )
|
|
.arg( mCurrentFolder->name() ) );
|
|
return;
|
|
}
|
|
else
|
|
mPendingMessages.append( serNum );
|
|
}
|
|
archiveNextMessage();
|
|
}
|
|
|
|
// TODO
|
|
// - error handling
|
|
// - import
|
|
// - connect to progressmanager, especially abort
|
|
// - messagebox when finished (?)
|
|
// - ui dialog
|
|
// - use correct permissions
|
|
// - save index and serial number?
|
|
// - guarded pointers for folders
|
|
// - online IMAP: check mails first, so sernums are up-to-date?
|
|
// - "ignore errors"-mode, with summary how many messages couldn't be archived?
|
|
// - do something when the user quits KMail while the backup job is running
|
|
// - run in a thread?
|
|
// - delete source folder after completion. dangerous!!!
|
|
//
|
|
// BUGS
|
|
// - Online IMAP: Test Mails -> Test%20Mails
|
|
// - corrupted sernums indices stop backup job
|
|
void BackupJob::start()
|
|
{
|
|
Q_ASSERT( !mMailArchivePath.isEmpty() );
|
|
Q_ASSERT( mRootFolder );
|
|
|
|
queueFolders( mRootFolder );
|
|
|
|
switch ( mArchiveType ) {
|
|
case Zip: {
|
|
KZip *zip = new KZip( mMailArchivePath.path() );
|
|
zip->setCompression( KZip::DeflateCompression );
|
|
mArchive = zip;
|
|
break;
|
|
}
|
|
case Tar: {
|
|
mArchive = new KTar( mMailArchivePath.path(), "application/x-tar" );
|
|
break;
|
|
}
|
|
case TarGz: {
|
|
mArchive = new KTar( mMailArchivePath.path(), "application/x-gzip" );
|
|
break;
|
|
}
|
|
case TarBz2: {
|
|
mArchive = new KTar( mMailArchivePath.path(), "application/x-bzip2" );
|
|
break;
|
|
}
|
|
}
|
|
|
|
kdDebug(5006) << "Starting backup." << endl;
|
|
if ( !mArchive->open( IO_WriteOnly ) ) {
|
|
abort( i18n( "Unable to open archive for writing." ) );
|
|
return;
|
|
}
|
|
|
|
mProgressItem = KPIM::ProgressManager::createProgressItem(
|
|
"BackupJob",
|
|
i18n( "Archiving" ),
|
|
TQString(),
|
|
true );
|
|
mProgressItem->setUsesBusyIndicator( true );
|
|
connect( mProgressItem, TQT_SIGNAL(progressItemCanceled(KPIM::ProgressItem*)),
|
|
this, TQT_SLOT(cancelJob()) );
|
|
|
|
archiveNextFolder();
|
|
}
|
|
|
|
#include "backupjob.moc"
|
|
|