[ExternalStorage] add WebDAV external storage implementation

This commit is contained in:
Julien Cabieces 2021-05-11 10:42:59 +02:00
parent 0ce7f90350
commit 96eb75a118
23 changed files with 619 additions and 22 deletions

View File

@ -11,12 +11,20 @@ services:
httpbin:
image: kennethreitz/httpbin:latest
webdav:
image: nginx
volumes:
- ${GH_WORKSPACE}/.docker/webdav/nginx.conf:/etc/nginx/conf.d/default.conf
- ${GH_WORKSPACE}/.docker/webdav/passwords.list:/etc/nginx/.passwords.list
- /tmp/webdav_tests:/tmp/webdav_tests_root/webdav_tests
qgis-deps:
tty: true
image: qgis3-build-deps-binary-image
volumes:
- ${GH_WORKSPACE}:/root/QGIS
# links:
links:
- webdav
# - mssql
links:
- httpbin

View File

@ -20,3 +20,6 @@ QGIS_CONTINUOUS_INTEGRATION_RUN=true
PUSH_TO_CDASH=false
XDG_RUNTIME_DIR=/tmp
QGIS_WEBDAV_HOST=webdav
QGIS_WEBDAV_PORT=80

21
.docker/webdav/nginx.conf Normal file
View File

@ -0,0 +1,21 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
location /webdav_tests {
auth_basic realm_name;
auth_basic_user_file /etc/nginx/.passwords.list;
dav_methods PUT DELETE MKCOL COPY MOVE;
#dav_ext_methods PROPFIND OPTIONS;
dav_access user:rw group:rw all:r;
autoindex on;
client_max_body_size 0;
create_full_put_path on;
root /tmp/webdav_tests_root;
}
}

View File

@ -0,0 +1 @@
qgis:$apr1$cxID/nB1$3tG4J0FkYvEHyWAB.yqjo.

View File

@ -386,4 +386,5 @@ jobs:
[[ ${{ matrix.test-batch }} == "ORACLE" ]] && sudo rm -rf /usr/share/dotnet/sdk
echo "TEST_BATCH=$TEST_BATCH"
echo "DOCKERFILE=$DOCKERFILE"
mkdir -p /tmp/webdav_tests && chmod 777 /tmp/webdav_tests
docker-compose -f .docker/$DOCKERFILE run qgis-deps /root/QGIS/.docker/docker-qgis-test.sh $TEST_BATCH

View File

@ -69,7 +69,7 @@ can be retrieved by calling :py:func:`~QgsBlockingNetworkRequest.errorMessage`.
.. seealso:: :py:func:`post`
%End
ErrorCode post( QNetworkRequest &request, const QByteArray &data, bool forceRefresh = false, QgsFeedback *feedback = 0 );
ErrorCode post( QNetworkRequest &request, QIODevice *data, bool forceRefresh = false, QgsFeedback *feedback = 0 );
%Docstring
Performs a "post" operation on the specified ``request``, using the given ``data``.
@ -89,6 +89,15 @@ If an error was encountered then a specific ErrorCode will be returned, and a de
can be retrieved by calling :py:func:`~QgsBlockingNetworkRequest.errorMessage`.
.. seealso:: :py:func:`get`
.. versionadded:: 3.22
%End
ErrorCode post( QNetworkRequest &request, const QByteArray &data, bool forceRefresh = false, QgsFeedback *feedback = 0 );
%Docstring
This is an overloaded function.
Performs a "post" operation on the specified ``request``, using the given ``data``.
%End
ErrorCode head( QNetworkRequest &request, bool forceRefresh = false, QgsFeedback *feedback = 0 );
@ -113,7 +122,7 @@ can be retrieved by calling :py:func:`~QgsBlockingNetworkRequest.errorMessage`.
.. versionadded:: 3.18
%End
ErrorCode put( QNetworkRequest &request, const QByteArray &data, QgsFeedback *feedback = 0 );
ErrorCode put( QNetworkRequest &request, QIODevice *data, QgsFeedback *feedback = 0 );
%Docstring
Performs a "put" operation on the specified ``request``, using the given ``data``.
@ -129,6 +138,15 @@ by calling :py:func:`~QgsBlockingNetworkRequest.reply`.
If an error was encountered then a specific ErrorCode will be returned, and a detailed error message
can be retrieved by calling :py:func:`~QgsBlockingNetworkRequest.errorMessage`.
.. versionadded:: 3.22
%End
ErrorCode put( QNetworkRequest &request, const QByteArray &data, QgsFeedback *feedback = 0 );
%Docstring
This is an overloaded function.
Performs a "put" operation on the specified ``request``, using the given ``data``.
.. versionadded:: 3.18
%End
@ -192,6 +210,13 @@ Emitted when when data arrives during a request.
void downloadFinished();
%Docstring
Emitted once a request has finished downloading.
%End
void uploadProgress( qint64, qint64 );
%Docstring
Emitted when when data are sent during a request.
.. versionadded:: 3.22
%End
};

View File

@ -95,6 +95,14 @@ Emitted when content has loaded
Emitted when data is received.
.. versionadded:: 3.2
%End
void errorOccurred( QNetworkReply::NetworkError code, const QString &errorMsg );
%Docstring
Emitted when an error with ``code`` error occured while processing the request
``errorMsg`` is a textual description of the error
.. versionadded:: 3.22
%End
};

View File

@ -32,7 +32,8 @@ FetchedContent holds useful information about a network content being fetched
Failed
};
explicit QgsFetchedContent( const QString &url, QTemporaryFile *file = 0, ContentStatus status = NotStarted );
explicit QgsFetchedContent( const QString &url, QTemporaryFile *file = 0, ContentStatus status = NotStarted,
const QString &authConfig = QString() );
%Docstring
Constructs a FetchedContent with pointer to the downloaded file and status of the download
%End
@ -54,6 +55,11 @@ Returns the status of the download
QNetworkReply::NetworkError error() const;
%Docstring
Returns the potential error of the download
%End
QString authConfig() const;
%Docstring
Returns the authentication configuration id use for this fetched content
%End
public slots:
@ -74,6 +80,14 @@ Cancel the download operation.
void fetched();
%Docstring
Emitted when the file is fetched and accessible
%End
void errorOccurred( QNetworkReply::NetworkError code, const QString &errorMsg );
%Docstring
Emitted when an error with ``code`` error occured while processing the request
``errorMsg`` is a textual description of the error
.. versionadded:: 3.22
%End
};
@ -103,12 +117,13 @@ Create the registry for temporary downloaded files
~QgsNetworkContentFetcherRegistry();
const QgsFetchedContent *fetch( const QString &url, Qgis::ActionStart fetchingMode = Qgis::ActionStart::Deferred );
QgsFetchedContent *fetch( const QString &url, Qgis::ActionStart fetchingMode = Qgis::ActionStart::Deferred, const QString &authConfig = QString() );
%Docstring
Initialize a download for the given URL
:param url: the URL to be fetched
:param fetchingMode: defines if the download will start immediately or shall be manually triggered
:param authConfig: authentication configuration id to be used while fetching
.. note::

View File

@ -91,6 +91,14 @@ of whether the fetch was successful or not.
Users of QgsNetworkContentFetcherTask should connect to this signal,
and from the associated slot they can then safely access the network :py:func:`~QgsNetworkContentFetcherTask.reply`
without danger of the task being first removed by the :py:class:`QgsTaskManager`.
%End
void errorOccurred( QNetworkReply::NetworkError code, const QString &errorMsg );
%Docstring
Emitted when an error with ``code`` error occured while processing the request
``errorMsg`` is a textual description of the error
.. versionadded:: 3.22
%End
};

View File

@ -133,6 +133,7 @@ set(QGIS_CORE_SRCS
externalstorage/qgsexternalstorage.cpp
externalstorage/qgsexternalstorageregistry.cpp
externalstorage/qgssimplecopyexternalstorage.cpp
externalstorage/qgswebdavexternalstorage.cpp
layertree/qgscolorramplegendnode.cpp
layertree/qgscolorramplegendnodesettings.cpp
@ -1750,8 +1751,8 @@ set(QGIS_CORE_PRIVATE_HDRS
qgsspatialindexkdbush_p.h
editform/qgseditformconfig_p.h
externalstorage/qgssimplecopyexternalstorage_p.h
externalstorage/qgswebdavexternalstorage_p.h
proj/qgscoordinatereferencesystem_p.h
proj/qgscoordinatetransformcontext_p.h

View File

@ -17,10 +17,12 @@
#include "qgsexternalstorage.h"
#include "qgssimplecopyexternalstorage_p.h"
#include "qgswebdavexternalstorage_p.h"
QgsExternalStorageRegistry::QgsExternalStorageRegistry()
{
registerExternalStorage( new QgsSimpleCopyExternalStorage() );
registerExternalStorage( new QgsWebDAVExternalStorage() );
}
QgsExternalStorageRegistry::~QgsExternalStorageRegistry()

View File

@ -0,0 +1,200 @@
/***************************************************************************
qgswebdavexternalstorage.cpp
--------------------------------------
Date : March 2021
Copyright : (C) 2021 by Julien Cabieces
Email : julien dot cabieces at oslandia dot 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. *
* *
***************************************************************************/
#include "qgswebdavexternalstorage_p.h"
#include "qgsnetworkcontentfetcherregistry.h"
#include "qgsblockingnetworkrequest.h"
#include "qgsnetworkaccessmanager.h"
#include "qgsapplication.h"
#include <QFile>
#include <QPointer>
#include <QFileInfo>
QgsWebDAVExternalStorageStoreTask::QgsWebDAVExternalStorageStoreTask( const QUrl &url, const QString &filePath, const QString &authCfg )
: QgsTask( tr( "Storing %1" ).arg( QFileInfo( filePath ).baseName() ) )
, mUrl( url )
, mFilePath( filePath )
, mAuthCfg( authCfg )
, mFeedback( new QgsFeedback( this ) )
{
}
bool QgsWebDAVExternalStorageStoreTask::run()
{
QgsBlockingNetworkRequest request;
request.setAuthCfg( mAuthCfg );
QNetworkRequest req( mUrl );
QgsSetRequestInitiatorClass( req, QStringLiteral( "QgsWebDAVExternalStorageStoreTask" ) );
QFile *f = new QFile( mFilePath );
f->open( QIODevice::ReadOnly );
connect( &request, &QgsBlockingNetworkRequest::uploadProgress, this, [ = ]( qint64 bytesReceived, qint64 bytesTotal )
{
if ( !isCanceled() && bytesTotal > 0 )
{
const int progress = ( bytesReceived * 100 ) / bytesTotal;
setProgress( progress );
}
} );
QgsBlockingNetworkRequest::ErrorCode err = request.put( req, f, mFeedback );
if ( err != QgsBlockingNetworkRequest::NoError )
{
mErrorString = request.errorMessage();
}
return !isCanceled() && err == QgsBlockingNetworkRequest::NoError;
}
void QgsWebDAVExternalStorageStoreTask::cancel()
{
mFeedback->cancel();
QgsTask::cancel();
}
QString QgsWebDAVExternalStorageStoreTask::errorString() const
{
return mErrorString;
}
QgsWebDAVExternalStorageStoredContent::QgsWebDAVExternalStorageStoredContent( const QString &filePath, const QString &url, const QString &authcfg )
{
QString storageUrl = url;
if ( storageUrl.endsWith( "/" ) )
storageUrl.append( QFileInfo( filePath ).fileName() );
mUploadTask = new QgsWebDAVExternalStorageStoreTask( storageUrl, filePath, authcfg );
connect( mUploadTask, &QgsTask::taskCompleted, this, [ = ]
{
mUrl = storageUrl;
mStatus = Qgis::ContentStatus::Finished;
emit stored();
} );
connect( mUploadTask, &QgsTask::taskTerminated, this, [ = ]
{
reportError( mUploadTask->errorString() );
} );
connect( mUploadTask, &QgsTask::progressChanged, this, [ = ]( double progress )
{
emit progressChanged( progress );
} );
}
void QgsWebDAVExternalStorageStoredContent::store()
{
mStatus = Qgis::ContentStatus::Running;
QgsApplication::instance()->taskManager()->addTask( mUploadTask );
}
void QgsWebDAVExternalStorageStoredContent::cancel()
{
if ( !mUploadTask )
return;
disconnect( mUploadTask, &QgsTask::taskTerminated, this, nullptr );
connect( mUploadTask, &QgsTask::taskTerminated, this, [ = ]
{
mStatus = Qgis::ContentStatus::Canceled;
emit canceled();
} );
mUploadTask->cancel();
}
QString QgsWebDAVExternalStorageStoredContent::url() const
{
return mUrl;
}
QgsWebDAVExternalStorageFetchedContent::QgsWebDAVExternalStorageFetchedContent( QgsFetchedContent *fetchedContent )
: mFetchedContent( fetchedContent )
{
connect( mFetchedContent, &QgsFetchedContent::fetched, this, &QgsWebDAVExternalStorageFetchedContent::onFetched );
connect( mFetchedContent, &QgsFetchedContent::errorOccurred, this, [ = ]( QNetworkReply::NetworkError code, const QString & errorMsg )
{
Q_UNUSED( code );
reportError( errorMsg );
} );
}
void QgsWebDAVExternalStorageFetchedContent::fetch()
{
if ( !mFetchedContent )
return;
mStatus = Qgis::ContentStatus::Running;
mFetchedContent->download();
// could be already fetched/cached
if ( mFetchedContent->status() == QgsFetchedContent::Finished )
{
mStatus = Qgis::ContentStatus::Finished;
emit fetched();
}
}
QString QgsWebDAVExternalStorageFetchedContent::filePath() const
{
return mFetchedContent ? mFetchedContent->filePath() : QString();
}
void QgsWebDAVExternalStorageFetchedContent::onFetched()
{
if ( !mFetchedContent )
return;
if ( mFetchedContent->status() == QgsFetchedContent::Finished )
{
mStatus = Qgis::ContentStatus::Finished;
emit fetched();
}
}
void QgsWebDAVExternalStorageFetchedContent::cancel()
{
mFetchedContent->cancel();
}
QString QgsWebDAVExternalStorage::type() const
{
return QStringLiteral( "WebDAV" );
};
QString QgsWebDAVExternalStorage::displayName() const
{
return QObject::tr( "WebDAV Storage" );
};
QgsExternalStorageStoredContent *QgsWebDAVExternalStorage::doStore( const QString &filePath, const QString &url, const QString &authcfg ) const
{
return new QgsWebDAVExternalStorageStoredContent( filePath, url, authcfg );
};
QgsExternalStorageFetchedContent *QgsWebDAVExternalStorage::doFetch( const QString &url, const QString &authConfig ) const
{
QgsFetchedContent *fetchedContent = QgsApplication::instance()->networkContentFetcherRegistry()->fetch( url, Qgis::ActionStart::Deferred, authConfig );
return new QgsWebDAVExternalStorageFetchedContent( fetchedContent );
}

View File

@ -0,0 +1,141 @@
/***************************************************************************
qgswebdavexternalstorage.h
--------------------------------------
Date : March 2021
Copyright : (C) 2021 by Julien Cabieces
Email : julien dot cabieces at oslandia dot 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. *
* *
***************************************************************************/
#ifndef QGSWEBDAVEXTERNALSTORAGE_H
#define QGSWEBDAVEXTERNALSTORAGE_H
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgstaskmanager.h"
#include "externalstorage/qgsexternalstorage.h"
#include <QPointer>
class QgsWebDAVExternalStorageStoreTask;
class QgsFetchedContent;
///@cond PRIVATE
#define SIP_NO_FILE
/**
* \ingroup core
* \brief External storage implementation using the protocol WebDAV.
*
* \since QGIS 3.22
*/
class CORE_EXPORT QgsWebDAVExternalStorage : public QgsExternalStorage
{
public:
QString type() const override;
QString displayName() const override;
protected:
QgsExternalStorageStoredContent *doStore( const QString &filePath, const QString &url, const QString &authcfg = QString() ) const override;
QgsExternalStorageFetchedContent *doFetch( const QString &url, const QString &authConfig = QString() ) const override;
};
/**
* \ingroup core
* \brief Class for WebDAV stored content
*
* \since QGIS 3.22
*/
class QgsWebDAVExternalStorageStoredContent : public QgsExternalStorageStoredContent
{
Q_OBJECT
public:
QgsWebDAVExternalStorageStoredContent( const QString &filePath, const QString &url, const QString &authcfg = QString() );
void cancel() override;
QString url() const override;
void store() override;
private:
QPointer<QgsWebDAVExternalStorageStoreTask> mUploadTask;
QString mUrl;
};
/**
* \ingroup core
* \brief Class for WebDAV fetched content
*
* \since QGIS 3.22
*/
class QgsWebDAVExternalStorageFetchedContent : public QgsExternalStorageFetchedContent
{
Q_OBJECT
public:
QgsWebDAVExternalStorageFetchedContent( QgsFetchedContent *fetchedContent );
QString filePath() const override;
void cancel() override;
void fetch() override;
private slots:
void onFetched();
private:
QPointer<QgsFetchedContent> mFetchedContent;
};
/**
* \ingroup core
* \brief Task to store a file to a given WebDAV url
*
* \since QGIS 3.22
*/
class QgsWebDAVExternalStorageStoreTask : public QgsTask
{
Q_OBJECT
public:
QgsWebDAVExternalStorageStoreTask( const QUrl &url, const QString &filePath, const QString &authCfg );
bool run() override;
void cancel() override;
QString errorString() const;
private:
const QUrl mUrl;
const QString mFilePath;
const QString mAuthCfg;
QgsFeedback *mFeedback = nullptr;
QString mErrorString;
};
#endif // QGSWEBDAVEXTERNALSTORAGE_H

View File

@ -27,6 +27,7 @@
#include <QWaitCondition>
#include <QNetworkCacheMetaData>
#include <QAuthenticator>
#include <QBuffer>
QgsBlockingNetworkRequest::QgsBlockingNetworkRequest()
{
@ -60,6 +61,14 @@ QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::get( QNetworkReq
}
QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::post( QNetworkRequest &request, const QByteArray &data, bool forceRefresh, QgsFeedback *feedback )
{
QByteArray ldata( data );
QBuffer buffer( &ldata );
buffer.open( QIODevice::ReadOnly );
return post( request, &buffer, forceRefresh, feedback );
}
QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::post( QNetworkRequest &request, QIODevice *data, bool forceRefresh, QgsFeedback *feedback )
{
mPayloadData = data;
return doRequest( Post, request, forceRefresh, feedback );
@ -71,6 +80,14 @@ QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::head( QNetworkRe
}
QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::put( QNetworkRequest &request, const QByteArray &data, QgsFeedback *feedback )
{
QByteArray ldata( data );
QBuffer buffer( &ldata );
buffer.open( QIODevice::ReadOnly );
return put( request, &buffer, feedback );
}
QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::put( QNetworkRequest &request, QIODevice *data, QgsFeedback *feedback )
{
mPayloadData = data;
return doRequest( Put, request, true, feedback );
@ -177,6 +194,7 @@ QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::doRequest( QgsBl
// * or the owner thread of mReply is currently not doing anything because it's blocked in future.waitForFinished() (if it is the main thread)
connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
auto resumeMainThread = [&waitConditionMutex, &authRequestBufferNotEmpty ]()
{
@ -288,7 +306,10 @@ void QgsBlockingNetworkRequest::replyProgress( qint64 bytesReceived, qint64 byte
}
}
emit downloadProgress( bytesReceived, bytesTotal );
if ( mMethod == Put || mMethod == Post )
emit uploadProgress( bytesReceived, bytesTotal );
else
emit downloadProgress( bytesReceived, bytesTotal );
}
void QgsBlockingNetworkRequest::replyFinished()
@ -351,6 +372,7 @@ void QgsBlockingNetworkRequest::replyFinished()
connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
return;
}
}

View File

@ -103,6 +103,14 @@ class CORE_EXPORT QgsBlockingNetworkRequest : public QObject
* can be retrieved by calling errorMessage().
*
* \see get()
* \since 3.22
*/
ErrorCode post( QNetworkRequest &request, QIODevice *data, bool forceRefresh = false, QgsFeedback *feedback = nullptr );
/**
* This is an overloaded function.
*
* Performs a "post" operation on the specified \a request, using the given \a data.
*/
ErrorCode post( QNetworkRequest &request, const QByteArray &data, bool forceRefresh = false, QgsFeedback *feedback = nullptr );
@ -143,6 +151,14 @@ class CORE_EXPORT QgsBlockingNetworkRequest : public QObject
* If an error was encountered then a specific ErrorCode will be returned, and a detailed error message
* can be retrieved by calling errorMessage().
*
* \since 3.22
*/
ErrorCode put( QNetworkRequest &request, QIODevice *data, QgsFeedback *feedback = nullptr );
/**
* This is an overloaded function.
*
* Performs a "put" operation on the specified \a request, using the given \a data.
* \since 3.18
*/
ErrorCode put( QNetworkRequest &request, const QByteArray &data, QgsFeedback *feedback = nullptr );
@ -207,6 +223,12 @@ class CORE_EXPORT QgsBlockingNetworkRequest : public QObject
*/
void downloadFinished();
/**
* Emitted when when data are sent during a request.
* \since QGIS 3.22
*/
void uploadProgress( qint64, qint64 );
private slots:
void replyProgress( qint64, qint64 );
void replyFinished();
@ -227,7 +249,9 @@ class CORE_EXPORT QgsBlockingNetworkRequest : public QObject
QNetworkReply *mReply = nullptr;
Method mMethod = Get;
QByteArray mPayloadData;
//! payload data used in PUT/POST request
QIODevice *mPayloadData;
//! Authentication configuration ID
QString mAuthCfg;

View File

@ -71,6 +71,17 @@ void QgsNetworkContentFetcher::fetchContent( const QNetworkRequest &r, const QSt
mReply->setParent( nullptr ); // we don't want thread locale QgsNetworkAccessManagers to delete the reply - we want ownership of it to belong to this object
connect( mReply, &QNetworkReply::finished, this, [ = ] { contentLoaded(); } );
connect( mReply, &QNetworkReply::downloadProgress, this, &QgsNetworkContentFetcher::downloadProgress );
auto onError = [ = ]( QNetworkReply::NetworkError code )
{
emit errorOccurred( code, mReply->errorString() );
};
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
connect( mReply, qOverload<QNetworkReply::NetworkError>( &QNetworkReply::error ), this, onError );
#else
connect( mReply, &QNetworkReply::errorOccurred, this, onError );
#endif
}
QNetworkReply *QgsNetworkContentFetcher::reply()
@ -190,7 +201,3 @@ void QgsNetworkContentFetcher::contentLoaded( bool ok )
mReply->deleteLater();
fetchContent( redirect.toUrl(), mAuthCfg );
}

View File

@ -105,6 +105,13 @@ class CORE_EXPORT QgsNetworkContentFetcher : public QObject
*/
void downloadProgress( qint64 bytesReceived, qint64 bytesTotal );
/**
* Emitted when an error with \a code error occured while processing the request
* \a errorMsg is a textual description of the error
* \since QGIS 3.22
*/
void errorOccurred( QNetworkReply::NetworkError code, const QString &errorMsg );
private:
QString mAuthCfg;

View File

@ -20,6 +20,8 @@
#include "qgsapplication.h"
#include <QUrl>
#include <QFileInfo>
#include <QDir>
QgsNetworkContentFetcherRegistry::~QgsNetworkContentFetcherRegistry()
{
@ -31,7 +33,7 @@ QgsNetworkContentFetcherRegistry::~QgsNetworkContentFetcherRegistry()
mFileRegistry.clear();
}
const QgsFetchedContent *QgsNetworkContentFetcherRegistry::fetch( const QString &url, const Qgis::ActionStart fetchingMode )
QgsFetchedContent *QgsNetworkContentFetcherRegistry::fetch( const QString &url, const Qgis::ActionStart fetchingMode, const QString &authConfig )
{
if ( mFileRegistry.contains( url ) )
@ -39,7 +41,7 @@ const QgsFetchedContent *QgsNetworkContentFetcherRegistry::fetch( const QString
return mFileRegistry.value( url );
}
QgsFetchedContent *content = new QgsFetchedContent( url, nullptr, QgsFetchedContent::NotStarted );
QgsFetchedContent *content = new QgsFetchedContent( url, nullptr, QgsFetchedContent::NotStarted, authConfig );
mFileRegistry.insert( url, content );
@ -126,9 +128,11 @@ void QgsFetchedContent::download( bool redownload )
status() == QgsFetchedContent::NotStarted ||
status() == QgsFetchedContent::Failed )
{
mFetchingTask = new QgsNetworkContentFetcherTask( mUrl );
mFetchingTask = new QgsNetworkContentFetcherTask( mUrl, mAuthConfig );
// use taskCompleted which is main thread rather than fetched signal in worker thread
connect( mFetchingTask, &QgsNetworkContentFetcherTask::taskCompleted, this, &QgsFetchedContent::taskCompleted );
connect( mFetchingTask, &QgsNetworkContentFetcherTask::taskTerminated, this, &QgsFetchedContent::taskCompleted );
connect( mFetchingTask, &QgsNetworkContentFetcherTask::errorOccurred, this, &QgsFetchedContent::errorOccurred );
QgsApplication::instance()->taskManager()->addTask( mFetchingTask );
mStatus = QgsFetchedContent::Downloading;
}
@ -163,7 +167,12 @@ void QgsFetchedContent::taskCompleted()
QNetworkReply *reply = mFetchingTask->reply();
if ( reply->error() == QNetworkReply::NoError )
{
QTemporaryFile *tf = new QTemporaryFile( QStringLiteral( "XXXXXX" ) );
// keep extension, it can be usefull when guessing file content
// (when loading this file in a Qt WebView for instance)
const QString extension = QFileInfo( reply->request().url().fileName() ).completeSuffix();
QTemporaryFile *tf = new QTemporaryFile( extension.isEmpty() ? QString( "XXXXXX" ) :
QString( "%1/XXXXXX.%2" ).arg( QDir::tempPath(), extension ) );
mFile = tf;
tf->open();
mFile->write( reply->readAll() );

View File

@ -51,10 +51,12 @@ class CORE_EXPORT QgsFetchedContent : public QObject
};
//! Constructs a FetchedContent with pointer to the downloaded file and status of the download
explicit QgsFetchedContent( const QString &url, QTemporaryFile *file = nullptr, ContentStatus status = NotStarted )
explicit QgsFetchedContent( const QString &url, QTemporaryFile *file = nullptr, ContentStatus status = NotStarted,
const QString &authConfig = QString() )
: mUrl( url )
, mFile( file )
, mStatus( status )
, mAuthConfig( authConfig )
{}
~QgsFetchedContent() override
@ -79,6 +81,11 @@ class CORE_EXPORT QgsFetchedContent : public QObject
//! Returns the potential error of the download
QNetworkReply::NetworkError error() const {return mError;}
/**
* Returns the authentication configuration id use for this fetched content
*/
QString authConfig() const {return mAuthConfig;}
public slots:
/**
@ -96,6 +103,13 @@ class CORE_EXPORT QgsFetchedContent : public QObject
//! Emitted when the file is fetched and accessible
void fetched();
/**
* Emitted when an error with \a code error occured while processing the request
* \a errorMsg is a textual description of the error
* \since QGIS 3.22
*/
void errorOccurred( QNetworkReply::NetworkError code, const QString &errorMsg );
private slots:
void taskCompleted();
@ -106,6 +120,8 @@ class CORE_EXPORT QgsFetchedContent : public QObject
QgsNetworkContentFetcherTask *mFetchingTask = nullptr;
ContentStatus mStatus = NotStarted;
QNetworkReply::NetworkError mError = QNetworkReply::NoError;
QString mAuthConfig;
QString mErrorString;
};
/**
@ -134,9 +150,10 @@ class CORE_EXPORT QgsNetworkContentFetcherRegistry : public QObject
* \brief Initialize a download for the given URL
* \param url the URL to be fetched
* \param fetchingMode defines if the download will start immediately or shall be manually triggered
* \param authConfig authentication configuration id to be used while fetching
* \note If the download starts immediately, it will not redownload any already fetched or currently fetching file.
*/
const QgsFetchedContent *fetch( const QString &url, Qgis::ActionStart fetchingMode = Qgis::ActionStart::Deferred );
QgsFetchedContent *fetch( const QString &url, Qgis::ActionStart fetchingMode = Qgis::ActionStart::Deferred, const QString &authConfig = QString() );
#ifndef SIP_RUN

View File

@ -41,8 +41,13 @@ bool QgsNetworkContentFetcherTask::run()
{
mFetcher = new QgsNetworkContentFetcher();
QEventLoop loop;
// We need to set the event loop (and not 'this') as receiver for all signal to ensure execution
// in the same thread and in the same order of emission. Indeed 'this' and 'loop' lives in
// different thread because they have been created in different thread.
connect( mFetcher, &QgsNetworkContentFetcher::finished, &loop, &QEventLoop::quit );
connect( mFetcher, &QgsNetworkContentFetcher::downloadProgress, this, [ = ]( qint64 bytesReceived, qint64 bytesTotal )
connect( mFetcher, &QgsNetworkContentFetcher::downloadProgress, &loop, [ = ]( qint64 bytesReceived, qint64 bytesTotal )
{
if ( !isCanceled() && bytesTotal > 0 )
{
@ -53,12 +58,22 @@ bool QgsNetworkContentFetcherTask::run()
setProgress( progress );
}
} );
bool hasErrorOccurred = false;
connect( mFetcher, &QgsNetworkContentFetcher::errorOccurred, &loop, [ &hasErrorOccurred, this ]( QNetworkReply::NetworkError code, const QString & errorMsg )
{
hasErrorOccurred = true;
emit errorOccurred( code, errorMsg );
} );
mFetcher->fetchContent( mRequest, mAuthcfg );
loop.exec();
if ( !isCanceled() )
setProgress( 100 );
emit fetched();
return true;
return !isCanceled() && !hasErrorOccurred;
}
void QgsNetworkContentFetcherTask::cancel()

View File

@ -23,9 +23,9 @@
#include "qgstaskmanager.h"
#include "qgis_core.h"
#include <QNetworkRequest>
#include <QNetworkReply>
class QgsNetworkContentFetcher;
class QNetworkReply;
/**
* \class QgsNetworkContentFetcherTask
@ -103,12 +103,20 @@ class CORE_EXPORT QgsNetworkContentFetcherTask : public QgsTask
*/
void fetched();
/**
* Emitted when an error with \a code error occured while processing the request
* \a errorMsg is a textual description of the error
* \since QGIS 3.22
*/
void errorOccurred( QNetworkReply::NetworkError code, const QString &errorMsg );
private:
QNetworkRequest mRequest;
QString mAuthcfg;
QgsNetworkContentFetcher *mFetcher = nullptr;
QString mMode;
QIODevice *mContent = nullptr;
};
#endif //QGSNETWORKCONTENTFETCHERTASK_H

View File

@ -80,6 +80,7 @@ ADD_PYTHON_TEST(PyQgsExpressionBuilderWidget test_qgsexpressionbuilderwidget.py)
ADD_PYTHON_TEST(PyQgsExpressionLineEdit test_qgsexpressionlineedit.py)
ADD_PYTHON_TEST(PyQgsExtentGroupBox test_qgsextentgroupbox.py)
ADD_PYTHON_TEST(PyQgsExtentWidget test_qgsextentwidget.py)
ADD_PYTHON_TEST(PyQgsExternalStorageWebDAV test_qgsexternalstorage_webdav.py)
ADD_PYTHON_TEST(PyQgsFeature test_qgsfeature.py)
ADD_PYTHON_TEST(PyQgsFeatureSink test_qgsfeaturesink.py)
ADD_PYTHON_TEST(PyQgsFeatureSource test_qgsfeaturesource.py)

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for WebDAV external storage
External storage backend must implement a test based on TestPyQgsExternalStorageBase
.. note:: 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.
"""
__author__ = 'Julien Cabieces'
__date__ = '31/03/2021'
__copyright__ = 'Copyright 2021, The QGIS Project'
from shutil import rmtree
import os
import tempfile
import time
from utilities import unitTestDataPath, waitServer
from test_qgsexternalstorage_base import TestPyQgsExternalStorageBase
from qgis.PyQt.QtCore import QCoreApplication, QEventLoop, QUrl
from qgis.core import (
QgsApplication,
QgsAuthMethodConfig,
QgsExternalStorageFetchedContent)
from qgis.testing import (
start_app,
unittest,
)
class TestPyQgsExternalStorageWebDAV(TestPyQgsExternalStorageBase, unittest.TestCase):
storageType = "WebDAV"
badUrl = "http://nothinghere/"
@classmethod
def setUpClass(cls):
"""Run before all tests:"""
super().setUpClass()
cls.url = "http://{}:{}/webdav_tests".format(
os.environ.get('QGIS_WEBDAV_HOST', 'localhost'), os.environ.get('QGIS_WEBDAV_PORT', '80'))
if __name__ == '__main__':
unittest.main()