New Discovery Algo: Permsission check

This commit is contained in:
Olivier Goffart 2018-07-25 18:31:36 +02:00
parent 04da4365f9
commit 01f35bdbef
5 changed files with 146 additions and 277 deletions

View File

@ -123,16 +123,6 @@ if(OWNCLOUD_5XX_NO_BLACKLIST)
add_definitions(-DOWNCLOUD_5XX_NO_BLACKLIST=1)
endif()
# When this option is enabled, a rename that is not allowed will be renamed back
# do the original as a restoration step. Withut this option, the restoration will
# re-download the file instead.
# The default is off because we don't want to rename the files back behind the user's back
# Added for IL issue #550
option(OWNCLOUD_RESTORE_RENAME "OWNCLOUD_RESTORE_RENAME" OFF)
if(OWNCLOUD_RESTORE_RENAME)
add_definitions(-DOWNCLOUD_RESTORE_RENAME=1)
endif()
# Disable shibboleth.
# So the client can be built without QtWebKit
option(NO_SHIBBOLETH "Build without Shibboleth support. Allow to build the client without QtWebKit" OFF)

View File

@ -35,7 +35,8 @@ void ProcessDirectoryJob::start()
DiscoverySingleDirectoryJob *serverJob = nullptr;
if (_queryServer == NormalQuery) {
serverJob = new DiscoverySingleDirectoryJob(_discoveryData->_account, _discoveryData->_remoteFolder + _currentFolder._server, this);
serverJob = new DiscoverySingleDirectoryJob(_discoveryData->_account,
_discoveryData->_remoteFolder + _currentFolder._server, this);
connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this](const auto &results) {
if (results) {
_serverEntries = *results;
@ -71,6 +72,8 @@ void ProcessDirectoryJob::start()
emit finished();
}
});
connect(serverJob, &DiscoverySingleDirectoryJob::firstDirectoryPermissions, this,
[this](const RemotePermissions &perms) { _rootPermissions = perms; });
serverJob->start();
} else {
_hasServerEntries = true;
@ -546,7 +549,6 @@ void ProcessDirectoryJob::processFile(PathTuple path,
}
qCInfo(lcDisco) << "Discovered" << item->_file << item->_instruction << item->_direction << item->isDirectory();
if (item->isDirectory()) {
auto job = new ProcessDirectoryJob(item, recurseQueryServer,
item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist,
@ -733,40 +735,78 @@ void ProcessDirectoryJob::processFile(PathTuple path,
if (!_dirItem || _dirItem->_direction == SyncFileItem::Down) {
_childModified = true;
}
// Check if it is a rename
// Check if it is a move
OCC::SyncJournalFileRecord base;
if (!_discoveryData->_statedb->getFileRecordByInode(localEntry.inode, &base)) {
qFatal("TODO: handle DB Errors");
}
bool isRename = base.isValid() && base._type == item->_type
bool isMove = base.isValid() && base._type == item->_type
&& ((base._modtime == localEntry.modtime && base._fileSize == localEntry.size) || item->_type == ItemTypeDirectory);
if (isRename) {
if (isMove) {
// The old file must have been deleted.
isRename = !QFile::exists(_discoveryData->_localDir + base._path);
isMove = !QFile::exists(_discoveryData->_localDir + base._path);
}
// Verify the checksum where possible
if (isRename && !base._checksumHeader.isEmpty() && item->_type == ItemTypeFile) {
if (isMove && !base._checksumHeader.isEmpty() && item->_type == ItemTypeFile) {
if (computeLocalChecksum(parseChecksumHeaderType(base._checksumHeader), path._original)) {
qCInfo(lcDisco) << "checking checksum of potential rename " << path._original << item->_checksumHeader << base._checksumHeader;
isRename = item->_checksumHeader == base._checksumHeader;
isMove = item->_checksumHeader == base._checksumHeader;
}
}
auto originalPath = QString::fromUtf8(base._path);
if (isRename && _discoveryData->_renamedItems.contains(originalPath))
isRename = false;
if (isRename) {
if (isMove && _discoveryData->_renamedItems.contains(originalPath))
isMove = false;
//Check local permission if we are allowed to put move the file here
if (isMove) {
auto destPerms = !_rootPermissions.isNull() ? _rootPermissions
: _dirItem ? _dirItem->_remotePerm : _rootPermissions;
auto filePerms = base._remotePerm; // Technicly we should use the one from the server, but we'll assume it is the same
//true when it is just a rename in the same directory. (not a move)
bool isRename = originalPath.startsWith(_currentFolder._original)
&& originalPath.lastIndexOf('/') == _currentFolder._original.size();
// Check if we are allowed to move to the destination.
bool destinationOK = true;
if (isRename || destPerms.isNull()) {
// no need to check for the destination dir permission
destinationOK = true;
} else if (item->isDirectory() && !destPerms.hasPermission(RemotePermissions::CanAddSubDirectories)) {
destinationOK = false;
} else if (!item->isDirectory() && !destPerms.hasPermission(RemotePermissions::CanAddFile)) {
destinationOK = false;
}
// check if we are allowed to move from the source
bool sourceOK = true;
if (!filePerms.isNull()
&& ((isRename && !filePerms.hasPermission(RemotePermissions::CanRename))
|| (!isRename && !filePerms.hasPermission(RemotePermissions::CanMove)))) {
// We are not allowed to move or rename this file
sourceOK = false;
}
if (!sourceOK || !destinationOK) {
qCInfo(lcDisco) << "Not a move because permission does not allow it." << sourceOK << destinationOK;
if (!sourceOK) {
// This is the behavior that we had in the client <= 2.5.
_discoveryData->_statedb->avoidRenamesOnNextSync(base._path);
}
isMove = false;
}
}
if (isMove) {
QByteArray oldEtag;
auto it = _discoveryData->_deletedItem.find(originalPath);
if (it != _discoveryData->_deletedItem.end()) {
if ((*it)->_instruction != CSYNC_INSTRUCTION_REMOVE)
isRename = false;
isMove = false;
else
(*it)->_instruction = CSYNC_INSTRUCTION_NONE;
oldEtag = (*it)->_etag;
if (!item->isDirectory() && oldEtag != base._etag) {
isRename = false;
isMove = false;
}
}
if (auto deleteJob = static_cast<ProcessDirectoryJob *>(_discoveryData->_queuedDeletedDirectories.value(originalPath).data())) {
@ -791,10 +831,10 @@ void ProcessDirectoryJob::processFile(PathTuple path,
path._server = adjustedOriginalPath;
qCInfo(lcDisco) << "Rename detected (up) " << item->_file << " -> " << item->_renameTarget;
};
if (isRename && !oldEtag.isEmpty()) {
if (isMove && !oldEtag.isEmpty()) {
recurseQueryServer = oldEtag == base._etag ? ParentNotChanged : NormalQuery;
processRename(path);
} else if (isRename) {
} else if (isMove) {
// We must query the server to know if the etag has not changed
_pendingAsyncJobs++;
auto job = new RequestEtagJob(_discoveryData->_account, originalPath, this);
@ -816,7 +856,8 @@ void ProcessDirectoryJob::processFile(PathTuple path,
}
qCInfo(lcDisco) << "Discovered" << item->_file << item->_instruction << item->_direction << item->isDirectory();
if (item->isDirectory()) {
bool recurse = checkPremission(item);
if (recurse && item->isDirectory()) {
auto job = new ProcessDirectoryJob(item, recurseQueryServer, NormalQuery, _discoveryData, this);
job->_currentFolder = path;
connect(job, &ProcessDirectoryJob::itemDiscovered, this, &ProcessDirectoryJob::itemDiscovered);
@ -875,7 +916,14 @@ void ProcessDirectoryJob::processFile(PathTuple path,
item->_type = ItemTypeVirtualFile;
item->_file.append(_discoveryData->_syncOptions._virtualFileSuffix);
} else if (!serverModified) {
if (!dbEntry._serverHasIgnoredFiles) {
// Removed locally: also remove on the server.
if (_dirItem && _dirItem->_isRestoration && _dirItem->_instruction == CSYNC_INSTRUCTION_NEW) {
// Also restore everything
item->_instruction = CSYNC_INSTRUCTION_NEW;
item->_direction = SyncFileItem::Down;
item->_isRestoration = true;
item->_errorString = tr("Not allowed to remove, restoring");
} else if (!dbEntry._serverHasIgnoredFiles) {
item->_instruction = CSYNC_INSTRUCTION_REMOVE;
item->_direction = SyncFileItem::Up;
}
@ -895,8 +943,11 @@ void ProcessDirectoryJob::processFile(PathTuple path,
if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_SYNC)
item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
bool recurse = item->isDirectory() || localEntry.isDirectory || serverEntry.isDirectory;
if (_queryLocal != NormalQuery && _queryServer != NormalQuery)
if (!checkPremission(item))
recurse = false;
if (_queryLocal != NormalQuery && _queryServer != NormalQuery && !item->_isRestoration)
recurse = false;
if (recurse) {
auto job = new ProcessDirectoryJob(item, recurseQueryServer,
@ -951,6 +1002,77 @@ void ProcessDirectoryJob::processBlacklisted(const PathTuple &path, const OCC::L
}
}
bool ProcessDirectoryJob::checkPremission(const OCC::SyncFileItemPtr &item)
{
if (item->_direction != SyncFileItem::Up) {
// Currently we only check server-side permissions
return true;
}
switch (item->_instruction) {
case CSYNC_INSTRUCTION_TYPE_CHANGE:
case CSYNC_INSTRUCTION_NEW: {
const auto perms = !_rootPermissions.isNull() ? _rootPermissions
: _dirItem ? _dirItem->_remotePerm : _rootPermissions;
if (perms.isNull()) {
// No permissions set
return true;
} else if (item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) {
qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
item->_instruction = CSYNC_INSTRUCTION_ERROR;
item->_status = SyncFileItem::NormalError;
item->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder");
return false;
} else if (!item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) {
qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
item->_instruction = CSYNC_INSTRUCTION_ERROR;
item->_status = SyncFileItem::NormalError;
item->_errorString = tr("Not allowed because you don't have permission to add files in that folder");
return false;
}
break;
}
case CSYNC_INSTRUCTION_SYNC: {
const auto perms = item->_remotePerm;
if (perms.isNull()) {
// No permissions set
return true;
}
if (!perms.hasPermission(RemotePermissions::CanWrite)) {
qCWarning(lcDisco) << "checkForPermission: RESTORING" << item->_file;
item->_instruction = CSYNC_INSTRUCTION_CONFLICT;
item->_errorString = tr("Not allowed to upload this file because it is read-only on the server, restoring");
item->_direction = SyncFileItem::Down;
item->_isRestoration = true;
// Take the things to write to the db from the "other" node (i.e: info from server).
// Do a lookup into the csync remote tree to get the metadata we need to restore.
qSwap(item->_size, item->_previousSize);
qSwap(item->_modtime, item->_previousModtime);
return false;
}
break;
}
case CSYNC_INSTRUCTION_REMOVE: {
const auto perms = item->_remotePerm;
if (perms.isNull()) {
// No permissions set
return true;
}
if (!perms.hasPermission(RemotePermissions::CanDelete)) {
qCWarning(lcDisco) << "checkForPermission: RESTORING" << item->_file;
item->_instruction = CSYNC_INSTRUCTION_NEW;
item->_direction = SyncFileItem::Down;
item->_isRestoration = true;
item->_errorString = tr("Not allowed to remove, restoring");
return true; // (we need to recurse to restore sub items)
}
break;
}
default:
break;
}
return true;
}
void ProcessDirectoryJob::subJobFinished()
{

View File

@ -75,12 +75,15 @@ private:
// return true if the file is excluded
bool handleExcluded(const QString &path, bool isDirectory, bool isHidden);
void processFile(PathTuple, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &dbEntry);
// Return false if there is an error and that a directory must not be recursively be taken
bool checkPremission(const SyncFileItemPtr &item);
void processBlacklisted(const PathTuple &, const LocalInfo &, const SyncJournalFileRecord &dbEntry);
void subJobFinished();
void progress();
QVector<RemoteInfo> _serverEntries;
QVector<LocalInfo> _localEntries;
RemotePermissions _rootPermissions;
bool _hasServerEntries = false;
bool _hasLocalEntries = false;
int _pendingAsyncJobs = 0;

View File

@ -250,13 +250,6 @@ static bool isFileTransferInstruction(csync_instructions_e instruction)
|| instruction == CSYNC_INSTRUCTION_TYPE_CHANGE;
}
static bool isFileModifyingInstruction(csync_instructions_e instruction)
{
return isFileTransferInstruction(instruction)
|| instruction == CSYNC_INSTRUCTION_RENAME
|| instruction == CSYNC_INSTRUCTION_REMOVE;
}
void SyncEngine::deleteStaleDownloadInfos(const SyncFileItemVector &syncItems)
{
// Find all downloadinfo paths that we want to preserve.
@ -1057,239 +1050,6 @@ void SyncEngine::slotProgress(const SyncFileItem &item, quint64 current)
}
/**
*
* Make sure that we are allowed to do what we do by checking the permissions and the selective sync list
*
*/
void SyncEngine::checkForPermission(SyncFileItemVector &syncItems)
{
bool selectiveListOk;
auto selectiveSyncBlackList = _journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &selectiveListOk);
std::sort(selectiveSyncBlackList.begin(), selectiveSyncBlackList.end());
SyncFileItemPtr needle;
for (SyncFileItemVector::iterator it = syncItems.begin(); it != syncItems.end(); ++it) {
if ((*it)->_direction != SyncFileItem::Up
|| !isFileModifyingInstruction((*it)->_instruction)) {
// Currently we only check server-side permissions
continue;
}
// Do not propagate anything in the server if it is in the selective sync blacklist
const QString path = (*it)->destination() + QLatin1Char('/');
switch ((*it)->_instruction) {
case CSYNC_INSTRUCTION_TYPE_CHANGE:
case CSYNC_INSTRUCTION_NEW: {
int slashPos = (*it)->_file.lastIndexOf('/');
QString parentDir = slashPos <= 0 ? "" : (*it)->_file.mid(0, slashPos);
const auto perms = getPermissions(parentDir);
if (perms.isNull()) {
// No permissions set
break;
} else if ((*it)->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) {
qCWarning(lcEngine) << "checkForPermission: ERROR" << (*it)->_file;
(*it)->_instruction = CSYNC_INSTRUCTION_ERROR;
(*it)->_status = SyncFileItem::NormalError;
(*it)->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder");
for (SyncFileItemVector::iterator it_next = it + 1; it_next != syncItems.end() && (*it_next)->destination().startsWith(path); ++it_next) {
it = it_next;
if ((*it)->_instruction == CSYNC_INSTRUCTION_RENAME) {
// The file was most likely moved in this directory.
// If the file was read only or could not be moved or removed, it should
// be restored. Do that in the next sync by not considering as a rename
// but delete and upload. It will then be restored if needed.
_journal->avoidRenamesOnNextSync((*it)->_file);
_anotherSyncNeeded = ImmediateFollowUp;
qCWarning(lcEngine) << "Moving of " << (*it)->_file << " canceled because no permission to add parent folder";
}
(*it)->_instruction = CSYNC_INSTRUCTION_ERROR;
(*it)->_status = SyncFileItem::SoftError;
(*it)->_errorString = tr("Not allowed because you don't have permission to add parent folder");
}
} else if (!(*it)->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) {
qCWarning(lcEngine) << "checkForPermission: ERROR" << (*it)->_file;
(*it)->_instruction = CSYNC_INSTRUCTION_ERROR;
(*it)->_status = SyncFileItem::NormalError;
(*it)->_errorString = tr("Not allowed because you don't have permission to add files in that folder");
}
break;
}
case CSYNC_INSTRUCTION_SYNC: {
const auto perms = getPermissions((*it)->_file);
if (perms.isNull()) {
// No permissions set
break;
}
if (!perms.hasPermission(RemotePermissions::CanWrite)) {
qCWarning(lcEngine) << "checkForPermission: RESTORING" << (*it)->_file;
(*it)->_instruction = CSYNC_INSTRUCTION_CONFLICT;
(*it)->_direction = SyncFileItem::Down;
(*it)->_isRestoration = true;
/*// Take the things to write to the db from the "other" node (i.e: info from server).
// Do a lookup into the csync remote tree to get the metadata we need to restore.
auto csyncIt = _csync_ctx->remote.files.find((*it)->_file.toUtf8());
if (csyncIt != _csync_ctx->remote.files.end()) {
(*it)->_modtime = csyncIt->second->modtime;
(*it)->_size = csyncIt->second->size;
(*it)->_fileId = csyncIt->second->file_id;
(*it)->_etag = csyncIt->second->etag;
}*/
(*it)->_errorString = tr("Not allowed to upload this file because it is read-only on the server, restoring");
continue;
}
break;
}
case CSYNC_INSTRUCTION_REMOVE: {
const auto perms = getPermissions((*it)->_file);
if (perms.isNull()) {
// No permissions set
break;
}
if (!perms.hasPermission(RemotePermissions::CanDelete)) {
qCWarning(lcEngine) << "checkForPermission: RESTORING" << (*it)->_file;
(*it)->_instruction = CSYNC_INSTRUCTION_NEW;
(*it)->_direction = SyncFileItem::Down;
(*it)->_isRestoration = true;
(*it)->_errorString = tr("Not allowed to remove, restoring");
if ((*it)->isDirectory()) {
// restore all sub items
for (SyncFileItemVector::iterator it_next = it + 1;
it_next != syncItems.end() && (*it_next)->_file.startsWith(path); ++it_next) {
it = it_next;
if ((*it)->_instruction != CSYNC_INSTRUCTION_REMOVE) {
qCWarning(lcEngine) << "non-removed job within a removed folder"
<< (*it)->_file << (*it)->_instruction;
continue;
}
qCWarning(lcEngine) << "checkForPermission: RESTORING" << (*it)->_file;
(*it)->_instruction = CSYNC_INSTRUCTION_NEW;
(*it)->_direction = SyncFileItem::Down;
(*it)->_isRestoration = true;
(*it)->_errorString = tr("Not allowed to remove, restoring");
}
}
} else if (perms.hasPermission(RemotePermissions::IsShared)
&& perms.hasPermission(RemotePermissions::CanDelete)) {
// this is a top level shared dir which can be removed to unshare it,
// regardless if it is a read only share or not.
// To avoid that we try to restore files underneath this dir which have
// not delete permission we fast forward the iterator and leave the
// delete jobs intact. It is not physically tried to remove this files
// underneath, propagator sees that.
if ((*it)->isDirectory()) {
// put a more descriptive message if a top level share dir really is removed.
if (it == syncItems.begin() || !(path.startsWith((*(it - 1))->_file))) {
(*it)->_errorString = tr("Local files and share folder removed.");
}
for (SyncFileItemVector::iterator it_next = it + 1;
it_next != syncItems.end() && (*it_next)->_file.startsWith(path); ++it_next) {
it = it_next;
}
}
}
break;
}
case CSYNC_INSTRUCTION_RENAME: {
int slashPos = (*it)->_renameTarget.lastIndexOf('/');
const QString parentDir = slashPos <= 0 ? "" : (*it)->_renameTarget.mid(0, slashPos);
const auto destPerms = getPermissions(parentDir);
const auto filePerms = getPermissions((*it)->_file);
//true when it is just a rename in the same directory. (not a move)
bool isRename = (*it)->_file.startsWith(parentDir) && (*it)->_file.lastIndexOf('/') == slashPos;
// Check if we are allowed to move to the destination.
bool destinationOK = true;
if (isRename || destPerms.isNull()) {
// no need to check for the destination dir permission
destinationOK = true;
} else if ((*it)->isDirectory() && !destPerms.hasPermission(RemotePermissions::CanAddSubDirectories)) {
destinationOK = false;
} else if (!(*it)->isDirectory() && !destPerms.hasPermission(RemotePermissions::CanAddFile)) {
destinationOK = false;
}
// check if we are allowed to move from the source
bool sourceOK = true;
if (!filePerms.isNull()
&& ((isRename && !filePerms.hasPermission(RemotePermissions::CanRename))
|| (!isRename && !filePerms.hasPermission(RemotePermissions::CanMove)))) {
// We are not allowed to move or rename this file
sourceOK = false;
if (filePerms.hasPermission(RemotePermissions::CanDelete) && destinationOK) {
// but we are allowed to delete it
// TODO! simulate delete & upload
}
}
#ifdef OWNCLOUD_RESTORE_RENAME /* We don't like the idea of renaming behind user's back, as the user may be working with the files */
if (!sourceOK && (!destinationOK || isRename)
// (not for directory because that's more complicated with the contents that needs to be adjusted)
&& !(*it)->isDirectory()) {
// Both the source and the destination won't allow move. Move back to the original
std::swap((*it)->_file, (*it)->_renameTarget);
(*it)->_direction = SyncFileItem::Down;
(*it)->_errorString = tr("Move not allowed, item restored");
(*it)->_isRestoration = true;
qCWarning(lcEngine) << "checkForPermission: MOVING BACK" << (*it)->_file;
// in case something does wrong, we will not do it next time
_journal->avoidRenamesOnNextSync((*it)->_file);
} else
#endif
if (!sourceOK || !destinationOK) {
// One of them is not possible, just throw an error
(*it)->_instruction = CSYNC_INSTRUCTION_ERROR;
(*it)->_status = SyncFileItem::NormalError;
const QString errorString = tr("Move not allowed because %1 is read-only").arg(sourceOK ? tr("the destination") : tr("the source"));
(*it)->_errorString = errorString;
qCWarning(lcEngine) << "checkForPermission: ERROR MOVING" << (*it)->_file << errorString;
// Avoid a rename on next sync:
// TODO: do the resolution now already so we don't need two sync
// At this point we would need to go back to the propagate phase on both remote to take
// the decision.
_journal->avoidRenamesOnNextSync((*it)->_file);
_anotherSyncNeeded = ImmediateFollowUp;
if ((*it)->isDirectory()) {
for (SyncFileItemVector::iterator it_next = it + 1;
it_next != syncItems.end() && (*it_next)->destination().startsWith(path); ++it_next) {
it = it_next;
(*it)->_instruction = CSYNC_INSTRUCTION_ERROR;
(*it)->_status = SyncFileItem::NormalError;
(*it)->_errorString = errorString;
qCWarning(lcEngine) << "checkForPermission: ERROR MOVING" << (*it)->_file;
}
}
}
break;
}
default:
break;
}
}
}
RemotePermissions SyncEngine::getPermissions(const QString &file) const
{
qFatal("FIXME");
return RemotePermissions();
}
void SyncEngine::restoreOldFiles(SyncFileItemVector &syncItems)
{
/* When the server is trying to send us lots of file in the past, this means that a backup

View File

@ -210,11 +210,7 @@ private slots:
//new directory should be uploaded
fakeFolder.localModifier().rename("readonlyDirectory_PERM_M_/subdir_PERM_CK_", "normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_");
applyPermissionsFromName(fakeFolder.remoteModifier());
fakeFolder.syncOnce();
if (fakeFolder.syncEngine().isAnotherSyncNeeded() == ImmediateFollowUp) {
QVERIFY(fakeFolder.syncOnce());
}
assertCsyncJournalOk(fakeFolder.syncJournal());
QVERIFY(fakeFolder.syncOnce());
currentLocalState = fakeFolder.currentLocalState();
// old name restored
@ -226,12 +222,14 @@ private slots:
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
//######################################################################
qInfo( "rename a directory in a read only folder and move a directory to a read-only" );
// do a sync to update the database
applyPermissionsFromName(fakeFolder.remoteModifier());
QVERIFY(fakeFolder.syncOnce());
assertCsyncJournalOk(fakeFolder.syncJournal());
//1. rename a directory in a read only folder
//Missing directory should be restored
@ -243,10 +241,6 @@ private slots:
// error: can't upload to readonly!
QVERIFY(!fakeFolder.syncOnce());
if (fakeFolder.syncEngine().isAnotherSyncNeeded() == ImmediateFollowUp) {
QVERIFY(!fakeFolder.syncOnce());
}
assertCsyncJournalOk(fakeFolder.syncJournal());
currentLocalState = fakeFolder.currentLocalState();
//1.