CSYNC_INSTRUCTION_IGNORE = 0x00000020, /* The file is ignored (UPDATE|RECONCILE) */
CSYNC_INSTRUCTION_SYNC = 0x00000040, /* The file need to be pushed to the other remote (RECONCILE) */
CSYNC_INSTRUCTION_STAT_ERROR = 0x00000080,
- CSYNC_INSTRUCTION_ERROR = 0x00000100
+ CSYNC_INSTRUCTION_ERROR = 0x00000100,
+ CSYNC_INSTRUCTION_TYPE_CHANGE = 0x0000200, /* Like NEW, but deletes the old entity first (RECONCILE)
+ Used when the type of something changes from directory to file
+ or back. */
};
enum csync_ftw_type_e {
break;
/* file on the other replica has not been modified */
case CSYNC_INSTRUCTION_NONE:
- cur->instruction = CSYNC_INSTRUCTION_SYNC;
+ if (cur->type != other->type) {
+ // If the type of the entity changed, it's like NEW, but
+ // needs to delete the other entity first.
+ cur->instruction = CSYNC_INSTRUCTION_TYPE_CHANGE;
+ } else {
+ cur->instruction = CSYNC_INSTRUCTION_SYNC;
+ }
break;
case CSYNC_INSTRUCTION_IGNORE:
cur->instruction = CSYNC_INSTRUCTION_IGNORE;
goto out;
}
}
+
+ // Preserve the EVAL flag later on if the type has changed.
+ if (tmp->type != fs->type)
+ st->child_modified = 1;
+
st->instruction = CSYNC_INSTRUCTION_EVAL;
goto out;
}
{ "INSTRUCTION_SYNC", CSYNC_INSTRUCTION_SYNC },
{ "INSTRUCTION_STAT_ERR", CSYNC_INSTRUCTION_STAT_ERROR },
{ "INSTRUCTION_ERROR", CSYNC_INSTRUCTION_ERROR },
+ { "INSTRUCTION_TYPE_CHANGE", CSYNC_INSTRUCTION_TYPE_CHANGE },
{ NULL, CSYNC_INSTRUCTION_ERROR }
};
if (!item->hasErrorStatus() && item->_direction == SyncFileItem::Down) {
switch (item->_instruction) {
case CSYNC_INSTRUCTION_NEW:
+ case CSYNC_INSTRUCTION_TYPE_CHANGE:
newItems++;
if (!firstItemNew)
firstItemNew = item;
if (state == SyncResult::Success) {
foreach (const SyncFileItemPtr &i, f->syncResult().syncFileItemVector()) {
if (i->_isDirectory && (i->_instruction == CSYNC_INSTRUCTION_NEW
+ || i->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE
|| i->_instruction == CSYNC_INSTRUCTION_REMOVE
|| i->_instruction == CSYNC_INSTRUCTION_RENAME)) {
// There is a new or a removed folder. reset all data
case CSYNC_INSTRUCTION_ERROR:
re = "INST_ERROR";
break;
+ case CSYNC_INSTRUCTION_TYPE_CHANGE:
+ re = "INST_TYPE_CHANGE";
+ break;
}
return re;
if( httpStatusCode == 403 && _propagator->isInSharedDirectory(_item->_file )) {
if( !_item->_isDirectory ) {
SyncFileItemPtr downloadItem(new SyncFileItem(*_item));
- if (downloadItem->_instruction == CSYNC_INSTRUCTION_NEW) {
+ if (downloadItem->_instruction == CSYNC_INSTRUCTION_NEW
+ || downloadItem->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) {
// don't try to recover pushing new files
return false;
} else if (downloadItem->_instruction == CSYNC_INSTRUCTION_SYNC) {
// ================================================================================
PropagateItemJob* OwncloudPropagator::createJob(const SyncFileItemPtr &item) {
+ bool deleteExisting = item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE;
switch(item->_instruction) {
case CSYNC_INSTRUCTION_REMOVE:
if (item->_direction == SyncFileItem::Down) return new PropagateLocalRemove(this, item);
else return new PropagateRemoteDelete(this, item);
case CSYNC_INSTRUCTION_NEW:
+ case CSYNC_INSTRUCTION_TYPE_CHANGE:
if (item->_isDirectory) {
- if (item->_direction == SyncFileItem::Down) return new PropagateLocalMkdir(this, item);
- else return new PropagateRemoteMkdir(this, item);
+ if (item->_direction == SyncFileItem::Down) {
+ auto job = new PropagateLocalMkdir(this, item);
+ job->setDeleteExistingFile(deleteExisting);
+ return job;
+ } else {
+ auto job = new PropagateRemoteMkdir(this, item);
+ job->setDeleteExisting(deleteExisting);
+ return job;
+ }
} //fall through
case CSYNC_INSTRUCTION_SYNC:
case CSYNC_INSTRUCTION_CONFLICT:
if (item->_isDirectory) {
- // Did a file turn into a directory?
- if (QFileInfo(getFilePath(item->_file)).isFile()) {
- auto job = new PropagateLocalMkdir(this, item);
- job->setDeleteExistingFile(true);
- return job;
- }
// Should we set the mtime?
return 0;
}
{
if (item->_direction != SyncFileItem::Up) {
- return new PropagateDownloadFileQNAM(this, item);
+ auto job = new PropagateDownloadFileQNAM(this, item);
+ job->setDeleteExistingFolder(deleteExisting);
+ return job;
} else {
- return new PropagateUploadFileQNAM(this, item);
+ auto job = new PropagateUploadFileQNAM(this, item);
+ job->setDeleteExisting(deleteExisting);
+ return job;
}
}
case CSYNC_INSTRUCTION_RENAME:
delDirJob->increaseAffectedCount();
}
continue;
- } else if (item->_instruction == CSYNC_INSTRUCTION_NEW && item->_isDirectory) {
+ } else if (item->_isDirectory
+ && (item->_instruction == CSYNC_INSTRUCTION_NEW
+ || item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE)) {
// create a new directory within a deleted directory? That can happen if the directory
// etag was not fetched properly on the previous sync because the sync was aborted
// while uploading this directory (which is now removed). We can ignore it.
if (item->_isDirectory) {
PropagateDirectory *dir = new PropagateDirectory(this, item);
dir->_firstJob.reset(createJob(item));
+
+ if (item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE
+ && item->_direction == SyncFileItem::Up) {
+ // Skip all potential uploads to the new folder.
+ // Processing them now leads to problems with permissions:
+ // checkForPermissions() has already run and used the permissions
+ // of the file we're about to delete to decide whether uploading
+ // to the new dir is ok...
+ foreach(const SyncFileItemPtr &item2, items) {
+ if (item2->destination().startsWith(item->destination() + "/")) {
+ item2->_instruction = CSYNC_INSTRUCTION_NONE;
+ _anotherSyncNeeded = true;
+ }
+ }
+ }
+
if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
// We do the removal of directories at the end, because there might be moves from
// these directories that will happen later.
}
directories.push(qMakePair(item->destination() + "/" , dir));
} else if (PropagateItemJob* current = createJob(item)) {
- // If the target of a job is currently a directory, we need to remove it!
- // This can happen when what used to be a directory changed to a file on the
- // server an a PropagateLocalRename or PropageDownload job wants to run.
- if (item->_direction == SyncFileItem::Down
- && QFileInfo(getFilePath(item->_file)).isDir()) {
- // The DirectoryConflict job *must* run before the file propagation job
- // and we also need to make sure other jobs that deal with the files
- // in the directory (like removes or moves, in particular of other
- // directories!) run first.
- // Making it a directory job ensures that moves run first and that the
- // (potential) directory rename happens before the file propagation.
- // Prepending all jobs to directoriesToRemove ensures that removals of
- // subdirectories happen before the directory is renamed.
- PropagateDirectory *dir = new PropagateDirectory(this, item);
- dir->_firstJob.reset(new PropagateLocalDirectoryConflict(this, item));
- dir->append(current);
- directoriesToRemove.prepend(dir);
+ if (item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) {
+ // will delete directories, so defer execution
+ directoriesToRemove.prepend(current);
+ removedDirectory = item->_file + "/";
} else {
directories.top().second->append(current);
}
switch(item._instruction) {
case CSYNC_INSTRUCTION_SYNC:
case CSYNC_INSTRUCTION_NEW:
+ case CSYNC_INSTRUCTION_TYPE_CHANGE:
if (item._direction != SyncFileItem::Up) {
return QCoreApplication::translate( "progress", "Downloaded");
} else {
case CSYNC_INSTRUCTION_CONFLICT:
case CSYNC_INSTRUCTION_SYNC:
case CSYNC_INSTRUCTION_NEW:
+ case CSYNC_INSTRUCTION_TYPE_CHANGE:
if (item._direction != SyncFileItem::Up)
return QCoreApplication::translate( "progress", "downloading");
else
return ! item._isDirectory && (
item._instruction == CSYNC_INSTRUCTION_CONFLICT
|| item._instruction == CSYNC_INSTRUCTION_SYNC
- || item._instruction == CSYNC_INSTRUCTION_NEW);
+ || item._instruction == CSYNC_INSTRUCTION_NEW
+ || item._instruction == CSYNC_INSTRUCTION_TYPE_CHANGE);
}
/**
qDebug() << Q_FUNC_INFO << _item->_file << _propagator->_activeJobs;
+ if (_deleteExisting) {
+ deleteExistingFolder();
+
+ // check for error with deletion
+ if (_state == Finished) {
+ return;
+ }
+ }
+
// do a klaas' case clash check.
if( _propagator->localFileNameClash(_item->_file) ) {
done( SyncFileItem::NormalError, tr("File %1 can not be downloaded because of a local file name clash!")
return 0;
}
+void PropagateDownloadFileQNAM::setDeleteExistingFolder(bool enabled)
+{
+ _deleteExisting = enabled;
+}
+
const char owncloudCustomSoftErrorStringC[] = "owncloud-custom-soft-error-string";
void PropagateDownloadFileQNAM::slotGetFinished()
{
done(SyncFileItem::SoftError, errMsg ); // tr("The file downloaded with a broken checksum, will be redownloaded."));
}
+void PropagateDownloadFileQNAM::deleteExistingFolder()
+{
+ QString existingDir = _propagator->getFilePath(_item->_file);
+ if (!QFileInfo(existingDir).isDir()) {
+ return;
+ }
+
+ // Delete the directory if it is empty!
+ QDir dir(existingDir);
+ if (dir.entryList(QDir::NoDotAndDotDot|QDir::AllEntries).count() == 0) {
+ if (dir.rmdir(existingDir)) {
+ return;
+ }
+ // on error, just try to move it away...
+ }
+
+ QString conflictDir = FileSystem::makeConflictFileName(
+ existingDir, Utility::qDateTimeFromTime_t(_item->_modtime));
+
+ _propagator->addTouchedFile(existingDir);
+ _propagator->addTouchedFile(conflictDir);
+ QString renameError;
+ if (!FileSystem::rename(existingDir, conflictDir, &renameError)) {
+ done(SyncFileItem::NormalError, renameError);
+ }
+}
+
namespace { // Anonymous namespace for the recall feature
static QString makeRecallFileName(const QString &fn)
{
Q_OBJECT
public:
PropagateDownloadFileQNAM(OwncloudPropagator* propagator,const SyncFileItemPtr& item)
- : PropagateItemJob(propagator, item), _resumeStart(0), _downloadProgress(0) {}
+ : PropagateItemJob(propagator, item), _resumeStart(0), _downloadProgress(0), _deleteExisting(false) {}
void start() Q_DECL_OVERRIDE;
qint64 committedDiskSpace() const Q_DECL_OVERRIDE;
+ /**
+ * Whether an existing folder with the same name may be deleted before
+ * the download.
+ *
+ * If it's a non-empty folder, it'll be renamed to a conflict-style name
+ * to preserve any non-synced content that may be inside.
+ *
+ * Default: false.
+ */
+ void setDeleteExistingFolder(bool enabled);
+
private slots:
void slotGetFinished();
void abort() Q_DECL_OVERRIDE;
void slotChecksumFail( const QString& errMsg );
private:
+ void deleteExistingFolder();
+
quint64 _resumeStart;
qint64 _downloadProgress;
QPointer<GETFileJob> _job;
QFile _tmpFile;
+ bool _deleteExisting;
};
}
#include "owncloudpropagator_p.h"
#include "account.h"
#include "syncjournalfilerecord.h"
+#include "propagateremotedelete.h"
#include <QFile>
namespace OCC {
qDebug() << Q_FUNC_INFO << _item->_file;
+ _propagator->_activeJobs++;
+
+ if (!_deleteExisting) {
+ return slotStartMkcolJob();
+ }
+
+ _job = new DeleteJob(_propagator->account(),
+ _propagator->_remoteFolder + _item->_file,
+ this);
+ connect(_job, SIGNAL(finishedSignal()), SLOT(slotStartMkcolJob()));
+ _job->start();
+}
+
+void PropagateRemoteMkdir::slotStartMkcolJob()
+{
+ if (_propagator->_abortRequested.fetchAndAddRelaxed(0))
+ return;
+
+ qDebug() << Q_FUNC_INFO << _item->_file;
+
_job = new MkColJob(_propagator->account(),
_propagator->_remoteFolder + _item->_file,
this);
connect(_job, SIGNAL(finished(QNetworkReply::NetworkError)), this, SLOT(slotMkcolJobFinished()));
- _propagator->_activeJobs++;
_job->start();
}
_job->reply()->abort();
}
+void PropagateRemoteMkdir::setDeleteExisting(bool enabled)
+{
+ _deleteExisting = enabled;
+}
+
void PropagateRemoteMkdir::slotMkcolJobFinished()
{
_propagator->_activeJobs--;
class PropagateRemoteMkdir : public PropagateItemJob {
Q_OBJECT
QPointer<AbstractNetworkJob> _job;
+ bool _deleteExisting;
friend class PropagateDirectory; // So it can access the _item;
public:
PropagateRemoteMkdir (OwncloudPropagator* propagator,const SyncFileItemPtr& item)
- : PropagateItemJob(propagator, item) {}
+ : PropagateItemJob(propagator, item), _deleteExisting(false) {}
void start() Q_DECL_OVERRIDE;
void abort() Q_DECL_OVERRIDE;
+
+ /**
+ * Whether an existing entity with the same name may be deleted before
+ * creating the directory.
+ *
+ * Default: false.
+ */
+ void setDeleteExisting(bool enabled);
+
private slots:
+ void slotStartMkcolJob();
void slotMkcolJobFinished();
void propfindResult(const QVariantMap &);
void propfindError();
#include "propagatorjobs.h"
#include "checksums.h"
#include "syncengine.h"
+#include "propagateremotedelete.h"
#include <json.h>
#include <QNetworkAccessManager>
}
}
+ _propagator->_activeJobs++;
+
+ if (!_deleteExisting) {
+ return slotComputeContentChecksum();
+ }
+
+ auto job = new DeleteJob(_propagator->account(),
+ _propagator->_remoteFolder + _item->_file,
+ this);
+ _jobs.append(job);
+ connect(job, SIGNAL(finishedSignal()), SLOT(slotComputeContentChecksum()));
+ connect(job, SIGNAL(destroyed(QObject*)), SLOT(slotJobDestroyed(QObject*)));
+ job->start();
+}
+
+void PropagateUploadFileQNAM::slotComputeContentChecksum()
+{
+ if (_propagator->_abortRequested.fetchAndAddRelaxed(0)) {
+ return;
+ }
+
+ _propagator->_activeJobs--; // from start
+
const QString filePath = _propagator->getFilePath(_item->_file);
// remember the modtime before checksumming to be able to detect a file
computeChecksum->start(filePath);
}
+void PropagateUploadFileQNAM::setDeleteExisting(bool enabled)
+{
+ _deleteExisting = enabled;
+}
+
void PropagateUploadFileQNAM::slotComputeTransmissionChecksum(const QByteArray& contentChecksumType, const QByteArray& contentChecksum)
{
_item->_contentChecksum = contentChecksum;
headers["OC-Tag"] = ".sys.admin#recall#";
}
- if (!_item->_etag.isEmpty() && _item->_etag != "empty_etag" &&
- _item->_instruction != CSYNC_INSTRUCTION_NEW // On new files never send a If-Match
+ if (!_item->_etag.isEmpty() && _item->_etag != "empty_etag"
+ && _item->_instruction != CSYNC_INSTRUCTION_NEW // On new files never send a If-Match
+ && _item->_instruction != CSYNC_INSTRUCTION_TYPE_CHANGE
+ && !_deleteExisting
) {
// We add quotes because the owncloud server always adds quotes around the etag, and
// csync_owncloud.c's owncloud_file_id always strips the quotes.
auto currentChunk = job->_chunk;
foreach (auto *job, _jobs) {
// Take the minimum finished one
- currentChunk = qMin(currentChunk, job->_chunk - 1);
+ if (auto putJob = qobject_cast<PUTFileJob*>(job)) {
+ currentChunk = qMin(currentChunk, putJob->_chunk - 1);
+ }
}
pi._chunk = (currentChunk + _startChunk + 1) % _chunkCount ; // next chunk to start with
pi._transferid = _transferId;
int _chunkCount; /// Total number of chunks for this file
int _transferId; /// transfer id (part of the url)
QElapsedTimer _duration;
- QVector<PUTFileJob*> _jobs; /// network jobs that are currently in transit
+ QVector<AbstractNetworkJob*> _jobs; /// network jobs that are currently in transit
bool _finished; // Tells that all the jobs have been finished
// measure the performance of checksum calc and upload
QByteArray _transmissionChecksum;
QByteArray _transmissionChecksumType;
+ bool _deleteExisting;
+
public:
PropagateUploadFileQNAM(OwncloudPropagator* propagator,const SyncFileItemPtr& item)
- : PropagateItemJob(propagator, item), _startChunk(0), _currentChunk(0), _chunkCount(0), _transferId(0), _finished(false) {}
+ : PropagateItemJob(propagator, item), _startChunk(0), _currentChunk(0), _chunkCount(0), _transferId(0), _finished(false), _deleteExisting(false) {}
void start() Q_DECL_OVERRIDE;
+
+ /**
+ * Whether an existing entity with the same name may be deleted before
+ * the upload.
+ *
+ * Default: false.
+ */
+ void setDeleteExisting(bool enabled);
+
private slots:
void slotPutFinished();
void slotPollFinished();
void slotJobDestroyed(QObject *job);
void slotStartUpload(const QByteArray& transmissionChecksumType, const QByteArray& transmissionChecksum);
void slotComputeTransmissionChecksum(const QByteArray& contentChecksumType, const QByteArray& contentChecksum);
+ void slotComputeContentChecksum();
private:
void startPollJob(const QString& path);
done(SyncFileItem::Success);
}
-void PropagateLocalDirectoryConflict::start()
-{
- if (_propagator->_abortRequested.fetchAndAddRelaxed(0))
- return;
-
- QString existingDir = _propagator->getFilePath(_item->_file);
- if (!QFileInfo(existingDir).isDir()) {
- done(SyncFileItem::Success);
- return;
- }
-
- // Delete the directory if it is empty!
- QDir dir(existingDir);
- if (dir.entryList(QDir::NoDotAndDotDot|QDir::AllEntries).count() == 0) {
- if (dir.rmdir(existingDir)) {
- done(SyncFileItem::Success);
- return;
- }
- // on error, just try to move it away...
- }
-
- QString conflictDir = FileSystem::makeConflictFileName(
- existingDir, Utility::qDateTimeFromTime_t(_item->_modtime));
-
- _propagator->addTouchedFile(existingDir);
- _propagator->addTouchedFile(conflictDir);
- QString renameError;
- if (!FileSystem::rename(existingDir, conflictDir, &renameError)) {
- done(SyncFileItem::NormalError, renameError);
- return;
- }
-
- done(SyncFileItem::Success);
-}
-
}
JobParallelism parallelism() Q_DECL_OVERRIDE { return WaitForFinishedInParentDirectory; }
};
-/**
- * Moves away a local directory when it should become a file.
- *
- * Example: Locally there's directory foo/ with three files in it,
- * one of them ignored, while the server has a file foo.
- * In this case, foo/ fill be moved to foo-conflict.../ and the
- * file will be downloaded to foo.
- *
- * If the directory is empty, it will be removed instead.
- *
- * @ingroup libsync
- */
-class PropagateLocalDirectoryConflict : public PropagateItemJob {
- Q_OBJECT
-public:
- PropagateLocalDirectoryConflict (OwncloudPropagator* propagator, const SyncFileItemPtr& item) : PropagateItemJob(propagator, item) {}
- void start() Q_DECL_OVERRIDE;
- JobParallelism parallelism() Q_DECL_OVERRIDE { return WaitForFinishedInParentDirectory; }
-};
-
}
item->_errorString = tr("Filename encoding is not valid");
}
- item->_isDirectory = file->type == CSYNC_FTW_TYPE_DIR;
+ bool isDirectory = file->type == CSYNC_FTW_TYPE_DIR;
if (file->etag && file->etag[0]) {
item->_etag = file->etag;
int re = 0;
switch(file->instruction) {
case CSYNC_INSTRUCTION_NONE: {
- if (remote && item->_should_update_metadata && !item->_isDirectory && item->_instruction == CSYNC_INSTRUCTION_NONE) {
+ if (remote && item->_should_update_metadata && !isDirectory && item->_instruction == CSYNC_INSTRUCTION_NONE) {
// Update the database now already: New remote fileid or Etag or RemotePerm
// Or for files that were detected as "resolved conflict".
// Or a local inode/mtime change (see localMetadataUpdate below)
_syncItemMap.remove(key);
}
// Any files that are instruction NONE?
- if (!item->_isDirectory && file->other.instruction == CSYNC_INSTRUCTION_NONE) {
+ if (!isDirectory && file->other.instruction == CSYNC_INSTRUCTION_NONE) {
_hasNoneFiles = true;
}
// We want to still update etags of directories, other NONE
// items can be ignored.
- bool directoryEtagUpdate = item->_isDirectory && file->should_update_metadata;
+ bool directoryEtagUpdate = isDirectory && file->should_update_metadata;
bool localMetadataUpdate = !remote && file->should_update_metadata;
if (!directoryEtagUpdate) {
if (localMetadataUpdate) {
// Hack, we want a local metadata update to happen, but only if the
// remote tree doesn't ask us to do some kind of propagation.
+ item->_isDirectory = isDirectory;
_syncItemMap.insert(key, item);
}
return re;
case CSYNC_INSTRUCTION_RENAME:
dir = !remote ? SyncFileItem::Down : SyncFileItem::Up;
item->_renameTarget = renameTarget;
- if (item->_isDirectory)
+ if (isDirectory)
_renamedFolders.insert(item->_file, item->_renameTarget);
break;
case CSYNC_INSTRUCTION_REMOVE:
break;
case CSYNC_INSTRUCTION_EVAL:
case CSYNC_INSTRUCTION_NEW:
+ case CSYNC_INSTRUCTION_TYPE_CHANGE:
case CSYNC_INSTRUCTION_SYNC:
case CSYNC_INSTRUCTION_STAT_ERROR:
default:
}
item->_direction = dir;
+ item->_isDirectory = isDirectory;
if (instruction != CSYNC_INSTRUCTION_NONE) {
// check for blacklisting of this item.
// if the item is on blacklist, the instruction was set to ERROR
}
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);