mirror of
https://github.com/nextcloud/desktop.git
synced 2024-10-29 00:26:33 +03:00
1065 lines
35 KiB
C++
1065 lines
35 KiB
C++
/*
|
|
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
|
|
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
|
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
|
*
|
|
* 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) any later version.
|
|
*
|
|
* 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.
|
|
*/
|
|
#include "config.h"
|
|
|
|
#include "account.h"
|
|
#include "accountstate.h"
|
|
#include "folder.h"
|
|
#include "folderman.h"
|
|
#include "logger.h"
|
|
#include "configfile.h"
|
|
#include "networkjobs.h"
|
|
#include "syncjournalfilerecord.h"
|
|
#include "syncresult.h"
|
|
#include "utility.h"
|
|
#include "clientproxy.h"
|
|
#include "syncengine.h"
|
|
#include "syncrunfilelog.h"
|
|
#include "theme.h"
|
|
#include "filesystem.h"
|
|
|
|
|
|
#include "creds/abstractcredentials.h"
|
|
|
|
#include <QDebug>
|
|
#include <QTimer>
|
|
#include <QUrl>
|
|
#include <QDir>
|
|
#include <QSettings>
|
|
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
|
|
namespace OCC {
|
|
|
|
static void csyncLogCatcher(int /*verbosity*/,
|
|
const char */*function*/,
|
|
const char *buffer,
|
|
void */*userdata*/)
|
|
{
|
|
Logger::instance()->csyncLog( QString::fromUtf8(buffer) );
|
|
}
|
|
|
|
|
|
Folder::Folder(AccountState* accountState,
|
|
const FolderDefinition& definition,
|
|
QObject* parent)
|
|
: QObject(parent)
|
|
, _accountState(accountState)
|
|
, _definition(definition)
|
|
, _csyncError(false)
|
|
, _csyncUnavail(false)
|
|
, _wipeDb(false)
|
|
, _proxyDirty(true)
|
|
, _lastSyncDuration(0)
|
|
, _forceSyncOnPollTimeout(false)
|
|
, _consecutiveFailingSyncs(0)
|
|
, _consecutiveFollowUpSyncs(0)
|
|
, _journal(definition.localPath)
|
|
, _csync_ctx(0)
|
|
{
|
|
qsrand(QTime::currentTime().msec());
|
|
_timeSinceLastSyncStart.start();
|
|
_timeSinceLastSyncDone.start();
|
|
|
|
_syncResult.setStatus( SyncResult::NotYetStarted );
|
|
|
|
// check if the local path exists
|
|
checkLocalPath();
|
|
|
|
_syncResult.setFolder(_definition.alias);
|
|
}
|
|
|
|
bool Folder::init()
|
|
{
|
|
// We need to reconstruct the url because the path need to be fully decoded, as csync will re-encode the path:
|
|
// Remember that csync will just append the filename to the path and pass it to the vio plugin.
|
|
// csync_owncloud will then re-encode everything.
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
|
|
QUrl url = remoteUrl();
|
|
QString url_string = url.scheme() + QLatin1String("://") + url.authority(QUrl::EncodeDelimiters) + url.path(QUrl::FullyDecoded);
|
|
#else
|
|
// Qt4 was broken anyway as it did not encode the '#' as it should have done (it was actually a provlem when parsing the path from QUrl::setPath
|
|
QString url_string = remoteUrl().toString();
|
|
#endif
|
|
url_string = Utility::toCSyncScheme(url_string);
|
|
|
|
QString localpath = path();
|
|
|
|
if( csync_create( &_csync_ctx, localpath.toUtf8().data(), url_string.toUtf8().data() ) < 0 ) {
|
|
qDebug() << "Unable to create csync-context!";
|
|
slotSyncError(tr("Unable to create csync-context"));
|
|
_csync_ctx = 0;
|
|
} else {
|
|
csync_set_log_callback( csyncLogCatcher );
|
|
csync_set_log_level( 11 );
|
|
|
|
_accountState->account()->credentials()->syncContextPreInit(_csync_ctx);
|
|
|
|
if( csync_init( _csync_ctx ) < 0 ) {
|
|
qDebug() << "Could not initialize csync!" << csync_get_status(_csync_ctx) << csync_get_status_string(_csync_ctx);
|
|
QString errStr = SyncEngine::csyncErrorToString(CSYNC_STATUS(csync_get_status(_csync_ctx)));
|
|
const char *errMsg = csync_get_status_string(_csync_ctx);
|
|
if( errMsg ) {
|
|
errStr += QLatin1String("<br/>");
|
|
errStr += QString::fromUtf8(errMsg);
|
|
}
|
|
slotSyncError(errStr);
|
|
csync_destroy(_csync_ctx);
|
|
_csync_ctx = 0;
|
|
}
|
|
}
|
|
return _csync_ctx;
|
|
}
|
|
|
|
Folder::~Folder()
|
|
{
|
|
if( _engine ) {
|
|
_engine->abort();
|
|
_engine.reset(0);
|
|
}
|
|
// Destroy csync here.
|
|
csync_destroy(_csync_ctx);
|
|
}
|
|
|
|
AccountState* Folder::accountState() const
|
|
{
|
|
return _accountState;
|
|
}
|
|
|
|
void Folder::checkLocalPath()
|
|
{
|
|
const QFileInfo fi(_definition.localPath);
|
|
|
|
if( fi.isDir() && fi.isReadable() ) {
|
|
qDebug() << "Checked local path ok";
|
|
} else {
|
|
if( !FileSystem::fileExists(_definition.localPath) ) {
|
|
// try to create the local dir
|
|
QDir d(_definition.localPath);
|
|
if( d.mkpath(_definition.localPath) ) {
|
|
qDebug() << "Successfully created the local dir " << _definition.localPath;
|
|
}
|
|
}
|
|
// Check directory again
|
|
if( !FileSystem::fileExists(_definition.localPath) ) {
|
|
_syncResult.setErrorString(tr("Local folder %1 does not exist.").arg(_definition.localPath));
|
|
_syncResult.setStatus( SyncResult::SetupError );
|
|
} else if( !fi.isDir() ) {
|
|
_syncResult.setErrorString(tr("%1 should be a directory but is not.").arg(_definition.localPath));
|
|
_syncResult.setStatus( SyncResult::SetupError );
|
|
} else if( !fi.isReadable() ) {
|
|
_syncResult.setErrorString(tr("%1 is not readable.").arg(_definition.localPath));
|
|
_syncResult.setStatus( SyncResult::SetupError );
|
|
}
|
|
}
|
|
}
|
|
|
|
QString Folder::alias() const
|
|
{
|
|
return _definition.alias;
|
|
}
|
|
|
|
QString Folder::path() const
|
|
{
|
|
QString p(_definition.localPath);
|
|
if( ! p.endsWith(QDir::separator()) ) {
|
|
p.append(QDir::separator());
|
|
}
|
|
return p;
|
|
}
|
|
|
|
QString Folder::cleanPath()
|
|
{
|
|
QString cleanedPath = QDir::cleanPath(_definition.localPath);
|
|
|
|
if(cleanedPath.length() == 3 && cleanedPath.endsWith(":/"))
|
|
cleanedPath.remove(2,1);
|
|
|
|
return cleanedPath;
|
|
}
|
|
|
|
bool Folder::isBusy() const
|
|
{
|
|
return !_engine.isNull();
|
|
}
|
|
|
|
QString Folder::remotePath() const
|
|
{
|
|
return _definition.targetPath;
|
|
}
|
|
|
|
QUrl Folder::remoteUrl() const
|
|
{
|
|
QUrl url = _accountState->account()->davUrl();
|
|
QString path = url.path();
|
|
if (!path.endsWith('/')) {
|
|
path.append('/');
|
|
}
|
|
path.append(remotePath());
|
|
url.setPath(path);
|
|
return url;
|
|
}
|
|
|
|
QString Folder::nativePath() const
|
|
{
|
|
return QDir::toNativeSeparators(path());
|
|
}
|
|
|
|
bool Folder::syncPaused() const
|
|
{
|
|
return _definition.paused;
|
|
}
|
|
|
|
void Folder::setSyncPaused( bool paused )
|
|
{
|
|
if (paused != _definition.paused) {
|
|
_definition.paused = paused;
|
|
saveToSettings();
|
|
}
|
|
|
|
if( !paused ) {
|
|
// qDebug() << "Syncing enabled on folder " << name();
|
|
} else {
|
|
// do not stop or start the watcher here, that is done internally by
|
|
// folder class. Even if the watcher fires, the folder does not
|
|
// schedule itself because it checks the var. _enabled before.
|
|
setSyncState(SyncResult::Paused);
|
|
}
|
|
}
|
|
|
|
void Folder::setSyncState(SyncResult::Status state)
|
|
{
|
|
_syncResult.setStatus(state);
|
|
}
|
|
|
|
SyncResult Folder::syncResult() const
|
|
{
|
|
return _syncResult;
|
|
}
|
|
|
|
void Folder::prepareToSync()
|
|
{
|
|
_syncResult.setStatus( SyncResult::NotYetStarted );
|
|
_syncResult.clearErrors();
|
|
}
|
|
|
|
void Folder::slotRunEtagJob()
|
|
{
|
|
qDebug() << "* Trying to check" << alias() << "for changes via ETag check. (time since last sync:" << (_timeSinceLastSyncDone.elapsed() / 1000) << "s)";
|
|
|
|
|
|
AccountPtr account = _accountState->account();
|
|
|
|
if (!_requestEtagJob.isNull()) {
|
|
qDebug() << Q_FUNC_INFO << alias() << "has ETag job queued, not trying to sync";
|
|
return;
|
|
}
|
|
|
|
if (_definition.paused || !_accountState->isConnected()) {
|
|
qDebug() << "Not syncing. :" << alias() << _definition.paused << AccountState::stateString(_accountState->state());
|
|
return;
|
|
}
|
|
|
|
bool forceSyncIntervalExpired =
|
|
quint64(_timeSinceLastSyncDone.elapsed()) > ConfigFile().forceSyncInterval();
|
|
bool syncAgainAfterFail = _consecutiveFailingSyncs > 0 && _consecutiveFailingSyncs < 3;
|
|
|
|
// There are several conditions under which we trigger a full-discovery sync:
|
|
// * When a suitably long time has passed since the last sync finished
|
|
// * When the last sync failed (only a couple of times)
|
|
// * When the last sync requested another sync to be done (only a couple of times)
|
|
//
|
|
// Note that the etag check (see below) and the file watcher may also trigger
|
|
// syncs.
|
|
if (forceSyncIntervalExpired
|
|
|| _forceSyncOnPollTimeout
|
|
|| syncAgainAfterFail) {
|
|
|
|
if (forceSyncIntervalExpired) {
|
|
qDebug() << "** Force Sync, because it has been " << _timeSinceLastSyncDone.elapsed() << "ms "
|
|
<< "since the last sync";
|
|
}
|
|
if (_forceSyncOnPollTimeout) {
|
|
qDebug() << "** Force Sync, because it was requested";
|
|
}
|
|
if (syncAgainAfterFail) {
|
|
qDebug() << "** Force Sync, because the last"
|
|
<< _consecutiveFailingSyncs << "syncs failed, last status:"
|
|
<< _syncResult.statusString();
|
|
}
|
|
_forceSyncOnPollTimeout = false;
|
|
emit scheduleToSync(this);
|
|
|
|
} else {
|
|
// Do the ordinary etag check for the root folder and only schedule a real
|
|
// sync if it's different.
|
|
|
|
_requestEtagJob = new RequestEtagJob(account, remotePath(), this);
|
|
// check if the etag is different
|
|
QObject::connect(_requestEtagJob, SIGNAL(etagRetreived(QString)), this, SLOT(etagRetreived(QString)));
|
|
FolderMan::instance()->slotScheduleETagJob(alias(), _requestEtagJob);
|
|
// The _requestEtagJob is auto deleting itself on finish. Our guard pointer _requestEtagJob will then be null.
|
|
}
|
|
}
|
|
|
|
void Folder::etagRetreived(const QString& etag)
|
|
{
|
|
qDebug() << "* Compare etag with previous etag: last:" << _lastEtag << ", received:" << etag;
|
|
|
|
// re-enable sync if it was disabled because network was down
|
|
FolderMan::instance()->setSyncEnabled(true);
|
|
|
|
if (_lastEtag != etag) {
|
|
_lastEtag = etag;
|
|
emit scheduleToSync(this);
|
|
}
|
|
}
|
|
|
|
void Folder::etagRetreivedFromSyncEngine(const QString& etag)
|
|
{
|
|
qDebug() << "Root etag from during sync:" << etag;
|
|
_lastEtag = etag;
|
|
}
|
|
|
|
|
|
void Folder::bubbleUpSyncResult()
|
|
{
|
|
// count new, removed and updated items
|
|
int newItems = 0;
|
|
int removedItems = 0;
|
|
int updatedItems = 0;
|
|
int ignoredItems = 0;
|
|
int renamedItems = 0;
|
|
int errorItems = 0;
|
|
|
|
SyncFileItemPtr firstItemNew;
|
|
SyncFileItemPtr firstItemDeleted;
|
|
SyncFileItemPtr firstItemUpdated;
|
|
SyncFileItemPtr firstItemRenamed;
|
|
SyncFileItemPtr firstItemError;
|
|
|
|
SyncRunFileLog syncFileLog;
|
|
|
|
syncFileLog.start(path(), _engine ? _engine->stopWatch() : Utility::StopWatch() );
|
|
|
|
QElapsedTimer timer;
|
|
timer.start();
|
|
|
|
foreach (const SyncFileItemPtr &item, _syncResult.syncFileItemVector() ) {
|
|
// Log the item
|
|
syncFileLog.logItem( *item );
|
|
|
|
// and process the item to the gui
|
|
if( item->_status == SyncFileItem::FatalError || item->_status == SyncFileItem::NormalError ) {
|
|
slotSyncError( tr("%1: %2").arg(item->_file, item->_errorString) );
|
|
errorItems++;
|
|
if (!firstItemError) {
|
|
firstItemError = item;
|
|
}
|
|
} else if( item->_status == SyncFileItem::FileIgnored ) {
|
|
// ignored files don't show up in notifications
|
|
continue;
|
|
} else {
|
|
// add new directories or remove gone away dirs to the watcher
|
|
if (item->_isDirectory && item->_instruction == CSYNC_INSTRUCTION_NEW ) {
|
|
FolderMan::instance()->addMonitorPath( alias(), path()+item->_file );
|
|
}
|
|
if (item->_isDirectory && item->_instruction == CSYNC_INSTRUCTION_REMOVE ) {
|
|
FolderMan::instance()->removeMonitorPath( alias(), path()+item->_file );
|
|
}
|
|
|
|
if (!item->hasErrorStatus() && item->_direction == SyncFileItem::Down) {
|
|
switch (item->_instruction) {
|
|
case CSYNC_INSTRUCTION_NEW:
|
|
newItems++;
|
|
if (!firstItemNew)
|
|
firstItemNew = item;
|
|
break;
|
|
case CSYNC_INSTRUCTION_REMOVE:
|
|
removedItems++;
|
|
if (!firstItemDeleted)
|
|
firstItemDeleted = item;
|
|
break;
|
|
case CSYNC_INSTRUCTION_CONFLICT:
|
|
case CSYNC_INSTRUCTION_SYNC:
|
|
updatedItems++;
|
|
if (!firstItemUpdated)
|
|
firstItemUpdated = item;
|
|
break;
|
|
case CSYNC_INSTRUCTION_ERROR:
|
|
qDebug() << "Got Instruction ERROR. " << _syncResult.errorString();
|
|
break;
|
|
case CSYNC_INSTRUCTION_RENAME:
|
|
if (!firstItemRenamed) {
|
|
firstItemRenamed = item;
|
|
}
|
|
renamedItems++;
|
|
break;
|
|
default:
|
|
// nothing.
|
|
break;
|
|
}
|
|
} else if( item->_direction == SyncFileItem::None ) { // ignored files counting.
|
|
if( item->_instruction == CSYNC_INSTRUCTION_IGNORE ) {
|
|
ignoredItems++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
syncFileLog.close();
|
|
|
|
qDebug() << "Processing result list and logging took " << timer.elapsed() << " Milliseconds.";
|
|
_syncResult.setWarnCount(ignoredItems);
|
|
|
|
if( firstItemNew ) {
|
|
createGuiLog( firstItemNew->_file, SyncFileStatus::STATUS_NEW, newItems );
|
|
}
|
|
if( firstItemDeleted ) {
|
|
createGuiLog( firstItemDeleted->_file, SyncFileStatus::STATUS_REMOVE, removedItems );
|
|
}
|
|
if( firstItemUpdated ) {
|
|
createGuiLog( firstItemUpdated->_file, SyncFileStatus::STATUS_UPDATED, updatedItems );
|
|
}
|
|
|
|
if( firstItemRenamed ) {
|
|
SyncFileStatus status(SyncFileStatus::STATUS_RENAME);
|
|
// if the path changes it's rather a move
|
|
QDir renTarget = QFileInfo(firstItemRenamed->_renameTarget).dir();
|
|
QDir renSource = QFileInfo(firstItemRenamed->_file).dir();
|
|
if(renTarget != renSource) {
|
|
status.set(SyncFileStatus::STATUS_MOVE);
|
|
}
|
|
createGuiLog( firstItemRenamed->_file, status, renamedItems, firstItemRenamed->_renameTarget );
|
|
}
|
|
|
|
createGuiLog( firstItemError->_file, SyncFileStatus::STATUS_ERROR, errorItems );
|
|
|
|
qDebug() << "OO folder slotSyncFinished: result: " << int(_syncResult.status());
|
|
}
|
|
|
|
void Folder::createGuiLog( const QString& filename, SyncFileStatus status, int count,
|
|
const QString& renameTarget )
|
|
{
|
|
if(count > 0) {
|
|
Logger *logger = Logger::instance();
|
|
|
|
QString file = QDir::toNativeSeparators(filename);
|
|
QString text;
|
|
|
|
// not all possible values of status are evaluated here because the others
|
|
// are not used in the calling function. Please check there.
|
|
switch (status.tag()) {
|
|
case SyncFileStatus::STATUS_REMOVE:
|
|
if( count > 1 ) {
|
|
text = tr("%1 and %2 other files have been removed.", "%1 names a file.").arg(file).arg(count-1);
|
|
} else {
|
|
text = tr("%1 has been removed.", "%1 names a file.").arg(file);
|
|
}
|
|
break;
|
|
case SyncFileStatus::STATUS_NEW:
|
|
if( count > 1 ) {
|
|
text = tr("%1 and %2 other files have been downloaded.", "%1 names a file.").arg(file).arg(count-1);
|
|
} else {
|
|
text = tr("%1 has been downloaded.", "%1 names a file.").arg(file);
|
|
}
|
|
break;
|
|
case SyncFileStatus::STATUS_UPDATED:
|
|
if( count > 1 ) {
|
|
text = tr("%1 and %2 other files have been updated.").arg(file).arg(count-1);
|
|
} else {
|
|
text = tr("%1 has been updated.", "%1 names a file.").arg(file);
|
|
}
|
|
break;
|
|
case SyncFileStatus::STATUS_RENAME:
|
|
if( count > 1 ) {
|
|
text = tr("%1 has been renamed to %2 and %3 other files have been renamed.").arg(file).arg(renameTarget).arg(count-1);
|
|
} else {
|
|
text = tr("%1 has been renamed to %2.", "%1 and %2 name files.").arg(file).arg(renameTarget);
|
|
}
|
|
break;
|
|
case SyncFileStatus::STATUS_MOVE:
|
|
if( count > 1 ) {
|
|
text = tr("%1 has been moved to %2 and %3 other files have been moved.").arg(file).arg(renameTarget).arg(count-1);
|
|
} else {
|
|
text = tr("%1 has been moved to %2.").arg(file).arg(renameTarget);
|
|
}
|
|
break;
|
|
case SyncFileStatus::STATUS_ERROR:
|
|
if( count > 1 ) {
|
|
text = tr("%1 and %2 other files could not be synced due to errors. See the log for details.", "%1 names a file.").arg(file).arg(count-1);
|
|
} else {
|
|
text = tr("%1 could not be synced due to an error. See the log for details.").arg(file);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if( !text.isEmpty() ) {
|
|
logger->postOptionalGuiLog( tr("Sync Activity"), text );
|
|
}
|
|
}
|
|
}
|
|
|
|
int Folder::slotDiscardDownloadProgress()
|
|
{
|
|
// Delete from journal and from filesystem.
|
|
QDir folderpath(_definition.localPath);
|
|
QSet<QString> keep_nothing;
|
|
const QVector<SyncJournalDb::DownloadInfo> deleted_infos =
|
|
_journal.getAndDeleteStaleDownloadInfos(keep_nothing);
|
|
foreach (const SyncJournalDb::DownloadInfo & deleted_info, deleted_infos) {
|
|
const QString tmppath = folderpath.filePath(deleted_info._tmpfile);
|
|
qDebug() << "Deleting temporary file: " << tmppath;
|
|
QFile::remove(tmppath);
|
|
}
|
|
return deleted_infos.size();
|
|
}
|
|
|
|
int Folder::downloadInfoCount()
|
|
{
|
|
return _journal.downloadInfoCount();
|
|
}
|
|
|
|
int Folder::errorBlackListEntryCount()
|
|
{
|
|
return _journal.errorBlackListEntryCount();
|
|
}
|
|
|
|
int Folder::slotWipeErrorBlacklist()
|
|
{
|
|
return _journal.wipeErrorBlacklist();
|
|
}
|
|
|
|
void Folder::slotWatchedPathChanged(const QString& path)
|
|
{
|
|
// When no sync is running or it's in the prepare phase, we can
|
|
// always schedule a new sync.
|
|
if (! _engine || _syncResult.status() == SyncResult::SyncPrepare) {
|
|
emit scheduleToSync(this);
|
|
return;
|
|
}
|
|
|
|
// The folder watcher fires a lot of bogus notifications during
|
|
// a sync operation, both for actual user files and the database
|
|
// and log. Therefore we check notifications against operations
|
|
// the sync is doing to filter out our own changes.
|
|
bool ownChange = false;
|
|
#ifdef Q_OS_MAC
|
|
// On OSX the folder watcher does not report changes done by our
|
|
// own process. Therefore nothing needs to be done here!
|
|
#else
|
|
// Use the path to figure out whether it was our own change
|
|
const auto maxNotificationDelay = 15*1000;
|
|
qint64 time = _engine->timeSinceFileTouched(path);
|
|
if (time != -1 && time < maxNotificationDelay) {
|
|
ownChange = true;
|
|
}
|
|
#endif
|
|
|
|
if (! ownChange) {
|
|
emit scheduleToSync(this);
|
|
}
|
|
}
|
|
|
|
static void addErroredSyncItemPathsToList(const SyncFileItemVector& items, QSet<QString>* set) {
|
|
Q_FOREACH(const SyncFileItemPtr &item, items) {
|
|
if (item->hasErrorStatus()) {
|
|
set->insert(item->_file);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Folder::slotThreadTreeWalkResult(const SyncFileItemVector& items)
|
|
{
|
|
addErroredSyncItemPathsToList(items, &this->_stateLastSyncItemsWithError);
|
|
_syncResult.setSyncFileItemVector(items);
|
|
}
|
|
|
|
void Folder::slotAboutToPropagate(SyncFileItemVector& items)
|
|
{
|
|
// empty the tainted list since the status generation code will use the _syncedItems
|
|
// (which imply the folder) to generate the syncing state icon now.
|
|
_stateTaintedFolders.clear();
|
|
|
|
addErroredSyncItemPathsToList(items, &this->_stateLastSyncItemsWithError);
|
|
}
|
|
|
|
|
|
bool Folder::estimateState(QString fn, csync_ftw_type_e t, SyncFileStatus* s)
|
|
{
|
|
if (t == CSYNC_FTW_TYPE_DIR) {
|
|
if (Utility::doesSetContainPrefix(_stateLastSyncItemsWithError, fn)) {
|
|
qDebug() << Q_FUNC_INFO << "Folder has error" << fn;
|
|
s->set(SyncFileStatus::STATUS_ERROR);
|
|
return true;
|
|
}
|
|
// If sync is running, check _syncedItems, possibly give it STATUS_EVAL (=syncing down)
|
|
if (!_engine.isNull()) {
|
|
if (_engine->estimateState(fn, t, s)) {
|
|
return true;
|
|
}
|
|
}
|
|
if(!fn.endsWith(QLatin1Char('/'))) {
|
|
fn.append(QLatin1Char('/'));
|
|
}
|
|
if (Utility::doesSetContainPrefix(_stateTaintedFolders, fn)) {
|
|
qDebug() << Q_FUNC_INFO << "Folder is tainted, EVAL!" << fn;
|
|
s->set(SyncFileStatus::STATUS_EVAL);
|
|
return true;
|
|
}
|
|
return false;
|
|
} else if ( t== CSYNC_FTW_TYPE_FILE) {
|
|
// check if errorList has the directory/file
|
|
if (Utility::doesSetContainPrefix(_stateLastSyncItemsWithError, fn)) {
|
|
s->set(SyncFileStatus::STATUS_ERROR);
|
|
return true;
|
|
}
|
|
// If sync running: _syncedItems -> SyncingState
|
|
if (!_engine.isNull()) {
|
|
if (_engine->estimateState(fn, t, s)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Folder::saveToSettings() const
|
|
{
|
|
QScopedPointer<QSettings> settings(_accountState->account()->settings());
|
|
settings->beginGroup(QLatin1String("Folders"));
|
|
FolderDefinition::save(*settings, _definition);
|
|
}
|
|
|
|
void Folder::removeFromSettings() const
|
|
{
|
|
QScopedPointer<QSettings> settings(_accountState->account()->settings());
|
|
settings->beginGroup(QLatin1String("Folders"));
|
|
settings->remove(_definition.alias);
|
|
}
|
|
|
|
void Folder::watcherSlot(QString fn)
|
|
{
|
|
// FIXME: On OS X we could not do this "if" since on OS X the file watcher ignores events for ourselves
|
|
// however to have the same behaviour atm on all platforms, we don't do it
|
|
if (!_engine.isNull()) {
|
|
qDebug() << Q_FUNC_INFO << "Sync running, IGNORE event for " << fn;
|
|
return;
|
|
}
|
|
QFileInfo fi(fn);
|
|
if (fi.isFile()) {
|
|
fn = fi.path(); // depending on OS, file watcher might be for dir or file
|
|
}
|
|
// Make it a relative path depending on the folder
|
|
QString relativePath = fn.remove(0, path().length());
|
|
if( !relativePath.endsWith(QLatin1Char('/'))) {
|
|
relativePath.append(QLatin1Char('/'));
|
|
}
|
|
qDebug() << Q_FUNC_INFO << fi.canonicalFilePath() << fn << relativePath;
|
|
_stateTaintedFolders.insert(relativePath);
|
|
|
|
// Notify the SocketAPI?
|
|
}
|
|
|
|
|
|
|
|
void Folder::slotTerminateSync()
|
|
{
|
|
qDebug() << "folder " << alias() << " Terminating!";
|
|
|
|
if( _engine ) {
|
|
_engine->abort();
|
|
|
|
// Do not display an error message, user knows his own actions.
|
|
// _errors.append( tr("The CSync thread terminated.") );
|
|
// _csyncError = true;
|
|
setSyncState(SyncResult::SyncAbortRequested);
|
|
}
|
|
}
|
|
|
|
// This removes the csync File database
|
|
// This is needed to provide a clean startup again in case another
|
|
// local folder is synced to the same ownCloud.
|
|
void Folder::wipe()
|
|
{
|
|
QString stateDbFile = path()+QLatin1String(".csync_journal.db");
|
|
|
|
// Delete files that have been partially downloaded.
|
|
slotDiscardDownloadProgress();
|
|
|
|
_journal.close(); // close the sync journal
|
|
|
|
QFile file(stateDbFile);
|
|
if( file.exists() ) {
|
|
if( !file.remove()) {
|
|
qDebug() << "WRN: Failed to remove existing csync StateDB " << stateDbFile;
|
|
} else {
|
|
qDebug() << "wipe: Removed csync StateDB " << stateDbFile;
|
|
}
|
|
} else {
|
|
qDebug() << "WRN: statedb is empty, can not remove.";
|
|
}
|
|
|
|
// Also remove other db related files
|
|
QFile::remove( stateDbFile + ".ctmp" );
|
|
QFile::remove( stateDbFile + "-shm" );
|
|
QFile::remove( stateDbFile + "-wal" );
|
|
QFile::remove( stateDbFile + "-journal" );
|
|
}
|
|
|
|
bool Folder::setIgnoredFiles()
|
|
{
|
|
bool ok = false;
|
|
|
|
ConfigFile cfgFile;
|
|
csync_clear_exclude_list( _csync_ctx );
|
|
QString excludeList = cfgFile.excludeFile( ConfigFile::SystemScope );
|
|
if( !excludeList.isEmpty() ) {
|
|
qDebug() << "==== added system ignore list to csync:" << excludeList.toUtf8();
|
|
if (csync_add_exclude_list( _csync_ctx, excludeList.toUtf8() ) == 0) {
|
|
ok = true;
|
|
}
|
|
}
|
|
excludeList = cfgFile.excludeFile( ConfigFile::UserScope );
|
|
if( !excludeList.isEmpty() ) {
|
|
qDebug() << "==== added user defined ignore list to csync:" << excludeList.toUtf8();
|
|
csync_add_exclude_list( _csync_ctx, excludeList.toUtf8() );
|
|
// reading the user exclude file is optional
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
void Folder::setProxyDirty(bool value)
|
|
{
|
|
_proxyDirty = value;
|
|
}
|
|
|
|
bool Folder::proxyDirty()
|
|
{
|
|
return _proxyDirty;
|
|
}
|
|
|
|
void Folder::startSync(const QStringList &pathList)
|
|
{
|
|
Q_UNUSED(pathList)
|
|
if (!_csync_ctx) {
|
|
// no _csync_ctx yet, initialize it.
|
|
init();
|
|
|
|
if (!_csync_ctx) {
|
|
qDebug() << Q_FUNC_INFO << "init failed.";
|
|
// the error should already be set
|
|
QMetaObject::invokeMethod(this, "slotSyncFinished", Qt::QueuedConnection);
|
|
return;
|
|
}
|
|
_clientProxy.setCSyncProxy(_accountState->account()->url(), _csync_ctx);
|
|
} else if (proxyDirty()) {
|
|
_clientProxy.setCSyncProxy(_accountState->account()->url(), _csync_ctx);
|
|
setProxyDirty(false);
|
|
}
|
|
|
|
if (isBusy()) {
|
|
qCritical() << "* ERROR csync is still running and new sync requested.";
|
|
return;
|
|
}
|
|
_errors.clear();
|
|
_csyncError = false;
|
|
_csyncUnavail = false;
|
|
|
|
_timeSinceLastSyncStart.restart();
|
|
_syncResult.clearErrors();
|
|
_syncResult.setStatus( SyncResult::SyncPrepare );
|
|
_syncResult.setSyncFileItemVector(SyncFileItemVector());
|
|
emit syncStateChange();
|
|
|
|
qDebug() << "*** Start syncing - client version"
|
|
<< qPrintable(Theme::instance()->version());
|
|
|
|
if (! setIgnoredFiles())
|
|
{
|
|
slotSyncError(tr("Could not read system exclude file"));
|
|
QMetaObject::invokeMethod(this, "slotSyncFinished", Qt::QueuedConnection);
|
|
return;
|
|
}
|
|
|
|
_engine.reset(new SyncEngine( _accountState->account(), _csync_ctx, path(), remoteUrl().path(), remotePath(), &_journal));
|
|
|
|
qRegisterMetaType<SyncFileItemVector>("SyncFileItemVector");
|
|
qRegisterMetaType<SyncFileItem::Direction>("SyncFileItem::Direction");
|
|
|
|
connect(_engine.data(), SIGNAL(rootEtag(QString)), this, SLOT(etagRetreivedFromSyncEngine(QString)));
|
|
connect( _engine.data(), SIGNAL(treeWalkResult(const SyncFileItemVector&)),
|
|
this, SLOT(slotThreadTreeWalkResult(const SyncFileItemVector&)), Qt::QueuedConnection);
|
|
connect( _engine.data(), SIGNAL(aboutToPropagate(SyncFileItemVector&)),
|
|
this, SLOT(slotAboutToPropagate(SyncFileItemVector&)));
|
|
|
|
connect(_engine.data(), SIGNAL(started()), SLOT(slotSyncStarted()), Qt::QueuedConnection);
|
|
connect(_engine.data(), SIGNAL(finished()), SLOT(slotSyncFinished()), Qt::QueuedConnection);
|
|
connect(_engine.data(), SIGNAL(csyncError(QString)), SLOT(slotSyncError(QString)), Qt::QueuedConnection);
|
|
connect(_engine.data(), SIGNAL(csyncUnavailable()), SLOT(slotCsyncUnavailable()), Qt::QueuedConnection);
|
|
|
|
//direct connection so the message box is blocking the sync.
|
|
connect(_engine.data(), SIGNAL(aboutToRemoveAllFiles(SyncFileItem::Direction,bool*)),
|
|
SLOT(slotAboutToRemoveAllFiles(SyncFileItem::Direction,bool*)));
|
|
connect(_engine.data(), SIGNAL(folderDiscovered(bool,QString)), this, SLOT(slotFolderDiscovered(bool,QString)));
|
|
connect(_engine.data(), SIGNAL(transmissionProgress(ProgressInfo)), this, SLOT(slotTransmissionProgress(ProgressInfo)));
|
|
connect(_engine.data(), SIGNAL(jobCompleted(const SyncFileItem &)), this, SLOT(slotJobCompleted(const SyncFileItem &)));
|
|
connect(_engine.data(), SIGNAL(syncItemDiscovered(const SyncFileItem &)), this, SLOT(slotSyncItemDiscovered(const SyncFileItem &)));
|
|
|
|
setDirtyNetworkLimits();
|
|
_engine->setSelectiveSyncBlackList(selectiveSyncBlackList());
|
|
|
|
QMetaObject::invokeMethod(_engine.data(), "startSync", Qt::QueuedConnection);
|
|
|
|
// disable events until syncing is done
|
|
// _watcher->setEventsEnabled(false);
|
|
emit syncStarted();
|
|
}
|
|
|
|
void Folder::setDirtyNetworkLimits()
|
|
{
|
|
if (_engine) {
|
|
|
|
ConfigFile cfg;
|
|
int downloadLimit = 0;
|
|
if (cfg.useDownloadLimit()) {
|
|
downloadLimit = cfg.downloadLimit() * 1000;
|
|
}
|
|
int uploadLimit = -75; // 75%
|
|
int useUpLimit = cfg.useUploadLimit();
|
|
if ( useUpLimit >= 1) {
|
|
uploadLimit = cfg.uploadLimit() * 1000;
|
|
} else if (useUpLimit == 0) {
|
|
uploadLimit = 0;
|
|
}
|
|
|
|
_engine->setNetworkLimits(uploadLimit, downloadLimit);
|
|
}
|
|
}
|
|
|
|
void Folder::setSelectiveSyncBlackList(const QStringList& blackList)
|
|
{
|
|
_definition.selectiveSyncBlackList = blackList;
|
|
for (int i = 0; i < blackList.count(); ++i) {
|
|
if (!blackList.at(i).endsWith(QLatin1Char('/'))) {
|
|
_definition.selectiveSyncBlackList[i].append(QLatin1Char('/'));
|
|
}
|
|
}
|
|
saveToSettings();
|
|
}
|
|
|
|
|
|
void Folder::slotSyncError(const QString& err)
|
|
{
|
|
_errors.append( err );
|
|
_csyncError = true;
|
|
}
|
|
|
|
void Folder::slotSyncStarted()
|
|
{
|
|
qDebug() << "#### Propagation start #################################################### >>";
|
|
_syncResult.setStatus(SyncResult::SyncRunning);
|
|
emit syncStateChange();
|
|
}
|
|
|
|
void Folder::slotCsyncUnavailable()
|
|
{
|
|
_csyncUnavail = true;
|
|
}
|
|
|
|
void Folder::slotSyncFinished()
|
|
{
|
|
if( _csyncError ) {
|
|
qDebug() << "-> SyncEngine finished with ERROR, warn count is" << _syncResult.warnCount();
|
|
} else {
|
|
qDebug() << "-> SyncEngine finished without problem.";
|
|
}
|
|
bubbleUpSyncResult();
|
|
|
|
bool anotherSyncNeeded = false;
|
|
if (_engine) {
|
|
anotherSyncNeeded = _engine->isAnotherSyncNeeded();
|
|
_engine.reset(0);
|
|
}
|
|
// _watcher->setEventsEnabledDelayed(2000);
|
|
|
|
|
|
|
|
// This is for sync state calculation
|
|
_stateLastSyncItemsWithError = _stateLastSyncItemsWithErrorNew;
|
|
_stateLastSyncItemsWithErrorNew.clear();
|
|
_stateTaintedFolders.clear(); // heuristic: assume the sync had been done, new file watches needed to taint dirs
|
|
|
|
if (_csyncError) {
|
|
_syncResult.setStatus(SyncResult::Error);
|
|
qDebug() << " ** error Strings: " << _errors;
|
|
_syncResult.setErrorStrings( _errors );
|
|
qDebug() << " * owncloud csync thread finished with error";
|
|
} else if (_csyncUnavail) {
|
|
_syncResult.setStatus(SyncResult::Error);
|
|
qDebug() << " ** csync not available.";
|
|
} else if( _syncResult.warnCount() > 0 ) {
|
|
// there have been warnings on the way.
|
|
_syncResult.setStatus(SyncResult::Problem);
|
|
} else {
|
|
_syncResult.setStatus(SyncResult::Success);
|
|
}
|
|
|
|
// Count the number of syncs that have failed in a row.
|
|
if (_syncResult.status() == SyncResult::Success
|
|
|| _syncResult.status() == SyncResult::Problem)
|
|
{
|
|
_consecutiveFailingSyncs = 0;
|
|
}
|
|
else
|
|
{
|
|
_consecutiveFailingSyncs++;
|
|
qDebug() << "the last" << _consecutiveFailingSyncs << "syncs failed";
|
|
}
|
|
|
|
emit syncStateChange();
|
|
|
|
// The syncFinished result that is to be triggered here makes the folderman
|
|
// clearing the current running sync folder marker.
|
|
// Lets wait a bit to do that because, as long as this marker is not cleared,
|
|
// file system change notifications are ignored for that folder. And it takes
|
|
// some time under certain conditions to make the file system notifications
|
|
// all come in.
|
|
QTimer::singleShot(200, this, SLOT(slotEmitFinishedDelayed() ));
|
|
|
|
_lastSyncDuration = _timeSinceLastSyncStart.elapsed();
|
|
_timeSinceLastSyncDone.restart();
|
|
|
|
// Increment the follow-up sync counter if necessary.
|
|
if (anotherSyncNeeded) {
|
|
_consecutiveFollowUpSyncs++;
|
|
qDebug() << "another sync was requested by the finished sync, this has"
|
|
<< "happened" << _consecutiveFollowUpSyncs << "times";
|
|
} else {
|
|
_consecutiveFollowUpSyncs = 0;
|
|
}
|
|
|
|
// Maybe force a follow-up sync to take place, but only a couple of times.
|
|
if (anotherSyncNeeded && _consecutiveFollowUpSyncs <= 3)
|
|
{
|
|
_forceSyncOnPollTimeout = true;
|
|
// We will make sure that the poll timer occurs soon enough.
|
|
// delay 1s, 4s, 9s
|
|
int c = _consecutiveFollowUpSyncs;
|
|
QTimer::singleShot(c*c * 1000, this, SLOT(slotRunEtagJob() ));
|
|
}
|
|
}
|
|
|
|
void Folder::slotEmitFinishedDelayed()
|
|
{
|
|
emit syncFinished( _syncResult );
|
|
}
|
|
|
|
|
|
void Folder::slotFolderDiscovered(bool, QString folderName)
|
|
{
|
|
ProgressInfo pi;
|
|
pi._currentDiscoveredFolder = folderName;
|
|
ProgressDispatcher::instance()->setProgressInfo(alias(), pi);
|
|
}
|
|
|
|
|
|
// the progress comes without a folder and the valid path set. Add that here
|
|
// and hand the result over to the progress dispatcher.
|
|
void Folder::slotTransmissionProgress(const ProgressInfo &pi)
|
|
{
|
|
if( !pi.hasStarted() ) {
|
|
// this is the beginning of a sync, set the warning level to 0
|
|
_syncResult.setWarnCount(0);
|
|
}
|
|
ProgressDispatcher::instance()->setProgressInfo(alias(), pi);
|
|
}
|
|
|
|
// a job is completed: count the errors and forward to the ProgressDispatcher
|
|
void Folder::slotJobCompleted(const SyncFileItem &item)
|
|
{
|
|
if (item.hasErrorStatus()) {
|
|
_stateLastSyncItemsWithError.insert(item._file);
|
|
}
|
|
|
|
if (Progress::isWarningKind(item._status)) {
|
|
// Count all error conditions.
|
|
_syncResult.setWarnCount(_syncResult.warnCount()+1);
|
|
}
|
|
emit ProgressDispatcher::instance()->jobCompleted(alias(), item);
|
|
}
|
|
|
|
void Folder::slotSyncItemDiscovered(const SyncFileItem & item)
|
|
{
|
|
emit ProgressDispatcher::instance()->syncItemDiscovered(alias(), item);
|
|
}
|
|
|
|
|
|
void Folder::slotAboutToRemoveAllFiles(SyncFileItem::Direction, bool *cancel)
|
|
{
|
|
QString msg =
|
|
tr("This sync would remove all the files in the sync folder '%1'.\n"
|
|
"This might be because the folder was silently reconfigured, or that all "
|
|
"the file were manually removed.\n"
|
|
"Are you sure you want to perform this operation?");
|
|
QMessageBox msgBox(QMessageBox::Warning, tr("Remove All Files?"),
|
|
msg.arg(alias()));
|
|
msgBox.addButton(tr("Remove all files"), QMessageBox::DestructiveRole);
|
|
QPushButton* keepBtn = msgBox.addButton(tr("Keep files"), QMessageBox::ActionRole);
|
|
if (msgBox.exec() == -1) {
|
|
*cancel = true;
|
|
return;
|
|
}
|
|
*cancel = msgBox.clickedButton() == keepBtn;
|
|
if (*cancel) {
|
|
wipe();
|
|
// speed up next sync
|
|
_lastEtag.clear();
|
|
_forceSyncOnPollTimeout = true;
|
|
QTimer::singleShot(50, this, SLOT(slotRunEtagJob()));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void FolderDefinition::save(QSettings& settings, const FolderDefinition& folder)
|
|
{
|
|
settings.beginGroup(folder.alias);
|
|
settings.setValue(QLatin1String("localPath"), folder.localPath);
|
|
settings.setValue(QLatin1String("targetPath"), folder.targetPath);
|
|
settings.setValue(QLatin1String("blackList"), folder.selectiveSyncBlackList);
|
|
settings.setValue(QLatin1String("paused"), folder.paused);
|
|
settings.endGroup();
|
|
}
|
|
|
|
bool FolderDefinition::load(QSettings& settings, const QString& alias,
|
|
FolderDefinition* folder)
|
|
{
|
|
settings.beginGroup(alias);
|
|
folder->alias = alias;
|
|
folder->localPath = settings.value(QLatin1String("localPath")).toString();
|
|
folder->targetPath = settings.value(QLatin1String("targetPath")).toString();
|
|
folder->selectiveSyncBlackList = settings.value(QLatin1String("blackList")).toStringList();
|
|
folder->paused = settings.value(QLatin1String("paused")).toBool();
|
|
settings.endGroup();
|
|
return true;
|
|
}
|
|
|
|
} // namespace OCC
|
|
|