Comparar commits

...

3 Commits

Autor SHA1 Mensagem Data
Piotr M 5c3fd36f28 tests 2017-01-16 12:34:07 +01:00
Piotr M 54ee5a81eb bundling final implementation 2016-11-25 01:33:04 +01:00
Piotr M d196ce98b2 prototype of bundled requests in the client 2016-11-24 01:51:32 +01:00
17 arquivos alterados com 1206 adições e 38 exclusões
+1
Ver Arquivo
@@ -52,6 +52,7 @@ set(libsync_SRCS
propagateupload.cpp
propagateuploadv1.cpp
propagateuploadng.cpp
propagatebundle.cpp
propagateremotedelete.cpp
propagateremotemove.cpp
propagateremotemkdir.cpp
+5
Ver Arquivo
@@ -128,6 +128,11 @@ QNetworkReply *AbstractNetworkJob::davRequest(const QByteArray &verb, const QUrl
return addTimer(_account->davRequest(verb, url, req, data));
}
QNetworkReply *AbstractNetworkJob::multipartRequest(const QString &relPath, QNetworkRequest req, QHttpMultiPart *multiPart)
{
return addTimer(_account->multipartRequest(relPath, req, multiPart));
}
QNetworkReply* AbstractNetworkJob::getRequest(const QString &relPath)
{
return addTimer(_account->getRequest(relPath));
+2
Ver Arquivo
@@ -26,6 +26,7 @@
#include "accountfwd.h"
class QUrl;
class QHttpMultiPart;
namespace OCC {
@@ -78,6 +79,7 @@ protected:
QNetworkReply* headRequest(const QString &relPath);
QNetworkReply* headRequest(const QUrl &url);
QNetworkReply* deleteRequest(const QUrl &url);
QNetworkReply* multipartRequest(const QString &relPath, QNetworkRequest req, QHttpMultiPart *multiPart);
int maxRedirects() const { return 10; }
virtual bool finished() = 0;
+26 -1
Ver Arquivo
@@ -86,6 +86,14 @@ void Account::setDavUser(const QString &newDavUser)
_davUser = newDavUser;
}
QString Account::davFilesPath() const
{
//TODO DO NOT HARCODE PATH, GET IT FROM THE SERVER!!!!
QString dfp("/remote.php/dav/files/");
dfp.append(_credentials->user());
return dfp;
}
QString Account::displayName() const
{
QString dn = QString("%1@%2").arg(davUser(), _url.host());
@@ -237,6 +245,20 @@ QNetworkReply *Account::davRequest(const QByteArray &verb, const QUrl &url, QNet
return _am->sendCustomRequest(req, verb, data);
}
QNetworkReply *Account::multipartRequest(const QString &relPath, QNetworkRequest req, QHttpMultiPart *multiPart)
{
return multipartRequest(Utility::concatUrlPath(url(), relPath), req, multiPart);
}
QNetworkReply *Account::multipartRequest(const QUrl &url, QNetworkRequest req, QHttpMultiPart *multiPart)
{
req.setUrl(url);
#if QT_VERSION > QT_VERSION_CHECK(4, 8, 4)
req.setSslConfiguration(this->getOrCreateSslConfig());
#endif
return _am->post(req, multiPart);
}
void Account::setCertificate(const QByteArray certficate, const QString privateKey)
{
_pemCertificate=certficate;
@@ -475,6 +497,9 @@ void Account::setNonShib(bool nonShib)
}
}
bool Account::bundledRequestsEnabled() const
{
return _capabilities.bundledRequest();
}
} // namespace OCC
+22 -9
Ver Arquivo
@@ -34,6 +34,7 @@ class QSettings;
class QNetworkReply;
class QUrl;
class QNetworkAccessManager;
class QHttpMultiPart;
namespace OCC {
@@ -64,6 +65,25 @@ public:
class OWNCLOUDSYNC_EXPORT Account : public QObject {
Q_OBJECT
public:
/**
* @brief The possibly themed dav path for the account. It has
* a trailing slash.
* @returns the (themeable) dav path for the account.
*/
QString davPath() const;
/**
* @brief The possibly themed files dav path for the account. It has
* a trailing slash.
* @returns the (themeable) files dav path for the account.
*/
QString davFilesPath() const;
void setDavPath(const QString&s) { _davPath = s; }
void setNonShib(bool nonShib);
/// Functions for bundle support
bool bundledRequestsEnabled() const;
static AccountPtr create();
~Account();
@@ -88,15 +108,6 @@ public:
void setUrl(const QUrl &url);
QUrl url() const { return _url; }
/**
* @brief The possibly themed dav path for the account. It has
* a trailing slash.
* @returns the (themeable) dav path for the account.
*/
QString davPath() const;
void setDavPath(const QString&s) { _davPath = s; }
void setNonShib(bool nonShib);
/** Returns webdav entry URL, based on url() */
QUrl davUrl() const;
@@ -113,6 +124,8 @@ public:
QNetworkReply* deleteRequest( const QUrl &url);
QNetworkReply* davRequest(const QByteArray &verb, const QString &relPath, QNetworkRequest req, QIODevice *data = 0);
QNetworkReply* davRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data = 0);
QNetworkReply* multipartRequest(const QString &relPath, QNetworkRequest req, QHttpMultiPart *multiPart);
QNetworkReply* multipartRequest(const QUrl &url, QNetworkRequest req, QHttpMultiPart *multiPart);
/** The ssl configuration during the first connection */
+9
Ver Arquivo
@@ -47,6 +47,15 @@ bool Capabilities::sharePublicLink() const
}
}
bool Capabilities::bundledRequest() const
{
static const auto bundling = qgetenv("OWNCLOUD_BUNDLING");
if (bundling == "0") return false;
if (bundling == "1") return true;
return _capabilities["dav"].toMap()["bundlerequest"].toByteArray() >= "1.0";
}
bool Capabilities::sharePublicLinkAllowUpload() const
{
return _capabilities["files_sharing"].toMap()["public"].toMap()["upload"].toBool();
+1
Ver Arquivo
@@ -41,6 +41,7 @@ public:
int sharePublicLinkExpireDateDays() const;
bool shareResharing() const;
bool chunkingNg() const;
bool bundledRequest() const;
/// returns true if the capabilities report notifications
bool notificationsAvailable() const;
+58 -15
Ver Arquivo
@@ -18,6 +18,7 @@
#include "syncjournalfilerecord.h"
#include "propagatedownload.h"
#include "propagateupload.h"
#include "propagatebundle.h"
#include "propagateremotedelete.h"
#include "propagateremotemove.h"
#include "propagateremotemkdir.h"
@@ -71,6 +72,14 @@ qint64 freeSpaceLimit()
OwncloudPropagator::~OwncloudPropagator()
{}
bool OwncloudPropagator::hasNetworkLimit()
{
if (_downloadLimit.fetchAndAddAcquire(0) != 0 || _uploadLimit.fetchAndAddAcquire(0) != 0) {
return true;
}
return false;
}
/* The maximum number of active jobs in parallel */
int OwncloudPropagator::maximumActiveJob()
{
@@ -79,7 +88,7 @@ int OwncloudPropagator::maximumActiveJob()
max = 3; //default
}
if (_downloadLimit.fetchAndAddAcquire(0) != 0 || _uploadLimit.fetchAndAddAcquire(0) != 0) {
if (hasNetworkLimit()) {
// disable parallelism when there is a network limit.
return 1;
}
@@ -115,18 +124,17 @@ static bool blacklistCheck(SyncJournalDb* journal, const SyncFileItem& item)
return newEntry.isValid();
}
void PropagateItemJob::done(SyncFileItem::Status status, const QString &errorString)
void PropagateItemJob::itemDone(SyncFileItemPtr item, SyncFileItem::Status status, const QString &errorString)
{
_state = Finished;
if (_item->_isRestoration) {
if (item->_isRestoration) {
if( status == SyncFileItem::Success || status == SyncFileItem::Conflict) {
status = SyncFileItem::Restoration;
} else {
_item->_errorString += tr("; Restoration Failed: %1").arg(errorString);
item->_errorString += tr("; Restoration Failed: %1").arg(errorString);
}
} else {
if( _item->_errorString.isEmpty() ) {
_item->_errorString = errorString;
if( item->_errorString.isEmpty() ) {
item->_errorString = errorString;
}
}
@@ -142,20 +150,20 @@ void PropagateItemJob::done(SyncFileItem::Status status, const QString &errorStr
// do not blacklist in case of soft error or fatal error.
break;
case SyncFileItem::NormalError:
if (blacklistCheck(_propagator->_journal, *_item) && _item->_hasBlacklistEntry) {
if (blacklistCheck(_propagator->_journal, *item) && item->_hasBlacklistEntry) {
// do not error if the item was, and continues to be, blacklisted
status = SyncFileItem::FileIgnored;
_item->_errorString.prepend(tr("Continue blacklisting:") + " ");
item->_errorString.prepend(tr("Continue blacklisting:") + " ");
}
break;
case SyncFileItem::Success:
case SyncFileItem::Restoration:
if( _item->_hasBlacklistEntry ) {
if( item->_hasBlacklistEntry ) {
// wipe blacklist entry.
_propagator->_journal->wipeErrorBlacklistEntry(_item->_file);
_propagator->_journal->wipeErrorBlacklistEntry(item->_file);
// remove a blacklist entry in case the file was moved.
if( _item->_originalFile != _item->_file ) {
_propagator->_journal->wipeErrorBlacklistEntry(_item->_originalFile);
if( item->_originalFile != item->_file ) {
_propagator->_journal->wipeErrorBlacklistEntry(item->_originalFile);
}
}
break;
@@ -166,9 +174,15 @@ void PropagateItemJob::done(SyncFileItem::Status status, const QString &errorStr
break;
}
_item->_status = status;
item->_status = status;
emit itemCompleted(*_item, *this);
emit itemCompleted(*item, *this);
}
void PropagateItemJob::done(SyncFileItem::Status status, const QString &errorString)
{
_state = Finished;
itemDone(_item, status, errorString);
emit finished(status);
}
@@ -310,6 +324,13 @@ void OwncloudPropagator::start(const SyncFileItemVector& items)
directories.push(qMakePair(QString(), _rootJob.data()));
QVector<PropagatorJob*> directoriesToRemove;
QString removedDirectory;
quint64 chunkSize = OwncloudPropagator::chunkSize();
// TODO: here we should also check somehow if bundle is not blacklisted
bool enableBundledRequests = _account->bundledRequestsEnabled() && !this->hasNetworkLimit();
//bool enableBundledRequests = false;
foreach(const SyncFileItemPtr &item, items) {
if (!removedDirectory.isEmpty() && item->_file.startsWith(removedDirectory)) {
@@ -386,6 +407,17 @@ void OwncloudPropagator::start(const SyncFileItemVector& items)
currentDirJob->append(dir);
}
directories.push(qMakePair(item->destination() + "/" , dir));
} else if (enableBundledRequests
&& (item->_instruction == CSYNC_INSTRUCTION_NEW)
&& (item->_direction == SyncFileItem::Up)
&& (item->_size < chunkSize)) {
//this will create list of bundle files to sync for that bundlejob
if (directories.top().second->_bundleJob.isNull()) {
PropagateBundle* bundleJob = new PropagateBundle(this);
directories.top().second->_bundleJob.reset(bundleJob);
}
PropagateBundle* bundleJob = qobject_cast<PropagateBundle*>(directories.top().second->_bundleJob.data());
bundleJob->append(item);
} else if (PropagateItemJob* current = createJob(item)) {
if (item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) {
// will delete directories, so defer execution
@@ -601,6 +633,16 @@ bool PropagateDirectory::scheduleNextJob()
if (_state == NotYetStarted) {
_state = Running;
if(_bundleJob){
PropagateBundle* bundle = qobject_cast<PropagateBundle*>(_bundleJob.take());
bundle->_item->_direction = SyncFileItem::Direction::Up;
bundle->_item->_type = SyncFileItem::Type::RequestsContainer;
bundle->_item->_instruction = CSYNC_INSTRUCTION_NEW;
bundle->_item->_size = bundle->syncItemsSize();
bundle->_item->_originalFile = tr("%1 file(s)").arg(bundle->syncItemsNumber());
_subJobs.append(bundle);
}
if (!_firstJob && _subJobs.isEmpty()) {
finalize();
return true;
@@ -639,6 +681,7 @@ bool PropagateDirectory::scheduleNextJob()
Q_ASSERT(_subJobs.at(i)->_state == Running);
auto paral = _subJobs.at(i)->parallelism();
if (paral == WaitForFinished) {
return false;
}
+11 -2
Ver Arquivo
@@ -136,6 +136,7 @@ class PropagateItemJob : public PropagatorJob {
Q_OBJECT
protected:
void done(SyncFileItem::Status status, const QString &errorString = QString());
void itemDone(SyncFileItemPtr item, SyncFileItem::Status status, const QString &errorString = QString());
bool checkForProblemsWithShared(int httpStatusCode, const QString& msg);
@@ -187,6 +188,9 @@ public:
// e.g: create the directory
QScopedPointer<PropagateItemJob>_firstJob;
// bundling upload files job
QScopedPointer<PropagateItemJob> _bundleJob;
// all the sub files or sub directories.
QVector<PropagatorJob *> _subJobs;
@@ -199,7 +203,7 @@ public:
explicit PropagateDirectory(OwncloudPropagator *propagator, const SyncFileItemPtr &item = SyncFileItemPtr(new SyncFileItem))
: PropagatorJob(propagator)
, _firstJob(0), _item(item), _jobsFinished(0), _runningNow(0), _hasError(SyncFileItem::NoStatus), _firstUnfinishedSubJob(0)
, _firstJob(0),_bundleJob(0), _item(item), _jobsFinished(0), _runningNow(0), _hasError(SyncFileItem::NoStatus), _firstUnfinishedSubJob(0)
{ }
virtual ~PropagateDirectory() {
@@ -288,7 +292,7 @@ public:
~OwncloudPropagator();
void start(const SyncFileItemVector &_syncedItems);
bool hasNetworkLimit();
QAtomicInt _downloadLimit;
QAtomicInt _uploadLimit;
BandwidthManager _bandwidthManager;
@@ -322,6 +326,10 @@ public:
emitFinished(SyncFileItem::NormalError);
}
int runningNowAtRootJob() const {
return _rootJob->_runningNow;
}
// timeout in seconds
static int httpTimeout();
@@ -382,6 +390,7 @@ private:
friend class PropagateRemoteMove;
friend class PropagateUploadFileV1;
friend class PropagateUploadFileNG;
friend class PropagateBundle;
#endif
};
+7 -5
Ver Arquivo
@@ -29,11 +29,13 @@ QString Progress::asResultString( const SyncFileItem& item)
case CSYNC_INSTRUCTION_SYNC:
case CSYNC_INSTRUCTION_NEW:
case CSYNC_INSTRUCTION_TYPE_CHANGE:
if (item._direction != SyncFileItem::Up) {
return QCoreApplication::translate( "progress", "Downloaded");
} else {
return QCoreApplication::translate( "progress", "Uploaded");
}
if (item._type != SyncFileItem::Type::RequestsContainer) {
if (item._direction != SyncFileItem::Up) {
return QCoreApplication::translate( "progress", "Downloaded");
} else
return QCoreApplication::translate( "progress", "Uploaded");
}
return QCoreApplication::translate( "progress", "Processed Bundle");
case CSYNC_INSTRUCTION_CONFLICT:
return QCoreApplication::translate( "progress", "Server version downloaded, copied changed local file into conflict file");
case CSYNC_INSTRUCTION_REMOVE:
+613
Ver Arquivo
@@ -0,0 +1,613 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@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 "propagateupload.h"
#include "propagatebundle.h"
#include "owncloudpropagator_p.h"
#include "networkjobs.h"
#include "account.h"
#include "syncjournaldb.h"
#include "syncjournalfilerecord.h"
#include "utility.h"
#include "filesystem.h"
#include "propagatorjobs.h"
#include "checksums.h"
#include "syncengine.h"
#include <QNetworkAccessManager>
#include <QFileInfo>
#include <QXmlStreamReader>
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
namespace {
const char owncloudShouldSoftCancelPropertyName[] = "owncloud-should-soft-cancel";
}
#endif
namespace OCC {
/**
* We do not want to upload files that are currently being modified.
* To avoid that, we don't upload files that have a modification time
* that is too close to the current time.
*
* This interacts with the msBetweenRequestAndSync delay in the folder
* manager. If that delay between file-change notification and sync
* has passed, we should accept the file for upload here.
*/
static bool fileIsStillChanging(const SyncFileItem & item)
{
const QDateTime modtime = Utility::qDateTimeFromTime_t(item._modtime);
const qint64 msSinceMod = modtime.msecsTo(QDateTime::currentDateTime());
return msSinceMod < SyncEngine::minimumFileAgeForUpload
// if the mtime is too much in the future we *do* upload the file
&& msSinceMod > -10000;
}
void PropagateBundle::start()
{
if (_propagator->_abortRequested.fetchAndAddRelaxed(0)) {
return;
}
if (!_itemsToChecksum.isEmpty()) {
//this will block other threads from processing files and bundling while this will do that
//after sufficient number of files being checksumed, flag will be unblocked
_preparingBundle = true;
//this will add checksums and remove itself from activeJobList after is completed
_propagator->_activeJobList.append(this);
return slotComputeTransmissionChecksum();
}
//this can generate itemDone(), dont add to activeJobList
startBundle();
}
void PropagateBundle::slotComputeTransmissionChecksum()
{
if (_propagator->_abortRequested.fetchAndAddRelaxed(0)) {
return;
}
const SyncFileItemPtr &item = _itemsToChecksum.first();
const QString filePath = _propagator->getFilePath(item->_file);
// remember the modtime before checksumming to be able to detect a file
// change during the checksum calculation
item->_modtime = FileSystem::getModTime(filePath);
QByteArray checksumType = contentChecksumType();
// Maybe the discovery already computed the checksum?
if (item->_contentChecksumType == checksumType
&& !item->_contentChecksum.isEmpty()) {
// Reuse the content checksum as the transmission checksum if possible
const auto supportedTransmissionChecksums =
_propagator->account()->capabilities().supportedChecksumTypes();
if (supportedTransmissionChecksums.contains(checksumType)) {
slotStartUpload(checksumType, item->_contentChecksum);
return;
}
}
// Compute the transmission checksum.
auto computeChecksum = new ComputeChecksum(this);
if (uploadChecksumEnabled()) {
computeChecksum->setChecksumType(_propagator->account()->capabilities().uploadChecksumType());
} else {
computeChecksum->setChecksumType(QByteArray());
}
connect(computeChecksum, SIGNAL(done(QByteArray,QByteArray)),
SLOT(slotStartUpload(QByteArray,QByteArray)));
computeChecksum->start(filePath);
}
void PropagateBundle::slotStartUpload(const QByteArray& transmissionChecksumType, const QByteArray& transmissionChecksum)
{
const SyncFileItemPtr &item = _itemsToChecksum.takeFirst();
item->_contentChecksum = transmissionChecksum;
item->_contentChecksumType = transmissionChecksumType;
// add this item to sync list, as it now has checksum computed
_itemsToSync.append(item);
_currentBundleSize += item->_size;
_currentRequestsNumber++;
// Remove ourselfs from the list of active job, before any posible call to itemDone()
_propagator->_activeJobList.removeOne(this);
// check if we have anything to add to our bundle
if (!_itemsToChecksum.empty()){
SyncFileItemPtr nextItem = _itemsToChecksum.first();
// Ensure that we always send at least 1/6 of total files to sync, to avoid situation that all files fit into 1 request (no parallelism)
quint64 bundleLimits = qMin(checkBundledRequestsLimits(), (_totalFiles/_propagator->hardMaximumActiveJob()));
// if next item will exceed bundle size, send the bundle now
// otherwise, compute next checksum
if (((_currentBundleSize + nextItem->_size) >= chunkSize())
|| (_currentRequestsNumber >= bundleLimits)){
_currentBundleSize = 0;
_currentRequestsNumber = 0;
startBundle();
} else {
start();
}
} else {
//_itemsToChecksum is already empty, send what is left in _itemsToSync.
startBundle();
}
}
void PropagateBundle::startBundle()
{
if (_propagator->_abortRequested.fetchAndAddRelaxed(0)) {
return;
}
// job takes ownership of device via a QScopedPointer. Job deletes itself when finishing
MultipartJob* job = new MultipartJob(_propagator->account(), _propagator->account()->davFilesPath(), new QHttpMultiPart(QHttpMultiPart::RelatedType));
// start constructing the bundle metadata xml file
QByteArray xml = "<?xml version='1.0' encoding='UTF-8'?>\n"
"<d:multipart xmlns:d=\"DAV:\">\n";
int contentID = 0;
while (!_itemsToSync.isEmpty()){
const SyncFileItemPtr &item = _itemsToSync.takeFirst();
const QString fullFilePath = _propagator->getFilePath(item->_file);
if (!FileSystem::fileExists(fullFilePath)) {
itemDone(item, SyncFileItem::SoftError, tr("File Removed"));
continue;
}
time_t prevModtime = item->_modtime; // the _item value was set in PropagateUploadFileQNAM::start()
// but a potential checksum calculation could have taken some time during which the file could
// have been changed again, so better check again here.
item->_modtime = FileSystem::getModTime(fullFilePath);
if( prevModtime != item->_modtime ) {
_propagator->_anotherSyncNeeded = true;
itemDone(item, SyncFileItem::SoftError, tr("Local file changed during syncing. It will be resumed."));
continue;
}
item->_size = FileSystem::getSize(fullFilePath);
// But skip the file if the mtime is too close to 'now'!
// That usually indicates a file that is still being changed
// or not yet fully copied to the destination.
if (fileIsStillChanging(*item)) {
_propagator->_anotherSyncNeeded = true;
itemDone(item, SyncFileItem::SoftError, tr("Local file changed during sync."));
continue;
}
//TODO use Upload Device to support bandwith limitation on the client
QFile *file = new QFile(fullFilePath);
if (! file->open(QIODevice::ReadOnly)) {
qDebug() << "ERR: Could not prepare upload device: " << file->errorString();
// If the file is currently locked, we want to retry the sync
// when it becomes available again.
if (FileSystem::isFileLocked(fullFilePath)) {
emit _propagator->seenLockedFile(fullFilePath);
}
// Soft error because this is likely caused by the user modifying his files while syncing
itemDone(item, SyncFileItem::SoftError, tr("ERR: Could not prepare upload device"));
} else {
// add xml part for this specific file
xml += " <d:part>\n"
" <d:prop>\n";
if (!item->_contentChecksumType.isEmpty()) {
xml += " <d:oc-checksum>" + makeChecksumHeader(item->_contentChecksumType, item->_contentChecksum) + "</d:oc-checksum>\n";
}
if(item->_file.contains(".sys.admin#recall#")) {
xml += " <d:oc-tag>" + QByteArray(".sys.admin#recall#") + "</d:oc-tag>\n";
}
xml += " <d:oc-path>" + getRemotePath(item->_file) + "</d:oc-path>\n"
" <d:oc-mtime>" + QByteArray::number(qint64(item->_modtime)) + "</d:oc-mtime>\n"
" <d:oc-id>" + QByteArray::number(contentID) + "</d:oc-id>\n"
" <d:oc-total-length>" + QByteArray::number(qint64(item->_size)) + "</d:oc-total-length>\n"
" </d:prop>\n"
" </d:part>\n";
QHttpPart bundleContent;
bundleContent.setRawHeader("Content-ID", QByteArray::number(qint64(contentID)));
bundleContent.setBody(file->readAll());
file->close();
job->addItemPart(bundleContent, item);
contentID++;
}
delete file;
}
// finish xml building
xml += "</d:multipart>";
if (!job->isEmpty())
{
QHttpPart bundleMetadata;
bundleMetadata.setBody(xml);
bundleMetadata.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("text/xml; charset=utf-8"));
bundleMetadata.setHeader(QNetworkRequest::ContentLengthHeader, QByteArray::number(xml.size()));
job->addRootPart(bundleMetadata);
_propagator->_activeJobList.append(this);
job->_duration.start();
_jobs.append(job);
connect(job, SIGNAL(finishedSignal()), this, SLOT(slotMultipartFinished()));
connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotJobDestroyed(QObject*)));
job->start();
// we have finished preparing bundles to send, allow other bundles to be send
_preparingBundle = false;
// Check if there are any items to be bundled and if you can run another parallel job
// Parallelisze to hardMaximumActiveJob() since it is the same as in upload.h -> isLikelyFinishedQuickly for small files
if (!_itemsToChecksum.empty() && (_propagator->_activeJobList.count() < _propagator->hardMaximumActiveJob())) {
start();
}
} else {
delete job;
}
}
QByteArray PropagateBundle::getRemotePath(QString filePath){
QString remotePath(_propagator->_remoteFolder+filePath);
return remotePath.toStdString().c_str();
}
void PropagateBundle::append(const SyncFileItemPtr &bundledFile){
_totalFiles++;
_size += bundledFile->_size;
_itemsToChecksum.append(bundledFile);
}
bool PropagateBundle::empty(){
return _itemsToChecksum.empty();
}
void PropagateBundle::slotMultipartFinished()
{
MultipartJob *job = qobject_cast<MultipartJob *>(sender());
Q_ASSERT(job);
slotJobDestroyed(job); // remove it from the _jobs list
qDebug() << Q_FUNC_INFO << job->reply()->request().url() << "FINISHED WITH STATUS"
<< job->reply()->error()
<< (job->reply()->error() == QNetworkReply::NoError ? QLatin1String("") : job->reply()->errorString())
<< job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute)
<< job->reply()->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
_propagator->_activeJobList.removeOne(this);
QNetworkReply::NetworkError err = job->reply()->error();
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
if (job->reply()->error() == QNetworkReply::OperationCanceledError && job->reply()->property(owncloudShouldSoftCancelPropertyName).isValid()) {
// Abort the job and try again later.
// This works around a bug in QNAM wich might reuse a non-empty buffer for the next request.
qDebug() << "Forcing job abort on HTTP connection reset with Qt < 5.4.2.";
_propagator->_anotherSyncNeeded = true;
abortWithError(SyncFileItem::SoftError, tr("Forcing job abort on HTTP connection reset with Qt < 5.4.2."));
return;
}
#endif
if (err != QNetworkReply::NoError) {
_item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QByteArray replyContent = job->reply()->readAll();
qDebug() << replyContent; // display the XML error in the debug
QString errorString = errorMessage(job->errorString(), replyContent);
if (job->reply()->hasRawHeader("OC-ErrorString")) {
errorString = job->reply()->rawHeader("OC-ErrorString");
}
//TODO: soft error till other mechanism will be added - this will retry bundle later.
abortWithError(SyncFileItem::SoftError, errorString);
return;
}
// Parse DAV response
QXmlStreamReader reader(job->reply());
reader.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("d", "DAV:"));
QString currentHref;
QString currentOcPath;
QString expectedPath(_propagator->account()->davFilesPath());
QMap<QString, QString> itemProperties;
QMap<QString, QMap<QString, QString> > responseObjectsProperties;
bool insidePropstat = false;
bool insideProp = false;
bool insideError = false;
while (!reader.atEnd()) {
QXmlStreamReader::TokenType type = reader.readNext();
QString name = reader.name().toString();
// Start elements with DAV:
if (type == QXmlStreamReader::StartElement && reader.namespaceUri() == QLatin1String("DAV:")) {
if (name == QLatin1String("href")) {
// We don't use URL encoding in our request URL (which is the expected path) (QNAM will do it for us)
// but the result will have URL encoding..
QString hrefString = QString::fromUtf8(QByteArray::fromPercentEncoding(reader.readElementText().toUtf8()));
if (!hrefString.startsWith(expectedPath)) {
qDebug() << "Invalid href" << hrefString << "expected ending with" << expectedPath;
}
currentHref = hrefString;
} else if (name == QLatin1String("response")) {
continue;
} else if (name == QLatin1String("propstat")) {
insidePropstat = true;
} else if (name == QLatin1String("status") && insidePropstat) {
QString httpStatus = reader.readElementText();
itemProperties.insert(name, httpStatus);
} else if (name == QLatin1String("prop")) {
insideProp = true;
continue;
} else if (name == QLatin1String("multistatus")) {
continue;
}
}
if (type == QXmlStreamReader::StartElement && insidePropstat && insideProp) {
if (name == QLatin1String("oc-path")){
currentOcPath = reader.readElementText(QXmlStreamReader::SkipChildElements);
} else if (name == QLatin1String("error")){
insideError = true;
} else if (name == QLatin1String("exception") && insideError){
itemProperties.insert(name, reader.readElementText(QXmlStreamReader::SkipChildElements));
} else if (name == QLatin1String("message") && insideError){
itemProperties.insert(name, reader.readElementText(QXmlStreamReader::SkipChildElements));
} else{
itemProperties.insert(name, reader.readElementText(QXmlStreamReader::SkipChildElements));
}
}
// End elements with DAV:
if (type == QXmlStreamReader::EndElement) {
if (reader.namespaceUri() == QLatin1String("DAV:")) {
if (reader.name() == "response") {
currentHref.clear();
} else if (reader.name() == "propstat") {
insidePropstat = false;
if (!currentOcPath.isEmpty()){
responseObjectsProperties.insert(currentOcPath, QMap<QString,QString>(itemProperties));
}
currentOcPath.clear();
itemProperties.clear();
} else if (reader.name() == "prop") {
insideProp = false;
} else if (reader.name() == "error") {
insideError = false;
}
}
}
}
if (reader.hasError()) {
// XML Parser error? Whatever had been emitted before will come as directoryListingIterated
qDebug() << "ERROR" << reader.errorString();
abortWithError(SyncFileItem::SoftError, tr("Cannot parse multistatus response!"));
return;
}
QList<SyncFileItemPtr> syncItems = job->syncItems();
//get the total duration, and try to estimate sync time for single item in the bundle
quint64 avgItemDuration = job->_duration.elapsed()/syncItems.count();
foreach(const SyncFileItemPtr &item, syncItems) {
item->_requestDuration = avgItemDuration;
item->_responseTimeStamp = job->responseTimestamp();
slotItemFinished(item, responseObjectsProperties);
}
// performance logging
qDebug() << "*==* duration BUNDLE UPLOAD" << syncItems.count() << "items synced"
<< "average duration" << avgItemDuration;
// The job might stay alive for the whole sync, release this tiny bit of memory.
if (_jobs.empty() && _itemsToChecksum.isEmpty()){
done(SyncFileItem::Success);
} else if (!_preparingBundle){
// This will be called only if some other thread is not currently preparing bundles (checksums)
start();
}
}
void PropagateBundle::slotItemFinished(const SyncFileItemPtr &item, QMap<QString, QMap<QString, QString> > &responseObjectsProperties)
{
QString itemFilePath(getRemotePath(item->_file));
QMap<QString, QString> fileProperties = responseObjectsProperties.value(itemFilePath);
item->_httpErrorCode = getHttpStatusCode(fileProperties.value("status"));
if (200 == item->_httpErrorCode){
// Check if the file still exists
const QString fullFilePath(_propagator->getFilePath(item->_file));
if( !FileSystem::fileExists(fullFilePath) ) {
_propagator->_anotherSyncNeeded = true;
}
if (! FileSystem::verifyFileUnchanged(fullFilePath, item->_size, item->_modtime)) {
_propagator->_anotherSyncNeeded = true;
}
//OC-FileID section
if (fileProperties.contains("oc-fileid")){
QString fid = fileProperties.value("oc-fileid");
if( !item->_fileId.isEmpty() && item->_fileId != fid ) {
qDebug() << "WARN: File ID changed!" << item->_fileId << fid;
}
item->_fileId =fid.toStdString().c_str();
}
//OC-ETag section
QByteArray ocEtag = parseEtag(fileProperties.value("oc-etag").toStdString().c_str());
QByteArray etag = parseEtag(fileProperties.value("etag").toStdString().c_str());
item->_etag = ocEtag.isEmpty() ? etag : ocEtag;
if (ocEtag.length() > 0 && ocEtag != etag) {
qDebug() << "Quite peculiar, we have an etag != OC-Etag [no problem!]" << etag << ocEtag;
}
if (fileProperties.value("oc-mtime") != "accepted"){
// OC-MTime is supported since owncloud 5.0. But not when chunking.
// Normally Owncloud 6 always puts OC-MTime
qWarning() << "Server does not support OC-MTime" << fileProperties.value("oc-mtime");
// Well, the mtime was not set
itemDone(item, SyncFileItem::SoftError, "Server does not support OC-MTime");
return;
}
} else{
//We do not check for problems with shared folder since it is only CREATE
//REMARK: if the file will support update, ensure to override checkForProblemsWithShared()
qDebug() << Q_FUNC_INFO << item->_file << "ERROR WITH STATUS"
<< fileProperties.value("status")
<< fileProperties.value("exception")
<< fileProperties.value("message");
if (412 == _item->_httpErrorCode) {
// Precondition Failed: Maybe the bad etag is in the database, we need to clear the
// parent folder etag so we won't read from DB next sync.
_propagator->_journal->avoidReadFromDbOnNextSync(item->_file);
_propagator->_anotherSyncNeeded = true;
}
item->_errorString = fileProperties.value("message");
item->_status = classifyError(QNetworkReply::ContentOperationNotPermittedError, item->_httpErrorCode,
&_propagator->_anotherSyncNeeded);
itemDone(item, item->_status, item->_errorString);
return;
}
if (!_propagator->_journal->setFileRecord(SyncJournalFileRecord(*item, _propagator->getFilePath(item->_file)))) {
itemDone(item, SyncFileItem::FatalError, tr("Error writing metadata to the database"));
return;
}
// Remove from the progress database:
_propagator->_journal->setUploadInfo(item->_file, SyncJournalDb::UploadInfo());
_propagator->_journal->commit("upload file start");
itemDone(item, SyncFileItem::Success);
}
int PropagateBundle::getHttpStatusCode(const QString &status){
//if cannot read code, it means that server is not supported and raise 500 Internal Server Error
int code = 500;
QStringList statusList = status.split(QRegExp("\\s+"));
if (2 < statusList.size()){
//we expect status list to be at least 3 elements and second element to be the http error code
code = statusList.at(1).toInt();
}
return code;
}
void PropagateBundle::slotJobDestroyed(QObject* job)
{
_jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job) , _jobs.end());
}
// This function is used whenever there is an error occuring and we need to raise status for whole the bundle
void PropagateBundle::abortWithError(SyncFileItem::Status status, const QString &error)
{
foreach(auto *job, _jobs) {
if (job->reply()) {
qDebug() << Q_FUNC_INFO << job;
job->reply()->abort();
}
}
done(status, error);
}
MultipartJob::~MultipartJob()
{
// Make sure that we destroy the QNetworkReply before our _device of which it keeps an internal pointer.
setReply(0);
}
void MultipartJob::start() {
while(!_syncParts.empty()) {
const QHttpPart &itemPart = _syncParts.takeFirst();
_multipart->append(itemPart);
}
QNetworkRequest req;
setReply(multipartRequest(path(), req, _multipart));
_multipart->setParent(reply()); // delete the multiPart with the job
setupConnections(reply());
if( reply()->error() != QNetworkReply::NoError ) {
qWarning() << Q_FUNC_INFO << " Network error: " << reply()->errorString();
}
connect(this, SIGNAL(networkActivity()), account().data(), SIGNAL(propagatorNetworkActivity()));
//TODO this uses _device, and we have no access here to that
// // For Qt versions not including https://codereview.qt-project.org/110150
// // Also do the runtime check if compiled with an old Qt but running with fixed one.
// // (workaround disabled on windows and mac because the binaries we ship have patched qt)
//#if QT_VERSION < QT_VERSION_CHECK(4, 8, 7)
// if (QLatin1String(qVersion()) < QLatin1String("4.8.7"))
// connect(_device.data(), SIGNAL(wasReset()), this, SLOT(slotSoftAbort()));
//#elif QT_VERSION > QT_VERSION_CHECK(5, 0, 0) && QT_VERSION < QT_VERSION_CHECK(5, 4, 2) && !defined Q_OS_WIN && !defined Q_OS_MAC
// if (QLatin1String(qVersion()) < QLatin1String("5.4.2"))
// connect(_device.data(), SIGNAL(wasReset()), this, SLOT(slotSoftAbort()));
//#endif
AbstractNetworkJob::start();
}
void MultipartJob::addItemPart(const QHttpPart &itemPart, const SyncFileItemPtr &item){
// add itemPart to queue, this queue will be used to construct the multipart message
_syncParts.append(itemPart);
// add item pointer to queue, it will be used to construct the response at the end.
_syncItems.append(item);
}
void MultipartJob::addRootPart(const QHttpPart &itemPart){
// if used, this part will be added to the begining of multipart request
_syncParts.prepend(itemPart);
}
void MultipartJob::slotTimeout() {
qDebug() << "Timeout" << (reply() ? reply()->request().url() : path());
if (!reply())
return;
_errorString = tr("Connection Timeout");
reply()->abort();
}
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
void MultipartJob::slotSoftAbort() {
reply()->setProperty(owncloudShouldSoftCancelPropertyName, true);
reply()->abort();
}
#endif
}
+141
Ver Arquivo
@@ -0,0 +1,141 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@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.
*/
#pragma once
#include "owncloudpropagator.h"
#include "networkjobs.h"
#include <QHttpMultiPart>
#include <QFile>
#include <QDebug>
namespace OCC {
/**
* @brief The MultipartJob class
* @ingroup libsync
*/
class MultipartJob : public AbstractNetworkJob {
Q_OBJECT
private:
QHttpMultiPart* _multipart;
// Vector_syncItems is used in constructing a response at the response arrival
QList<SyncFileItemPtr> _syncItems;
// Vector containing QHttpParts which will be later inserted into request body
QList<QHttpPart> _syncParts;
// error string
QString _errorString;
public:
QElapsedTimer _duration;
// Takes ownership of the device
explicit MultipartJob(AccountPtr account, const QString& path, QHttpMultiPart* multipart, QObject* parent = 0)
: AbstractNetworkJob(account, path, parent), _multipart(multipart), _errorString(QString()) {}
~MultipartJob();
virtual void start() Q_DECL_OVERRIDE;
virtual bool finished() Q_DECL_OVERRIDE {
emit finishedSignal();
return true;
}
void addItemPart(const QHttpPart &itemPart, const SyncFileItemPtr &item);
void addRootPart(const QHttpPart &itemPart);
bool isEmpty() {
return _syncItems.isEmpty();
}
QString errorString() {
return _errorString.isEmpty() ? reply()->errorString() : _errorString;
}
QList<SyncFileItemPtr> syncItems() const {
return _syncItems;
}
virtual void slotTimeout() Q_DECL_OVERRIDE;
signals:
void finishedSignal();
private slots:
#if QT_VERSION < 0x050402
void slotSoftAbort();
#endif
};
/**
* @brief The PropagateBundle class
* @ingroup libsync
*/
class PropagateBundle : public PropagateItemJob {
Q_OBJECT
private:
QLinkedList<SyncFileItemPtr> _itemsToSync;
QLinkedList<SyncFileItemPtr> _itemsToChecksum;
QVector<MultipartJob*> _jobs; /// network jobs that are currently in transit
bool _preparingBundle; // Flag blocking processing the same files by many threads
quint64 _size; // Tells what is the total size of _itemsToSync inside the bundle
quint64 _currentBundleSize;
quint64 _currentRequestsNumber;
quint64 _totalFiles;
// measure the performance of checksum calc and upload
Utility::StopWatch _stopWatch;
public:
PropagateBundle(OwncloudPropagator* propagator)
: PropagateItemJob(propagator, SyncFileItemPtr(new SyncFileItem)), _preparingBundle(true), _size(0), _currentBundleSize(0), _currentRequestsNumber(0), _totalFiles(0) {}
void start() Q_DECL_OVERRIDE;
void startBundle();
void append(const SyncFileItemPtr &bundledFile);
QByteArray getRemotePath(QString filePath);
bool empty();
bool scheduleNextJob() Q_DECL_OVERRIDE {
if (_state == NotYetStarted){
_state = Running;
QMetaObject::invokeMethod(this, "start");
return true;
}
return false;
}
quint64 syncItemsSize() const { return _size; }
quint64 syncItemsNumber() const { return _itemsToChecksum.count(); }
private slots:
void slotComputeTransmissionChecksum();
void slotStartUpload(const QByteArray& transmissionChecksumType, const QByteArray& transmissionChecksum);
void slotMultipartFinished();
void slotJobDestroyed(QObject *job);
void abortWithError(SyncFileItem::Status status, const QString &error);
private:
quint64 checkBundledRequestsLimits()
{
//TODO: obtain this value from the server or by other means
quint64 maximumBundledFiles = 100;
return maximumBundledFiles;
}
quint64 chunkSize() const { return _propagator->chunkSize(); }
int getHttpStatusCode(const QString &status);
void slotItemFinished(const SyncFileItemPtr &item, QMap<QString, QMap<QString, QString> > &responseObjectsProperties);
};
}
+1 -1
Ver Arquivo
@@ -32,6 +32,7 @@
#include <QDir>
#include <cmath>
#include <cstring>
#include <QXmlStreamReader>
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
namespace {
@@ -562,5 +563,4 @@ void PropagateUploadFileCommon::finalize()
done(SyncFileItem::Success);
}
}
+1 -2
Ver Arquivo
@@ -16,11 +16,11 @@
#include "owncloudpropagator.h"
#include "networkjobs.h"
#include <QHttpMultiPart>
#include <QBuffer>
#include <QFile>
#include <QDebug>
namespace OCC {
class BandwidthManager;
@@ -321,6 +321,5 @@ private slots:
void slotUploadProgress(qint64,qint64);
};
}
+1
Ver Arquivo
@@ -46,6 +46,7 @@ public:
enum Type {
UnknownType = 0,
RequestsContainer = 7,
File = CSYNC_FTW_TYPE_FILE,
Directory = CSYNC_FTW_TYPE_DIR,
SoftLink = CSYNC_FTW_TYPE_SLINK
+232 -3
Ver Arquivo
@@ -32,6 +32,7 @@ inline QString getFilePathFromUrl(const QUrl &url) {
return {};
}
static const QUrl sBundleRootUrl("owncloud://somehost/remote.php/dav/files/");
inline QString generateEtag() {
return QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch(), 16);
@@ -625,7 +626,6 @@ public:
qint64 readData(char *, qint64) override { return 0; }
};
class FakeErrorReply : public QNetworkReply
{
Q_OBJECT
@@ -649,6 +649,223 @@ public:
qint64 readData(char *, qint64) override { return 0; }
};
class FakeBundlePOSTReply : public QNetworkReply
{
Q_OBJECT
FileInfo *fileInfo;
QByteArray payload;
public:
FakeBundlePOSTReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &postPayload, QObject *parent)
: QNetworkReply{parent} {
setRequest(request);
QUrl rawUrl = request.url();
QString bundlePath(sBundleRootUrl.path()+rawUrl.userName());
setUrl(rawUrl);
setOperation(op);
open(QIODevice::ReadOnly);
const QString davUri{QStringLiteral("DAV:")};
const QString ocUri{QStringLiteral("http://owncloud.org/ns")};
const QString sabUri{QStringLiteral("http://sabredav.org/ns")};
QBuffer buffer{&payload};
buffer.open(QIODevice::WriteOnly);
QXmlStreamWriter xml( &buffer );
xml.writeNamespace(davUri, "d");
xml.writeNamespace(ocUri, "o");
xml.writeNamespace(sabUri, "s");
if ("erroruser" == rawUrl.userName()) {
// ERROR CASE
xml.writeStartDocument();
xml.writeStartElement(davUri, QStringLiteral("error"));
xml.writeTextElement(sabUri, QStringLiteral("exception"), QStringLiteral("OCA\\DAV\\Connector\\Sabre\\Exception\\Forbidden"));
xml.writeTextElement(sabUri, QStringLiteral("message"), QStringLiteral("URL endpoint has to be instance of \\OCA\\DAV\\Files\\FilesHome"));
xml.writeTextElement(ocUri, QStringLiteral("retry"), QStringLiteral("false"));
xml.writeTextElement(ocUri, QStringLiteral("reason"), QStringLiteral("URL endpoint has to be instance of \\OCA\\DAV\\Files\\FilesHome"));
xml.writeEndElement(); // error
xml.writeEndDocument();
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 403);
} else {
//NORMAL CASE
auto writeFileResponse = [&](const FileInfo &fileInfo) {
xml.writeStartElement(davUri, QStringLiteral("response"));
//TODO: no need for X-OC-PATH, href could contain that, fix client/server
xml.writeTextElement(davUri, QStringLiteral("href"), bundlePath);
xml.writeStartElement(davUri, QStringLiteral("propstat"));
xml.writeStartElement(davUri, QStringLiteral("prop"));
xml.writeTextElement(davUri, QStringLiteral("oc-etag"), fileInfo.etag);
xml.writeTextElement(davUri, QStringLiteral("etag"), fileInfo.etag);
xml.writeTextElement(davUri, QStringLiteral("oc-fileid"), fileInfo.fileId);
xml.writeTextElement(davUri, QStringLiteral("oc-mtime"), QStringLiteral("accepted"));
//TODO: this slash to be fixed on client/server
xml.writeTextElement(davUri, QStringLiteral("oc-path"), "/"+fileInfo.path());
xml.writeEndElement(); // prop
xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK");
xml.writeEndElement(); // propstat
xml.writeEndElement(); // response
};
auto writeFileErrorResponse = [&](const FileInfo &fileInfo, const QString &exception, const QString &message, const QString &status) {
xml.writeStartElement(davUri, QStringLiteral("response"));
//TODO: no need for X-OC-PATH, href could contain that, fix client/server
xml.writeTextElement(davUri, QStringLiteral("href"), bundlePath);
xml.writeStartElement(davUri, QStringLiteral("propstat"));
xml.writeStartElement(davUri, QStringLiteral("prop"));
xml.writeStartElement(davUri, QStringLiteral("error"));
xml.writeTextElement(sabUri, QStringLiteral("exception"), exception);
xml.writeTextElement(sabUri, QStringLiteral("message"), message);
xml.writeEndElement(); // error
//TODO: this slash to be fixed on client/server
xml.writeTextElement(davUri, QStringLiteral("oc-path"), "/"+fileInfo.path());
xml.writeEndElement(); // prop
xml.writeTextElement(davUri, QStringLiteral("status"), status);
xml.writeEndElement(); // propstat
xml.writeEndElement(); // response
};
Q_ASSERT(request.url().path().endsWith(bundlePath));
xml.writeStartDocument();
xml.writeStartElement(davUri, QStringLiteral("multistatus"));
QString headerSectEnd = "\r\n\r\n";
QString headerEnd = "\r\n";
QString headerConType= "Content-Type: ";
QString headerConID= "Content-ID: ";
QString multipartStart = "<d:multipart xmlns:d=\"DAV:\">";
QString partStart = "<d:part>";
QString pathStart = "<d:oc-path>";
QString pathEnd = "</d:oc-path>";
QString mtimeStart = "<d:oc-mtime>";
QString mtimeEnd = "</d:oc-mtime>";
QString cidStart = "<d:oc-id>";
QString cidEnd = "</d:oc-id>";
QString lengthStart = "<d:oc-total-length>";
QString lenghtEnd = "</d:oc-total-length>";
//we will use it to navigate in the request body
int indexPointer = 0;
int indexPointerEnd = 0;
QChar contentChar;
QMap<int, QMap<QString, QString>> pathMap;
QMap<QString, QString> objectMap;
/* Find Content-Type of bundle metadata.*/
//find index of content type and move to its end
indexPointer = postPayload.indexOf(headerConType,indexPointerEnd) + headerConType.length();
//find index of \r\n, so end of HTTP header and move to its end
indexPointerEnd = postPayload.indexOf(headerEnd,indexPointer);
//assert if what is between these indexes is our desired content type
Q_ASSERT(postPayload.mid(indexPointer,indexPointerEnd-indexPointer) == QString("text/xml; charset=utf-8"));
/* Verify if there is xml which contains multipartStart element*/
//find index of nearest header end and move to its end
indexPointer = postPayload.indexOf(headerSectEnd,indexPointerEnd) + headerSectEnd.length();
//
indexPointerEnd = postPayload.indexOf(multipartStart,indexPointer);
//assert if what is between these indexes is our desired content type
Q_ASSERT(postPayload.mid(indexPointer,indexPointerEnd-indexPointer) == QString("<?xml version='1.0' encoding='UTF-8'?>\n"));
/*Check metadata contents*/
while(postPayload.indexOf(partStart,indexPointerEnd) + partStart.length() >=indexPointerEnd) {
//find oc-path
indexPointer = postPayload.indexOf(pathStart,indexPointerEnd) + pathStart.length();
indexPointerEnd = postPayload.indexOf(pathEnd,indexPointer);
QString filePath(postPayload.mid(indexPointer,indexPointerEnd-indexPointer));
Q_ASSERT(!filePath.isNull());
indexPointer = postPayload.indexOf(mtimeStart,indexPointerEnd) + mtimeStart.length();
indexPointerEnd = postPayload.indexOf(mtimeEnd,indexPointer);
QString fileMtime(postPayload.mid(indexPointer,indexPointerEnd-indexPointer));
Q_ASSERT(!fileMtime.isNull());
indexPointer = postPayload.indexOf(cidStart,indexPointerEnd) + cidStart.length();
indexPointerEnd = postPayload.indexOf(cidEnd,indexPointer);
QString fileID(postPayload.mid(indexPointer,indexPointerEnd-indexPointer));
Q_ASSERT(!fileID.isNull());
indexPointer = postPayload.indexOf(lengthStart,indexPointerEnd) + lengthStart.length();
indexPointerEnd = postPayload.indexOf(lenghtEnd,indexPointer);
QString fileLength(postPayload.mid(indexPointer,indexPointerEnd-indexPointer));
Q_ASSERT(!fileLength.isNull());
objectMap["mtime"] = fileMtime;
objectMap["path"] = filePath;
objectMap["length"] = fileLength;
pathMap[fileID.toInt()] = objectMap;
}
while(postPayload.indexOf(headerConID,indexPointerEnd) + headerConID.length() >=indexPointerEnd) {
/* Find Content-ID of bundle binary.*/
indexPointer = postPayload.indexOf(headerConID,indexPointerEnd) + headerConID.length();
indexPointerEnd = postPayload.indexOf(headerEnd,indexPointer);
//assert if what is between these indexes is our desired content type
int id = QString(postPayload.mid(indexPointer,indexPointerEnd-indexPointer)).toInt();
Q_ASSERT(pathMap.contains(id));
indexPointer = postPayload.indexOf(headerSectEnd,indexPointerEnd) + headerSectEnd.length();
Q_ASSERT(postPayload.mid(indexPointer,1).size()==1);
QChar contentChar(postPayload.mid(indexPointer,1)[0]);
QString filePath(pathMap[id]["path"]);
QString fileSize(pathMap[id]["length"]);
if ((fileInfo = remoteRootFileInfo.find(filePath))) {
fileInfo->size = fileSize.toInt();
fileInfo->contentChar = contentChar.toAscii();
} else {
// Assume that the file is filled with the same character
fileInfo = remoteRootFileInfo.create(filePath, fileSize.toInt(), contentChar.toAscii());
}
if (!fileInfo) {
abort();
return;
}
if (filePath.endsWith("normalerrorfile")){
writeFileErrorResponse(*fileInfo, QStringLiteral("Sabre\\DAV\\Exception\\BadRequest"), QStringLiteral("Method not allowed - file exists - update of the file is not supported!"), QStringLiteral("HTTP/1.1 400 Bad Request"));
} else if (filePath.endsWith("fatalerrorfile")){
writeFileErrorResponse(*fileInfo, QStringLiteral("Sabre\\DAV\\Exception\\ServiceUnavailable"), QStringLiteral("Failed to check file size"), QStringLiteral("HTTP/1.1 503 Service Unavailable"));
} else if (filePath.endsWith("softerrorfile")){
writeFileErrorResponse(*fileInfo, QStringLiteral("OCA\\DAV\\Connector\\Sabre\\Exception\\FileLocked"), QStringLiteral("Target file is locked by another process."), QStringLiteral("HTTP/1.1 423 Locked (WebDAV; RFC 4918)"));
} else {
writeFileResponse(*fileInfo);
}
}
xml.writeEndElement(); // multistatus
xml.writeEndDocument();
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
setFinished(true);
setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8");
setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
}
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
}
Q_INVOKABLE void respond() {
emit metaDataChanged();
if (bytesAvailable())
emit readyRead();
emit finished();
}
void abort() override { }
qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
qint64 readData(char *data, qint64 maxlen) override {
qint64 len = std::min(qint64{payload.size()}, maxlen);
strncpy(data, payload.constData(), len);
payload.remove(0, len);
return len;
}
};
class FakeQNAM : public QNetworkAccessManager
{
FileInfo _remoteRootFileInfo;
@@ -664,7 +881,13 @@ protected:
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
QIODevice *outgoingData = 0) {
const QString fileName = getFilePathFromUrl(request.url());
Q_ASSERT(!fileName.isNull());
if (op != QNetworkAccessManager::PostOperation){
Q_ASSERT(!fileName.isNull());
} else {
//For Bundle, fileName of bundle is "". Otherwise, assert
Q_ASSERT(fileName.isNull());
}
if (_errorPaths.contains(fileName))
return new FakeErrorReply{op, request, this};
@@ -687,7 +910,9 @@ protected:
return new FakeMoveReply{info, op, request, this};
else if (verb == QLatin1String("MOVE") && isUpload)
return new FakeChunkMoveReply{info, _remoteRootFileInfo, op, request, this};
else {
else if (op == QNetworkAccessManager::PostOperation) {
return new FakeBundlePOSTReply{_remoteRootFileInfo, op, request, outgoingData->readAll(), this};
} else {
qDebug() << verb << outgoingData;
Q_UNREACHABLE();
}
@@ -807,6 +1032,10 @@ public:
return execUntilFinished();
}
OCC::AccountPtr getAccount() {
return _account;
}
private:
static void toDisk(QDir &dir, const FileInfo &templateFi) {
foreach (const FileInfo &child, templateFi.children) {
+75
Ver Arquivo
@@ -31,6 +31,16 @@ bool itemDidCompleteSuccessfully(const QSignalSpy &spy, const QString &path)
return false;
}
SyncFileItem::Status itemDidCompleteWithStatus(const QSignalSpy &spy, const QString &path)
{
for(const QList<QVariant> &args : spy) {
SyncFileItem item = args[0].value<SyncFileItem>();
if (item.destination() == path)
return item._status;
}
return SyncFileItem::NoStatus;
}
class TestSyncEngine : public QObject
{
Q_OBJECT
@@ -54,6 +64,71 @@ private slots:
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
void testFileUploadBundled() {
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
QVariantMap capBundle;
capBundle["bundlerequest"] = "1.0";
QVariantMap caps;
caps["dav"] = capBundle;
fakeFolder.getAccount()->setCapabilities(caps);
testFileUploadBundledAllFilesOK(fakeFolder);
testFileUploadBundledErrorForFile(fakeFolder);
//TODO unfinished, cannot generate NetworkError
//testFileUploadBundledNotHomeCollection(fakeFolder);
}
void testFileUploadBundledAllFilesOK(FakeFolder &fakeFolder) {
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
fakeFolder.localModifier().insert("A/a3");
fakeFolder.localModifier().insert("A/a4");
fakeFolder.syncOnce();
//check separate files
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a3"));
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a4"));
//check whole bundle
QVERIFY(itemDidCompleteSuccessfully(completeSpy, ""));
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
void testFileUploadBundledErrorForFile(FakeFolder &fakeFolder) {
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
fakeFolder.localModifier().insert("A/a5");
fakeFolder.localModifier().insert("A/normalerrorfile");
fakeFolder.localModifier().insert("A/fatalerrorfile");
fakeFolder.localModifier().insert("A/softerrorfile");
fakeFolder.syncOnce();
//check separate files
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a5"));
QVERIFY(SyncFileItem::NormalError == itemDidCompleteWithStatus(completeSpy, "A/normalerrorfile"));
QVERIFY(SyncFileItem::FatalError == itemDidCompleteWithStatus(completeSpy, "A/fatalerrorfile"));
QVERIFY(SyncFileItem::SoftError == itemDidCompleteWithStatus(completeSpy, "A/softerrorfile"));
//check whole bundle
QVERIFY(itemDidCompleteSuccessfully(completeSpy, ""));
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
void testFileUploadBundledNotHomeCollection(FakeFolder &fakeFolder) {
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
fakeFolder.localModifier().insert("A/a7");
fakeFolder.localModifier().insert("A/a8");
fakeFolder.localModifier().insert("B/b4");
//add the user "erroruser" which is not a FilesHomeCollection
fakeFolder.getAccount()->setUrl(QUrl(QStringLiteral("http://erroruser:admin@localhost/owncloud")));
fakeFolder.syncOnce();
//check whole bundle
QVERIFY(itemDidCompleteWithStatus(completeSpy, "") == SyncFileItem::SoftError);
//QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
void testDirDownload() {
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));