kdelibs/kdecore/io/ksavefile.cpp

447 lines
14 KiB
C++
Raw Normal View History

2014-11-13 01:04:59 +02:00
/* kate: tab-indents off; replace-tabs on; tab-width 4; remove-trailing-space on; encoding utf-8;*/
/*
This file is part of the KDE libraries
Copyright 1999 Waldo Bastian <bastian@kde.org>
Copyright 2006 Allen Winter <winter@kde.org>
Copyright 2006 Gregory S. Hayes <syncomm@kde.org>
Copyright 2006 Jaison Lee <lee.jaison@gmail.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2 as published by the Free Software Foundation.
This library 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
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "ksavefile.h"
#include <config.h>
#include <QDir>
#include <QProcess>
#include <QTemporaryFile>
#include <kde_file.h>
#include <klocale.h>
#include <kstandarddirs.h>
// Only used by the backup file feature:
#include <kconfig.h>
#include <kconfiggroup.h>
#include <stdlib.h>
#include <errno.h>
class KSaveFile::Private
{
public:
QString realFileName; //The name of the end-result file
QString tempFileName; //The name of the temp file we are using
QFile::FileError error;
QString errorString;
bool needFinalize;
bool directWriteFallback;
Private()
: error(QFile::NoError),
needFinalize(false),
directWriteFallback(false)
{
}
};
KSaveFile::KSaveFile()
: d(new Private())
{
}
class KComponentData;
KSaveFile::KSaveFile(const QString &filename, const KComponentData & /*TODO REMOVE*/)
: d(new Private())
{
KSaveFile::setFileName(filename);
}
KSaveFile::~KSaveFile()
{
finalize();
delete d;
}
bool KSaveFile::open(OpenMode flags)
{
if (isOpen()) {
return false;
}
d->needFinalize = false;
if ( d->realFileName.isNull() ) {
d->error=QFile::OpenError;
d->errorString=i18n("No target filename has been given.");
return false;
}
if ( !d->tempFileName.isNull() ) {
#if 0 // do not set an error here, this open() fails, but the file itself is without errors
d->error=QFile::OpenError;
d->errorString=i18n("Already opened.");
#endif
return false;
}
//Create our temporary file
QTemporaryFile tempFile;
tempFile.setAutoRemove(false);
tempFile.setFileTemplate(d->realFileName + QLatin1String("XXXXXX.new"));
if (!tempFile.open()) {
#ifdef Q_OS_UNIX
if (d->directWriteFallback && errno == EACCES) {
QFile::setFileName(d->realFileName);
if (QFile::open(flags)) {
d->tempFileName.clear();
d->error = QFile::NoError;
d->needFinalize = true;
return true;
}
}
#endif
// we only check here if the directory can be written to
// the actual filename isn't written to, but replaced later
// with the contents of our tempfile
const QFileInfo fileInfo(d->realFileName);
QDir parentDir = fileInfo.dir();
if (!QFileInfo(parentDir.absolutePath()).isWritable()) {
d->error=QFile::PermissionsError;
d->errorString=i18n("Insufficient permissions in target directory.");
return false;
}
d->error=QFile::OpenError;
d->errorString=i18n("Unable to open temporary file. Error was: %1.", tempFile.error());
return false;
}
// if we're overwriting an existing file, ensure temp file's
// permissions are the same as existing file so the existing
// file's permissions are preserved. this will succeed completely
// only if we are the same owner and group - or allmighty root.
QFileInfo fi ( d->realFileName );
if (fi.exists()) {
//Qt apparently has no way to change owner/group of file :(
if (fchown(tempFile.handle(), fi.ownerId(), fi.groupId())) {
// failed to set user and group => try to restore group only.
fchown(tempFile.handle(), -1, fi.groupId());
}
tempFile.setPermissions(fi.permissions());
}
else {
mode_t umsk = KGlobal::umask();
fchmod(tempFile.handle(), 0666&(~umsk));
}
//Open oursleves with the temporary file
QFile::setFileName(tempFile.fileName());
if (!QFile::open(flags)) {
tempFile.setAutoRemove(true);
return false;
}
d->tempFileName = tempFile.fileName();
d->error=QFile::NoError;
d->errorString.clear();
d->needFinalize = true;
return true;
}
void KSaveFile::setFileName(const QString &filename)
{
d->realFileName = filename;
// make absolute if needed
if ( QDir::isRelativePath( filename ) ) {
d->realFileName = QDir::current().absoluteFilePath( filename );
}
// follow symbolic link, if any
d->realFileName = KStandardDirs::realFilePath( d->realFileName );
return;
}
QFile::FileError KSaveFile::error() const
{
if ( d->error != QFile::NoError ) {
return d->error;
} else {
return QFile::error();
}
}
QString KSaveFile::errorString() const
{
if ( !d->errorString.isEmpty() ) {
return d->errorString;
} else {
return QFile::errorString();
}
}
QString KSaveFile::fileName() const
{
return d->realFileName;
}
void KSaveFile::abort()
{
close();
if (!d->tempFileName.isEmpty()) {
QFile::remove(d->tempFileName); //non-static QFile::remove() does not work.
d->needFinalize = false;
}
}
#ifdef HAVE_FDATASYNC
# define FDATASYNC fdatasync
#else
# define FDATASYNC fsync
#endif
bool KSaveFile::finalize()
{
if (!d->needFinalize) {
return false;
}
bool success = false;
#ifdef Q_OS_UNIX
static int extraSync = -1;
if (extraSync < 0)
extraSync = getenv("KDE_EXTRA_FSYNC") != 0 ? 1 : 0;
if (extraSync) {
if (flush()) {
forever {
if (!FDATASYNC(handle()))
break;
if (errno != EINTR) {
d->error = QFile::WriteError;
d->errorString = i18n("Synchronization to disk failed");
break;
}
}
}
}
#endif
close();
if (!d->tempFileName.isEmpty()) {
if (error() != NoError) {
QFile::remove(d->tempFileName);
}
//Qt does not allow us to atomically overwrite an existing file,
//so if the target file already exists, there is no way to change it
//to the temp file without creating a small race condition. So we use
//the standard rename call instead, which will do the copy without the
//race condition.
else if (0 == KDE::rename(d->tempFileName,d->realFileName)) {
d->error=QFile::NoError;
d->errorString.clear();
success = true;
} else {
d->error=QFile::OpenError;
d->errorString=i18n("Error during rename.");
QFile::remove(d->tempFileName);
}
} else { // direct overwrite
success = true;
}
d->needFinalize = false;
return success;
}
#undef FDATASYNC
void KSaveFile::setDirectWriteFallback(bool enabled)
{
d->directWriteFallback = enabled;
}
bool KSaveFile::directWriteFallback() const
{
return d->directWriteFallback;
}
bool KSaveFile::backupFile( const QString& qFilename, const QString& backupDir )
{
// get backup type from config, by default use "simple"
// get extension from config, by default use "~"
// get max number of backups from config, by default set to 10
KConfigGroup g(KGlobal::config(), "Backups"); // look in the Backups section
QString type = g.readEntry( "Type", "simple" );
QString extension = g.readEntry( "Extension", "~" );
QString message = g.readEntry( "Message", "Automated KDE Commit" );
int maxnum = g.readEntry( "MaxBackups", 10 );
if ( type.toLower() == QLatin1String("numbered") ) {
return( numberedBackupFile( qFilename, backupDir, extension, maxnum ) );
} else if ( type.toLower() == QLatin1String("rcs") ) {
return( rcsBackupFile( qFilename, backupDir, message ) );
} else {
return( simpleBackupFile( qFilename, backupDir, extension ) );
}
}
bool KSaveFile::simpleBackupFile( const QString& qFilename,
const QString& backupDir,
const QString& backupExtension )
{
QString backupFileName = qFilename + backupExtension;
if ( !backupDir.isEmpty() ) {
QFileInfo fileInfo ( qFilename );
backupFileName = backupDir + QLatin1Char('/') + fileInfo.fileName() + backupExtension;
}
// kDebug(180) << "KSaveFile copying " << qFilename << " to " << backupFileName;
QFile::remove(backupFileName);
return QFile::copy(qFilename, backupFileName);
}
bool KSaveFile::rcsBackupFile( const QString& qFilename,
const QString& backupDir,
const QString& backupMessage )
{
QFileInfo fileInfo ( qFilename );
QString qBackupFilename;
if ( backupDir.isEmpty() ) {
qBackupFilename = qFilename;
} else {
qBackupFilename = backupDir + fileInfo.fileName();
}
qBackupFilename += QString::fromLatin1( ",v" );
// If backupDir is specified, copy qFilename to the
// backupDir and perform the commit there, unlinking
// backupDir/qFilename when finished.
if ( !backupDir.isEmpty() )
{
if ( !QFile::copy(qFilename, backupDir + fileInfo.fileName()) ) {
return false;
}
fileInfo.setFile(backupDir + QLatin1Char('/') + fileInfo.fileName());
}
QString cipath = KStandardDirs::findExe(QString::fromLatin1("ci"));
QString copath = KStandardDirs::findExe(QString::fromLatin1("co"));
QString rcspath = KStandardDirs::findExe(QString::fromLatin1("rcs"));
if ( cipath.isEmpty() || copath.isEmpty() || rcspath.isEmpty() )
return false;
// Check in the file unlocked with 'ci'
QProcess ci;
if ( !backupDir.isEmpty() )
ci.setWorkingDirectory( backupDir );
ci.start( cipath, QStringList() << QString::fromLatin1("-u") << fileInfo.filePath() );
if ( !ci.waitForStarted() )
return false;
ci.write( backupMessage.toLatin1() );
ci.write(".");
ci.closeWriteChannel();
if( !ci.waitForFinished() )
return false;
// Use 'rcs' to unset strict locking
QProcess rcs;
if ( !backupDir.isEmpty() )
rcs.setWorkingDirectory( backupDir );
rcs.start( rcspath, QStringList() << QString::fromLatin1("-U") << qBackupFilename );
if ( !rcs.waitForFinished() )
return false;
// Use 'co' to checkout the current revision and restore permissions
QProcess co;
if ( !backupDir.isEmpty() )
co.setWorkingDirectory( backupDir );
co.start( copath, QStringList() << qBackupFilename );
if ( !co.waitForFinished() )
return false;
if ( !backupDir.isEmpty() ) {
return QFile::remove( fileInfo.filePath() );
} else {
return true;
}
}
bool KSaveFile::numberedBackupFile( const QString& qFilename,
const QString& backupDir,
const QString& backupExtension,
const uint maxBackups )
{
QFileInfo fileInfo ( qFilename );
// The backup file name template.
QString sTemplate;
if ( backupDir.isEmpty() ) {
sTemplate = qFilename + QLatin1String(".%1") + backupExtension;
} else {
sTemplate = backupDir + QLatin1Char('/') + fileInfo.fileName() + QLatin1String(".%1") + backupExtension;
}
// First, search backupDir for numbered backup files to remove.
// Remove all with number 'maxBackups' and greater.
QDir d = backupDir.isEmpty() ? fileInfo.dir() : backupDir;
d.setFilter( QDir::Files | QDir::Hidden | QDir::NoSymLinks );
const QStringList nameFilters = QStringList( fileInfo.fileName() + QLatin1String(".*") + backupExtension );
d.setNameFilters( nameFilters );
d.setSorting( QDir::Name );
uint maxBackupFound = 0;
foreach ( const QFileInfo &fi, d.entryInfoList() ) {
if ( fi.fileName().endsWith( backupExtension ) ) {
// sTemp holds the file name, without the ending backupExtension
QString sTemp = fi.fileName();
sTemp.truncate( fi.fileName().length()-backupExtension.length() );
// compute the backup number
int idex = sTemp.lastIndexOf( QLatin1Char('.') );
if ( idex > 0 ) {
bool ok;
uint num = sTemp.mid( idex+1 ).toUInt( &ok );
if ( ok ) {
if ( num >= maxBackups ) {
QFile::remove( fi.filePath() );
} else {
maxBackupFound = qMax( maxBackupFound, num );
}
}
}
}
}
// Next, rename max-1 to max, max-2 to max-1, etc.
QString to=sTemplate.arg( maxBackupFound+1 );
for ( int i=maxBackupFound; i>0; i-- ) {
QString from = sTemplate.arg( i );
// kDebug(180) << "KSaveFile renaming " << from << " to " << to;
QFile::rename( from, to );
to = from;
}
// Finally create most recent backup by copying the file to backup number 1.
// kDebug(180) << "KSaveFile copying " << qFilename << " to " << sTemplate.arg(1);
return QFile::copy(qFilename, sTemplate.arg(1));
}