Add AWS S3 authentication and external storage

Added python tests as well
This commit is contained in:
Jacky Volpes 2022-12-13 19:22:05 +01:00
parent f4d2f27fbd
commit 7a157038d0
20 changed files with 1056 additions and 80 deletions

View File

@ -123,6 +123,9 @@ else
COMMAND=bash
fi
# Create an empty minio folder with appropriate permissions so www user can write inside it
mkdir -p /tmp/minio_tests/test_bucket && chmod -R 777 /tmp/minio_tests
# Create an empty webdav folder with appropriate permissions so www user can write inside it
mkdir -p /tmp/webdav_tests && chmod 777 /tmp/webdav_tests

View File

@ -18,6 +18,15 @@ services:
- ${QGIS_WORKSPACE}/.docker/webdav/passwords.list:/etc/nginx/.passwords.list
- /tmp/webdav_tests:/tmp/webdav_tests_root/webdav_tests
minio:
image: minio/minio
volumes:
- /tmp/minio_tests:/data
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=adminio€
command: server /data
qgis-deps:
tty: true
image: qgis3-build-deps-binary-image
@ -27,6 +36,7 @@ services:
links:
# - mssql
- webdav
- minio
- httpbin
env_file:
- docker-variables.env

View File

@ -208,6 +208,33 @@ EOT
fi
#######################################
# Wait for Minio container to be ready
#######################################
if [ $# -eq 0 ] || [ $1 = "ALL_BUT_PROVIDERS" ] || [ $1 = "ALL" ] ; then
echo "::group::Setup Minio"
echo "Wait for minio to be ready..."
COUNT=0
while ! curl http://$QGIS_MINIO_HOST:$QGIS_MINIO_PORT &> /dev/null;
do
printf "."
sleep 5
if [[ $(( COUNT++ )) -eq 40 ]]; then
break
fi
done
if [[ ${COUNT} -eq 41 ]]; then
echo "Error: Minio docker timeout!!!"
else
echo "done"
fi
echo "::endgroup::"
fi
#######################################
# Wait for WebDAV container to be ready
#######################################

View File

@ -21,5 +21,8 @@ PUSH_TO_CDASH=false
XDG_RUNTIME_DIR=/tmp
QGIS_MINIO_HOST=minio
QGIS_MINIO_PORT=9000
QGIS_WEBDAV_HOST=webdav
QGIS_WEBDAV_PORT=80

View File

@ -396,6 +396,7 @@ jobs:
echo "TEST_BATCH=$TEST_BATCH"
echo "DOCKERFILE=$DOCKERFILE"
mkdir -p /tmp/webdav_tests && chmod 777 /tmp/webdav_tests
mkdir -p /tmp/minio_tests/test_bucket && chmod -R 777 /tmp/minio_tests
docker-compose -f .docker/$DOCKERFILE run qgis-deps /root/QGIS/.docker/docker-qgis-test.sh $TEST_BATCH
- name: Archive test results report

View File

@ -19,6 +19,7 @@ add_subdirectory(pkipaths)
add_subdirectory(pkipkcs12)
add_subdirectory(apiheader)
add_subdirectory(maptiler_hmacsha256)
add_subdirectory(awss3)
if (WITH_OAUTH2_PLUGIN)
add_subdirectory(oauth2)

View File

@ -0,0 +1,77 @@
set(AUTH_AWSS3_SRCS
core/qgsauthawss3method.cpp
)
set(AUTH_AWSS3_HDRS
core/qgsauthawss3method.h
)
set(AUTH_AWSS3_UIS_H "")
if (WITH_GUI)
set(AUTH_AWSS3_SRCS ${AUTH_AWSS3_SRCS}
gui/qgsauthawss3edit.cpp
)
set(AUTH_AWSS3_HDRS ${AUTH_AWSS3_HDRS}
gui/qgsauthawss3edit.h
)
set(AUTH_AWSS3_UIS gui/qgsauthawss3edit.ui)
if (BUILD_WITH_QT6)
QT6_WRAP_UI(AUTH_AWSS3_UIS_H ${AUTH_AWSS3_UIS})
else()
QT5_WRAP_UI(AUTH_AWSS3_UIS_H ${AUTH_AWSS3_UIS})
endif()
endif()
# static library
add_library(authmethod_awss3_a STATIC ${AUTH_AWSS3_SRCS} ${AUTH_AWSS3_HDRS} ${AUTH_AWSS3_UIS_H})
target_include_directories(authmethod_awss3_a PUBLIC ${CMAKE_SOURCE_DIR}/src/auth/awss3/core)
# require c++17
target_compile_features(authmethod_awss3_a PRIVATE cxx_std_17)
target_link_libraries(authmethod_awss3_a qgis_core)
if (WITH_GUI)
target_include_directories(authmethod_awss3_a PRIVATE
${CMAKE_SOURCE_DIR}/src/auth/awss3/gui
${CMAKE_BINARY_DIR}/src/auth/awss3
)
target_link_libraries (authmethod_awss3_a qgis_gui)
endif()
target_compile_definitions(authmethod_awss3_a PRIVATE "-DQT_NO_FOREACH")
if (FORCE_STATIC_LIBS)
# for (external) mobile apps to be able to pick up provider for linking
install (TARGETS authmethod_awss3_a ARCHIVE DESTINATION ${QGIS_PLUGIN_DIR})
else()
# dynamically loaded module
add_library(authmethod_awss3 MODULE ${AUTH_AWSS3_SRCS} ${AUTH_AWSS3_HDRS} ${AUTH_AWSS3_UIS_H})
# require c++17
target_compile_features(authmethod_awss3 PRIVATE cxx_std_17)
target_link_libraries(authmethod_awss3 qgis_core)
if (WITH_GUI)
target_include_directories(authmethod_awss3 PRIVATE
${CMAKE_SOURCE_DIR}/src/auth/awss3/gui
${CMAKE_BINARY_DIR}/src/auth/awss3
)
target_link_libraries (authmethod_awss3 qgis_gui)
add_dependencies(authmethod_awss3 ui)
endif()
target_compile_definitions(authmethod_awss3 PRIVATE "-DQT_NO_FOREACH")
install (TARGETS authmethod_awss3
RUNTIME DESTINATION ${QGIS_PLUGIN_DIR}
LIBRARY DESTINATION ${QGIS_PLUGIN_DIR}
)
endif()

View File

@ -0,0 +1,208 @@
/***************************************************************************
qgsauthawss3method.cpp
--------------------------------------
Date : December 2022
Copyright : (C) 2022 by Jacky Volpes
Email : jacky dot volpes 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 "qgsauthawss3method.h"
#include <QUrlQuery>
#include <QDateTime>
#include <QCryptographicHash>
#include <QMessageAuthenticationCode>
#include "qgsauthmanager.h"
#include "qgslogger.h"
#include "qgsapplication.h"
#ifdef HAVE_GUI
#include "qgsauthawss3edit.h"
#endif
const QString QgsAuthAwsS3Method::AUTH_METHOD_KEY = QStringLiteral( "AWSS3" );
const QString QgsAuthAwsS3Method::AUTH_METHOD_DESCRIPTION = QStringLiteral( "AWS S3" );
const QString QgsAuthAwsS3Method::AUTH_METHOD_DISPLAY_DESCRIPTION = tr( "AWS S3" );
QMap<QString, QgsAuthMethodConfig> QgsAuthAwsS3Method::sAuthConfigCache = QMap<QString, QgsAuthMethodConfig>();
QgsAuthAwsS3Method::QgsAuthAwsS3Method()
{
setVersion( 4 );
setExpansions( QgsAuthMethod::NetworkRequest );
setDataProviders( QStringList() << QStringLiteral( "awss3" ) );
}
QString QgsAuthAwsS3Method::key() const
{
return AUTH_METHOD_KEY;
}
QString QgsAuthAwsS3Method::description() const
{
return AUTH_METHOD_DESCRIPTION;
}
QString QgsAuthAwsS3Method::displayDescription() const
{
return AUTH_METHOD_DISPLAY_DESCRIPTION;
}
bool QgsAuthAwsS3Method::updateNetworkRequest( QNetworkRequest &request, const QString &authcfg,
const QString &dataprovider )
{
Q_UNUSED( dataprovider )
const QgsAuthMethodConfig config = getMethodConfig( authcfg );
if ( !config.isValid() )
{
QgsDebugMsg( QStringLiteral( "Update request config FAILED for authcfg: %1: config invalid" ).arg( authcfg ) );
return false;
}
const QByteArray username = config.config( QStringLiteral( "username" ) ).toLocal8Bit();
const QByteArray password = config.config( QStringLiteral( "password" ) ).toLocal8Bit();
const QByteArray region = config.config( QStringLiteral( "region" ) ).toLocal8Bit();
const QByteArray headerList = "host;x-amz-content-sha256;x-amz-date";
const QByteArray encryptionMethod = "AWS4-HMAC-SHA256";
const QDateTime currentDateTime = QDateTime::currentDateTime().toUTC();
const QByteArray date = currentDateTime.toString( "yyyyMMdd" ).toLocal8Bit();
const QByteArray dateTime = currentDateTime.toString( "yyyyMMddThhmmssZ" ).toLocal8Bit();
QByteArray canonicalPath = QUrl::toPercentEncoding( request.url().path(), "/" ); // Don't encode slash
if ( canonicalPath.isEmpty() )
{
canonicalPath = "/";
}
QByteArray method;
QByteArray payloadHash;
if ( request.hasRawHeader( "X-Amz-Content-SHA256" ) )
{
method = "PUT";
payloadHash = request.rawHeader( "X-Amz-Content-SHA256" );
}
else
{
method = "GET";
payloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; // Sha256 of empty payload
request.setRawHeader( QByteArray( "X-Amz-Content-SHA256" ), payloadHash );
}
const QByteArray canonicalRequest = method + '\n' +
canonicalPath + '\n' +
'\n' +
"host:" + request.url().host().toLocal8Bit() + '\n' +
"x-amz-content-sha256:" + payloadHash + '\n' +
"x-amz-date:" + dateTime + '\n' +
'\n' +
headerList + '\n' +
payloadHash;
const QByteArray canonicalRequestHash = QCryptographicHash::hash( canonicalRequest, QCryptographicHash::Sha256 ).toHex();
const QByteArray stringToSign = encryptionMethod + '\n' +
dateTime + '\n' +
date + "/" + region + "/s3/aws4_request" + '\n' +
canonicalRequestHash;
const QByteArray signingKey = QMessageAuthenticationCode::hash( "aws4_request",
QMessageAuthenticationCode::hash( "s3",
QMessageAuthenticationCode::hash( region,
QMessageAuthenticationCode::hash( date, "AWS4" + password,
QCryptographicHash::Sha256 ),
QCryptographicHash::Sha256 ),
QCryptographicHash::Sha256 ),
QCryptographicHash::Sha256 );
const QByteArray signature = QMessageAuthenticationCode::hash( stringToSign, signingKey, QCryptographicHash::Sha256 ).toHex();
request.setRawHeader( QByteArray( "Host" ), request.url().host().toLocal8Bit() );
request.setRawHeader( QByteArray( "X-Amz-Date" ), dateTime );
request.setRawHeader( QByteArray( "Authorization" ),
encryptionMethod + "Credential=" + username + '/' + date + "/" + region + "/s3/aws4_request, SignedHeaders=" + headerList + ", Signature=" + signature );
return true;
}
void QgsAuthAwsS3Method::clearCachedConfig( const QString &authcfg )
{
removeMethodConfig( authcfg );
}
void QgsAuthAwsS3Method::updateMethodConfig( QgsAuthMethodConfig &mconfig )
{
Q_UNUSED( mconfig );
// NOTE: add updates as method version() increases due to config storage changes
}
QgsAuthMethodConfig QgsAuthAwsS3Method::getMethodConfig( const QString &authcfg, bool fullconfig )
{
const QMutexLocker locker( &mMutex );
QgsAuthMethodConfig config;
// check if it is cached
if ( sAuthConfigCache.contains( authcfg ) )
{
config = sAuthConfigCache.value( authcfg );
QgsDebugMsgLevel( QStringLiteral( "Retrieved config for authcfg: %1" ).arg( authcfg ), 2 );
return config;
}
// else build bundle
if ( !QgsApplication::authManager()->loadAuthenticationConfig( authcfg, config, fullconfig ) )
{
QgsDebugMsgLevel( QStringLiteral( "Retrieved config for authcfg: %1" ).arg( authcfg ), 2 );
return QgsAuthMethodConfig();
}
// cache bundle
putMethodConfig( authcfg, config );
return config;
}
void QgsAuthAwsS3Method::putMethodConfig( const QString &authcfg, const QgsAuthMethodConfig &mconfig )
{
const QMutexLocker locker( &mMutex );
QgsDebugMsgLevel( QStringLiteral( "Putting AWS S3 config for authcfg: %1" ).arg( authcfg ), 2 );
sAuthConfigCache.insert( authcfg, mconfig );
}
void QgsAuthAwsS3Method::removeMethodConfig( const QString &authcfg )
{
const QMutexLocker locker( &mMutex );
if ( sAuthConfigCache.contains( authcfg ) )
{
sAuthConfigCache.remove( authcfg );
QgsDebugMsgLevel( QStringLiteral( "Removed Aws S3 config for authcfg: %1" ).arg( authcfg ), 2 );
}
}
#ifdef HAVE_GUI
QWidget *QgsAuthAwsS3Method::editWidget( QWidget *parent ) const
{
return new QgsAuthAwsS3Edit( parent );
}
#endif
//////////////////////////////////////////////
// Plugin externals
//////////////////////////////////////////////
#ifndef HAVE_STATIC_PROVIDERS
QGISEXTERN QgsAuthMethodMetadata *authMethodMetadataFactory()
{
return new QgsAuthAwsS3MethodMetadata();
}
#endif

View File

@ -0,0 +1,77 @@
/***************************************************************************
qgsauthawss3method.h
--------------------------------------
Date : December 2022
Copyright : (C) 2022 by Jacky Volpes
Email : jacky dot volpes 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 QGSAUTHAWSS3METHOD_H
#define QGSAUTHAWSS3METHOD_H
#include <QObject>
#include <QMutex>
#include "qgsauthconfig.h"
#include "qgsauthmethod.h"
#include "qgsauthmethodmetadata.h"
class QgsAuthAwsS3Method : public QgsAuthMethod
{
Q_OBJECT
public:
static const QString AUTH_METHOD_KEY;
static const QString AUTH_METHOD_DESCRIPTION;
static const QString AUTH_METHOD_DISPLAY_DESCRIPTION;
explicit QgsAuthAwsS3Method();
// QgsAuthMethod interface
QString key() const override;
QString description() const override;
QString displayDescription() const override;
bool updateNetworkRequest( QNetworkRequest &request, const QString &authcfg,
const QString &dataprovider = QString() ) override;
void clearCachedConfig( const QString &authcfg ) override;
void updateMethodConfig( QgsAuthMethodConfig &mconfig ) override;
#ifdef HAVE_GUI
QWidget *editWidget( QWidget *parent )const override;
#endif
private:
QgsAuthMethodConfig getMethodConfig( const QString &authcfg, bool fullconfig = true );
void putMethodConfig( const QString &authcfg, const QgsAuthMethodConfig &mconfig );
void removeMethodConfig( const QString &authcfg );
static QMap<QString, QgsAuthMethodConfig> sAuthConfigCache;
};
class QgsAuthAwsS3MethodMetadata : public QgsAuthMethodMetadata
{
public:
QgsAuthAwsS3MethodMetadata()
: QgsAuthMethodMetadata( QgsAuthAwsS3Method::AUTH_METHOD_KEY, QgsAuthAwsS3Method::AUTH_METHOD_DESCRIPTION )
{}
QgsAuthAwsS3Method *createAuthMethod() const override {return new QgsAuthAwsS3Method;}
};
#endif // QGSAUTHAWSS3METHOD_H

View File

@ -0,0 +1,97 @@
/***************************************************************************
qgsauthawss3edit.cpp
--------------------------------------
Date : December 2022
Copyright : (C) 2022 by Jacky Volpes
Email : jacky dot volpes 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 "qgsauthawss3edit.h"
#include "ui_qgsauthawss3edit.h"
QgsAuthAwsS3Edit::QgsAuthAwsS3Edit( QWidget *parent )
: QgsAuthMethodEdit( parent )
{
setupUi( this );
connect( leUsername, &QLineEdit::textChanged, this, &QgsAuthAwsS3Edit::leUsername_textChanged );
connect( lePassword, &QLineEdit::textChanged, this, &QgsAuthAwsS3Edit::lePassword_textChanged );
connect( leRegion, &QLineEdit::textChanged, this, &QgsAuthAwsS3Edit::leRegion_textChanged );
connect( chkPasswordShow, &QCheckBox::stateChanged, this, &QgsAuthAwsS3Edit::chkPasswordShow_stateChanged );
}
bool QgsAuthAwsS3Edit::validateConfig()
{
const bool curvalid = !leUsername->text().isEmpty() && !lePassword->text().isEmpty() && !leRegion->text().isEmpty();
if ( mValid != curvalid )
{
mValid = curvalid;
emit validityChanged( curvalid );
}
return curvalid;
}
QgsStringMap QgsAuthAwsS3Edit::configMap() const
{
QgsStringMap config;
config.insert( QStringLiteral( "username" ), leUsername->text() );
config.insert( QStringLiteral( "password" ), lePassword->text() );
config.insert( QStringLiteral( "region" ), leRegion->text() );
return config;
}
void QgsAuthAwsS3Edit::loadConfig( const QgsStringMap &configmap )
{
clearConfig();
mConfigMap = configmap;
leUsername->setText( configmap.value( QStringLiteral( "username" ) ) );
lePassword->setText( configmap.value( QStringLiteral( "password" ) ) );
leRegion->setText( configmap.value( QStringLiteral( "region" ) ) );
validateConfig();
}
void QgsAuthAwsS3Edit::resetConfig()
{
loadConfig( mConfigMap );
}
void QgsAuthAwsS3Edit::clearConfig()
{
leUsername->clear();
lePassword->clear();
leRegion->clear();
chkPasswordShow->setChecked( false );
}
void QgsAuthAwsS3Edit::leUsername_textChanged( const QString &txt )
{
Q_UNUSED( txt )
validateConfig();
}
void QgsAuthAwsS3Edit::lePassword_textChanged( const QString &txt )
{
Q_UNUSED( txt )
validateConfig();
}
void QgsAuthAwsS3Edit::leRegion_textChanged( const QString &txt )
{
Q_UNUSED( txt )
validateConfig();
}
void QgsAuthAwsS3Edit::chkPasswordShow_stateChanged( int state )
{
lePassword->setEchoMode( ( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
}

View File

@ -0,0 +1,59 @@
/***************************************************************************
qgsauthawss3edit.h
--------------------------------------
Date : December 2022
Copyright : (C) 2022 by Jacky Volpes
Email : jacky dot volpes 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 QGSAUTHAWSS3EDIT_H
#define QGSAUTHAWSS3EDIT_H
#include <QWidget>
#include "qgsauthmethodedit.h"
#include "ui_qgsauthawss3edit.h"
#include "qgsauthconfig.h"
class QgsAuthAwsS3Edit : public QgsAuthMethodEdit, private Ui::QgsAuthAwsS3Edit
{
Q_OBJECT
public:
explicit QgsAuthAwsS3Edit( QWidget *parent = nullptr );
bool validateConfig() override;
QgsStringMap configMap() const override;
public slots:
void loadConfig( const QgsStringMap &configmap ) override;
void resetConfig() override;
void clearConfig() override;
private slots:
void leUsername_textChanged( const QString &txt );
void lePassword_textChanged( const QString &txt );
void leRegion_textChanged( const QString &txt );
void chkPasswordShow_stateChanged( int state );
private:
QgsStringMap mConfigMap;
bool mValid = false;
};
#endif // QGSAUTHAWSS3EDIT_H

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QgsAuthAwsS3Edit</class>
<widget class="QWidget" name="QgsAuthAwsS3Edit">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="2" column="1">
<widget class="QLineEdit" name="leRegion">
<property name="placeholderText">
<string>Required</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="leUsername">
<property name="toolTip">
<string>AWS AccessKeyId</string>
</property>
<property name="placeholderText">
<string>Required</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lblRegion">
<property name="text">
<string>Region</string>
</property>
</widget>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>173</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="QLineEdit" name="lePassword">
<property name="toolTip">
<string>AWS SecretAccessKey</string>
</property>
<property name="text">
<string/>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Required</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkPasswordShow">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Show</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="lblUsername">
<property name="toolTip">
<string>AWS AccessKeyId</string>
</property>
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblPassword">
<property name="toolTip">
<string>AWS SecretAccessKey</string>
</property>
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>leUsername</tabstop>
<tabstop>lePassword</tabstop>
<tabstop>leRegion</tabstop>
<tabstop>chkPasswordShow</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -154,7 +154,7 @@ set(QGIS_CORE_SRCS
externalstorage/qgsexternalstorage.cpp
externalstorage/qgsexternalstorageregistry.cpp
externalstorage/qgssimplecopyexternalstorage.cpp
externalstorage/qgswebdavexternalstorage.cpp
externalstorage/qgshttpexternalstorage.cpp
layertree/qgscolorramplegendnode.cpp
layertree/qgscolorramplegendnodesettings.cpp
@ -1925,7 +1925,7 @@ set(QGIS_CORE_PRIVATE_HDRS
expression/qgsexpression_p.h
externalstorage/qgssimplecopyexternalstorage_p.h
externalstorage/qgswebdavexternalstorage_p.h
externalstorage/qgshttpexternalstorage_p.h
proj/qgscoordinatereferencesystem_p.h
proj/qgscoordinatetransformcontext_p.h

View File

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

View File

@ -13,7 +13,7 @@
* *
***************************************************************************/
#include "qgswebdavexternalstorage_p.h"
#include "qgshttpexternalstorage_p.h"
#include "qgsnetworkcontentfetcherregistry.h"
#include "qgsblockingnetworkrequest.h"
@ -24,10 +24,11 @@
#include <QFile>
#include <QPointer>
#include <QFileInfo>
#include <QCryptographicHash>
///@cond PRIVATE
QgsWebDAVExternalStorageStoreTask::QgsWebDAVExternalStorageStoreTask( const QUrl &url, const QString &filePath, const QString &authCfg )
QgsHttpExternalStorageStoreTask::QgsHttpExternalStorageStoreTask( const QUrl &url, const QString &filePath, const QString &authCfg )
: QgsTask( tr( "Storing %1" ).arg( QFileInfo( filePath ).baseName() ) )
, mUrl( url )
, mFilePath( filePath )
@ -36,17 +37,20 @@ QgsWebDAVExternalStorageStoreTask::QgsWebDAVExternalStorageStoreTask( const QUrl
{
}
bool QgsWebDAVExternalStorageStoreTask::run()
bool QgsHttpExternalStorageStoreTask::run()
{
QgsBlockingNetworkRequest request;
request.setAuthCfg( mAuthCfg );
QNetworkRequest req( mUrl );
QgsSetRequestInitiatorClass( req, QStringLiteral( "QgsWebDAVExternalStorageStoreTask" ) );
QgsSetRequestInitiatorClass( req, QStringLiteral( "QgsHttpExternalStorageStoreTask" ) );
QFile *f = new QFile( mFilePath );
f->open( QIODevice::ReadOnly );
if ( mPrepareRequestHandler )
mPrepareRequestHandler( req, f );
connect( &request, &QgsBlockingNetworkRequest::uploadProgress, this, [ = ]( qint64 bytesReceived, qint64 bytesTotal )
{
if ( !isCanceled() && bytesTotal > 0 )
@ -66,24 +70,29 @@ bool QgsWebDAVExternalStorageStoreTask::run()
return !isCanceled() && err == QgsBlockingNetworkRequest::NoError;
}
void QgsWebDAVExternalStorageStoreTask::cancel()
void QgsHttpExternalStorageStoreTask::cancel()
{
mFeedback->cancel();
QgsTask::cancel();
}
QString QgsWebDAVExternalStorageStoreTask::errorString() const
QString QgsHttpExternalStorageStoreTask::errorString() const
{
return mErrorString;
}
QgsWebDAVExternalStorageStoredContent::QgsWebDAVExternalStorageStoredContent( const QString &filePath, const QString &url, const QString &authcfg )
void QgsHttpExternalStorageStoreTask::setPrepareRequestHandler( std::function< void( QNetworkRequest &request, QFile *f ) > handler )
{
mPrepareRequestHandler = handler;
}
QgsHttpExternalStorageStoredContent::QgsHttpExternalStorageStoredContent( 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 );
mUploadTask = new QgsHttpExternalStorageStoreTask( storageUrl, filePath, authcfg );
connect( mUploadTask, &QgsTask::taskCompleted, this, [ = ]
{
@ -103,14 +112,14 @@ QgsWebDAVExternalStorageStoredContent::QgsWebDAVExternalStorageStoredContent( co
} );
}
void QgsWebDAVExternalStorageStoredContent::store()
void QgsHttpExternalStorageStoredContent::store()
{
mStatus = Qgis::ContentStatus::Running;
QgsApplication::taskManager()->addTask( mUploadTask );
}
void QgsWebDAVExternalStorageStoredContent::cancel()
void QgsHttpExternalStorageStoredContent::cancel()
{
if ( !mUploadTask )
return;
@ -125,16 +134,21 @@ void QgsWebDAVExternalStorageStoredContent::cancel()
mUploadTask->cancel();
}
QString QgsWebDAVExternalStorageStoredContent::url() const
QString QgsHttpExternalStorageStoredContent::url() const
{
return mUrl;
}
void QgsHttpExternalStorageStoredContent::setPrepareRequestHandler( std::function< void( QNetworkRequest &request, QFile *f ) > handler )
{
mUploadTask->setPrepareRequestHandler( handler );
}
QgsWebDAVExternalStorageFetchedContent::QgsWebDAVExternalStorageFetchedContent( QgsFetchedContent *fetchedContent )
QgsHttpExternalStorageFetchedContent::QgsHttpExternalStorageFetchedContent( QgsFetchedContent *fetchedContent )
: mFetchedContent( fetchedContent )
{
connect( mFetchedContent, &QgsFetchedContent::fetched, this, &QgsWebDAVExternalStorageFetchedContent::onFetched );
connect( mFetchedContent, &QgsFetchedContent::fetched, this, &QgsHttpExternalStorageFetchedContent::onFetched );
connect( mFetchedContent, &QgsFetchedContent::errorOccurred, this, [ = ]( QNetworkReply::NetworkError code, const QString & errorMsg )
{
Q_UNUSED( code );
@ -142,7 +156,7 @@ QgsWebDAVExternalStorageFetchedContent::QgsWebDAVExternalStorageFetchedContent(
} );
}
void QgsWebDAVExternalStorageFetchedContent::fetch()
void QgsHttpExternalStorageFetchedContent::fetch()
{
if ( !mFetchedContent )
return;
@ -158,12 +172,12 @@ void QgsWebDAVExternalStorageFetchedContent::fetch()
}
}
QString QgsWebDAVExternalStorageFetchedContent::filePath() const
QString QgsHttpExternalStorageFetchedContent::filePath() const
{
return mFetchedContent ? mFetchedContent->filePath() : QString();
}
void QgsWebDAVExternalStorageFetchedContent::onFetched()
void QgsHttpExternalStorageFetchedContent::onFetched()
{
if ( !mFetchedContent )
return;
@ -175,31 +189,68 @@ void QgsWebDAVExternalStorageFetchedContent::onFetched()
}
}
void QgsWebDAVExternalStorageFetchedContent::cancel()
void QgsHttpExternalStorageFetchedContent::cancel()
{
mFetchedContent->cancel();
}
QString QgsWebDAVExternalStorage::type() const
// WEB DAV PROTOCOL
QString QgsWebDavExternalStorage::type() const
{
return QStringLiteral( "WebDAV" );
};
QString QgsWebDAVExternalStorage::displayName() const
QString QgsWebDavExternalStorage::displayName() const
{
return QObject::tr( "WebDAV Storage" );
};
QgsExternalStorageStoredContent *QgsWebDAVExternalStorage::doStore( const QString &filePath, const QString &url, const QString &authcfg ) const
QgsExternalStorageStoredContent *QgsWebDavExternalStorage::doStore( const QString &filePath, const QString &url, const QString &authcfg ) const
{
return new QgsWebDAVExternalStorageStoredContent( filePath, url, authcfg );
return new QgsHttpExternalStorageStoredContent( filePath, url, authcfg );
};
QgsExternalStorageFetchedContent *QgsWebDAVExternalStorage::doFetch( const QString &url, const QString &authConfig ) const
QgsExternalStorageFetchedContent *QgsWebDavExternalStorage::doFetch( const QString &url, const QString &authConfig ) const
{
QgsFetchedContent *fetchedContent = QgsApplication::networkContentFetcherRegistry()->fetch( url, Qgis::ActionStart::Deferred, authConfig );
return new QgsWebDAVExternalStorageFetchedContent( fetchedContent );
return new QgsHttpExternalStorageFetchedContent( fetchedContent );
}
// AWS S3 PROTOCOL
QString QgsAwsS3ExternalStorage::type() const
{
return QStringLiteral( "AWSS3" );
};
QString QgsAwsS3ExternalStorage::displayName() const
{
return QObject::tr( "AWS S3" );
};
QgsExternalStorageStoredContent *QgsAwsS3ExternalStorage::doStore( const QString &filePath, const QString &url, const QString &authcfg ) const
{
std::unique_ptr<QgsHttpExternalStorageStoredContent> storedContent = std::make_unique<QgsHttpExternalStorageStoredContent>( filePath, url, authcfg );
storedContent->setPrepareRequestHandler( []( QNetworkRequest & request, QFile * f )
{
QCryptographicHash payloadCrypto( QCryptographicHash::Sha256 );
payloadCrypto.addData( f );
QByteArray payloadHash = payloadCrypto.result().toHex();
f->seek( 0 );
request.setRawHeader( QByteArray( "X-Amz-Content-SHA256" ), payloadHash );
} );
return storedContent.release();
};
QgsExternalStorageFetchedContent *QgsAwsS3ExternalStorage::doFetch( const QString &url, const QString &authConfig ) const
{
QgsFetchedContent *fetchedContent = QgsApplication::networkContentFetcherRegistry()->fetch( url, Qgis::ActionStart::Deferred, authConfig );
return new QgsHttpExternalStorageFetchedContent( fetchedContent );
}
///@endcond

View File

@ -26,7 +26,7 @@
#include <QUrl>
class QgsFeedback;
class QgsWebDAVExternalStorageStoreTask;
class QgsHttpExternalStorageStoreTask;
class QgsFetchedContent;
///@cond PRIVATE
@ -38,7 +38,7 @@ class QgsFetchedContent;
*
* \since QGIS 3.22
*/
class CORE_EXPORT QgsWebDAVExternalStorage : public QgsExternalStorage
class CORE_EXPORT QgsWebDavExternalStorage : public QgsExternalStorage
{
public:
@ -55,17 +55,38 @@ class CORE_EXPORT QgsWebDAVExternalStorage : public QgsExternalStorage
/**
* \ingroup core
* \brief Class for WebDAV stored content
* \brief External storage implementation using the protocol AWS S3.
*
* \since QGIS 3.28
*/
class CORE_EXPORT QgsAwsS3ExternalStorage : 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 HTTP stored content
*
* \since QGIS 3.22
*/
class QgsWebDAVExternalStorageStoredContent : public QgsExternalStorageStoredContent
class QgsHttpExternalStorageStoredContent : public QgsExternalStorageStoredContent
{
Q_OBJECT
public:
QgsWebDAVExternalStorageStoredContent( const QString &filePath, const QString &url, const QString &authcfg = QString() );
QgsHttpExternalStorageStoredContent( const QString &filePath, const QString &url, const QString &authcfg = QString() );
void cancel() override;
@ -73,25 +94,28 @@ class QgsWebDAVExternalStorageStoredContent : public QgsExternalStorageStoredCo
void store() override;
void setPrepareRequestHandler( std::function< void( QNetworkRequest &request, QFile *f ) > );
private:
QPointer<QgsWebDAVExternalStorageStoreTask> mUploadTask;
std::function< void( QNetworkRequest &request, QFile *f ) > mPrepareRequestHandler = nullptr;
QPointer<QgsHttpExternalStorageStoreTask> mUploadTask;
QString mUrl;
};
/**
* \ingroup core
* \brief Class for WebDAV fetched content
* \brief Class for HTTP fetched content
*
* \since QGIS 3.22
*/
class QgsWebDAVExternalStorageFetchedContent : public QgsExternalStorageFetchedContent
class QgsHttpExternalStorageFetchedContent : public QgsExternalStorageFetchedContent
{
Q_OBJECT
public:
QgsWebDAVExternalStorageFetchedContent( QgsFetchedContent *fetchedContent );
QgsHttpExternalStorageFetchedContent( QgsFetchedContent *fetchedContent );
QString filePath() const override;
@ -111,17 +135,17 @@ class QgsWebDAVExternalStorageFetchedContent : public QgsExternalStorageFetchedC
/**
* \ingroup core
* \brief Task to store a file to a given WebDAV url
* \brief Task to store a file to a given url
*
* \since QGIS 3.22
*/
class QgsWebDAVExternalStorageStoreTask : public QgsTask
class QgsHttpExternalStorageStoreTask : public QgsTask
{
Q_OBJECT
public:
QgsWebDAVExternalStorageStoreTask( const QUrl &url, const QString &filePath, const QString &authCfg );
QgsHttpExternalStorageStoreTask( const QUrl &url, const QString &filePath, const QString &authCfg );
bool run() override;
@ -129,8 +153,11 @@ class QgsWebDAVExternalStorageStoreTask : public QgsTask
QString errorString() const;
void setPrepareRequestHandler( std::function< void( QNetworkRequest &request, QFile *f ) > );
private:
std::function< void( QNetworkRequest &request, QFile *f ) > mPrepareRequestHandler = nullptr;
const QUrl mUrl;
const QString mFilePath;
const QString mAuthCfg;

View File

@ -89,7 +89,8 @@ 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(PyQgsExternalStorageWebDav test_qgsexternalstorage_webdav.py)
ADD_PYTHON_TEST(PyQgsExternalStorageAwsS3 test_qgsexternalstorage_awss3.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,67 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for AWS S3 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__ = "Jacky Volpes"
__date__ = "20/12/2022"
__copyright__ = "Copyright 2022, 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 TestPyQgsExternalStorageAwsS3(TestPyQgsExternalStorageBase, unittest.TestCase):
storageType = "AWSS3"
badUrl = "http://nothinghere/"
@classmethod
def setUpClass(cls):
"""Run before all tests:"""
super().setUpClass()
bucket_name = "test_bucket"
cls.auth_config = QgsAuthMethodConfig("AWSS3")
cls.auth_config.setConfig("username", "minioadmin")
cls.auth_config.setConfig("password", "adminio€")
cls.auth_config.setConfig("region", "us-east-1")
cls.auth_config.setName("test_awss3_auth_config")
assert cls.authm.storeAuthenticationConfig(cls.auth_config)[0]
assert cls.auth_config.isValid()
cls.url = "http://{}:{}/{}".format(
os.environ.get("QGIS_MINIO_HOST", "localhost"),
os.environ.get("QGIS_MINIO_PORT", "80"),
bucket_name,
)
if __name__ == "__main__":
unittest.main()

View File

@ -9,9 +9,9 @@ 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'
__author__ = "Julien Cabieces"
__date__ = "31/03/2021"
__copyright__ = "Copyright 2021, The QGIS Project"
from shutil import rmtree
import os
@ -27,7 +27,8 @@ from qgis.core import (
Qgis,
QgsApplication,
QgsAuthMethodConfig,
QgsExternalStorageFetchedContent)
QgsExternalStorageFetchedContent,
)
from qgis.testing import (
start_app,
@ -35,7 +36,7 @@ from qgis.testing import (
)
class TestPyQgsExternalStorageBase():
class TestPyQgsExternalStorageBase:
storageType = None
url = None
@ -50,13 +51,13 @@ class TestPyQgsExternalStorageBase():
start_app()
cls.authm = QgsApplication.authManager()
assert (cls.authm.setMasterPassword('masterpassword', True))
assert cls.authm.setMasterPassword("masterpassword", True)
assert not cls.authm.isDisabled(), cls.authm.disabledMessage()
cls.auth_config = QgsAuthMethodConfig("Basic")
cls.auth_config.setConfig('username', "qgis")
cls.auth_config.setConfig('password', "myPasswd!")
cls.auth_config.setName('test_basic_auth_config')
cls.auth_config.setConfig("username", "qgis")
cls.auth_config.setConfig("password", "myPasswd!")
cls.auth_config.setName("test_basic_auth_config")
assert cls.authm.storeAuthenticationConfig(cls.auth_config)[0]
assert cls.auth_config.isValid()
@ -80,16 +81,21 @@ class TestPyQgsExternalStorageBase():
"""Run after each test."""
pass
def getNewFile(self, content):
"""Return a newly created temporary file with content"""
f = tempfile.NamedTemporaryFile(suffix='.txt')
def getNewFile(self, content, with_special_characters=False):
"""Return a newly created temporary file with content
if with_special_characters is True then add url reserved characters in the file name"""
f = tempfile.NamedTemporaryFile(
suffix=".txt",
prefix="é~u!:;=\"',iù[ &²*k (~$£<" if with_special_characters else "",
)
f.write(content)
f.flush()
return f
def checkContent(self, file_path, content):
"""Check that file content matches given content"""
f = open(file_path, 'r')
f = open(file_path, "r")
self.assertTrue(f.read(), b"New content")
f.close()
@ -97,8 +103,10 @@ class TestPyQgsExternalStorageBase():
"""
Check that storage list in in correct order
"""
self.assertEqual([storage.type() for storage in self.registry.externalStorages()],
["SimpleCopy", "WebDAV"])
self.assertEqual(
[storage.type() for storage in self.registry.externalStorages()],
["SimpleCopy", "WebDAV", "AWSS3"],
)
def testStoreFetchFileLater(self):
"""
@ -130,7 +138,9 @@ class TestPyQgsExternalStorageBase():
self.assertEqual(spyProgressChanged[-1][0], 100)
# fetch
fetchedContent = self.storage.fetch(self.url + "/" + os.path.basename(f.name), self.auth_config.id())
fetchedContent = self.storage.fetch(
self.url + "/" + os.path.basename(f.name), self.auth_config.id()
)
self.assertTrue(fetchedContent)
self.assertEqual(fetchedContent.status(), Qgis.ContentStatus.NotStarted)
@ -147,10 +157,12 @@ class TestPyQgsExternalStorageBase():
self.assertFalse(fetchedContent.errorString())
self.assertTrue(fetchedContent.filePath())
self.checkContent(fetchedContent.filePath(), b"New content")
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], '.txt')
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], ".txt")
# fetch again, should be cached
fetchedContent = self.storage.fetch(self.url + "/" + os.path.basename(f.name), self.auth_config.id())
fetchedContent = self.storage.fetch(
self.url + "/" + os.path.basename(f.name), self.auth_config.id()
)
self.assertTrue(fetchedContent)
self.assertEqual(fetchedContent.status(), Qgis.ContentStatus.NotStarted)
@ -167,7 +179,7 @@ class TestPyQgsExternalStorageBase():
self.assertTrue(not fetchedContent.errorString())
self.assertTrue(fetchedContent.filePath())
self.checkContent(fetchedContent.filePath(), b"New content")
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], '.txt')
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], ".txt")
# fetch bad url
fetchedContent = self.storage.fetch(self.url + "/error", self.auth_config.id())
@ -187,16 +199,18 @@ class TestPyQgsExternalStorageBase():
self.assertTrue(fetchedContent.errorString())
self.assertFalse(fetchedContent.filePath())
def testStoreFetchFileImmediately(self):
def testStoreFetchFileImmediatelySpecialCharacters(self):
"""
Test file storing and fetching (Immediately mode)
Test file storing and fetching (Immediately mode) with special characters name
"""
f = self.getNewFile(b"New content")
f = self.getNewFile(b"New content", True)
# store
url = self.url + "/" + os.path.basename(f.name)
storedContent = self.storage.store(f.name, url, self.auth_config.id(), Qgis.ActionStart.Immediate)
storedContent = self.storage.store(
f.name, url, self.auth_config.id(), Qgis.ActionStart.Immediate
)
self.assertTrue(storedContent)
self.assertEqual(storedContent.status(), Qgis.ContentStatus.Running)
@ -216,14 +230,20 @@ class TestPyQgsExternalStorageBase():
self.assertEqual(spyProgressChanged[-1][0], 100)
# fetch
fetchedContent = self.storage.fetch(self.url + "/" + os.path.basename(f.name), self.auth_config.id(), Qgis.ActionStart.Immediate)
fetchedContent = self.storage.fetch(
self.url + "/" + os.path.basename(f.name),
self.auth_config.id(),
Qgis.ActionStart.Immediate,
)
self.assertTrue(fetchedContent)
# Some external storage (SimpleCopy) doesn't actually need to retrieve the resource
self.assertTrue(fetchedContent.status() == Qgis.ContentStatus.Finished or
fetchedContent.status() == Qgis.ContentStatus.Running)
self.assertTrue(
fetchedContent.status() == Qgis.ContentStatus.Finished
or fetchedContent.status() == Qgis.ContentStatus.Running
)
if (fetchedContent.status() == Qgis.ContentStatus.Running):
if fetchedContent.status() == Qgis.ContentStatus.Running:
spyErrorOccurred = QSignalSpy(fetchedContent.errorOccurred)
@ -238,27 +258,136 @@ class TestPyQgsExternalStorageBase():
self.assertFalse(fetchedContent.errorString())
self.assertTrue(fetchedContent.filePath())
self.checkContent(fetchedContent.filePath(), b"New content")
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], '.txt')
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], ".txt")
# fetch again, should be cached
fetchedContent = self.storage.fetch(self.url + "/" + os.path.basename(f.name), self.auth_config.id(), Qgis.ActionStart.Immediate)
fetchedContent = self.storage.fetch(
self.url + "/" + os.path.basename(f.name),
self.auth_config.id(),
Qgis.ActionStart.Immediate,
)
self.assertTrue(fetchedContent)
self.assertEqual(fetchedContent.status(), Qgis.ContentStatus.Finished)
self.assertTrue(not fetchedContent.errorString())
self.assertTrue(fetchedContent.filePath())
self.checkContent(fetchedContent.filePath(), b"New content")
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], '.txt')
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], ".txt")
# fetch bad url
fetchedContent = self.storage.fetch(self.url + "/error", self.auth_config.id(), Qgis.ActionStart.Immediate)
fetchedContent = self.storage.fetch(
self.url + "/error", self.auth_config.id(), Qgis.ActionStart.Immediate
)
self.assertTrue(fetchedContent)
# Some external storage (SimpleCopy) doesn't actually need to retrieve the resource
self.assertTrue(fetchedContent.status() == Qgis.ContentStatus.Failed or
fetchedContent.status() == Qgis.ContentStatus.Running)
self.assertTrue(
fetchedContent.status() == Qgis.ContentStatus.Failed
or fetchedContent.status() == Qgis.ContentStatus.Running
)
if (fetchedContent.status() == Qgis.ContentStatus.Running):
if fetchedContent.status() == Qgis.ContentStatus.Running:
spyErrorOccurred = QSignalSpy(fetchedContent.errorOccurred)
loop = QEventLoop()
fetchedContent.errorOccurred.connect(loop.quit)
fetchedContent.fetched.connect(loop.quit)
loop.exec()
self.assertEqual(len(spyErrorOccurred), 1)
self.assertEqual(fetchedContent.status(), Qgis.ContentStatus.Failed)
self.assertTrue(fetchedContent.errorString())
self.assertFalse(fetchedContent.filePath())
def testStoreFetchFileImmediately(self):
"""
Test file storing and fetching (Immediately mode)
"""
f = self.getNewFile(b"New content")
# store
url = self.url + "/" + os.path.basename(f.name)
storedContent = self.storage.store(
f.name, url, self.auth_config.id(), Qgis.ActionStart.Immediate
)
self.assertTrue(storedContent)
self.assertEqual(storedContent.status(), Qgis.ContentStatus.Running)
spyErrorOccurred = QSignalSpy(storedContent.errorOccurred)
spyProgressChanged = QSignalSpy(storedContent.progressChanged)
loop = QEventLoop()
storedContent.stored.connect(loop.quit)
storedContent.errorOccurred.connect(loop.quit)
loop.exec()
self.assertEqual(len(spyErrorOccurred), 0)
self.assertEqual(storedContent.url(), url)
self.assertFalse(storedContent.errorString())
self.assertEqual(storedContent.status(), Qgis.ContentStatus.Finished)
self.assertTrue(len(spyProgressChanged) > 0)
self.assertEqual(spyProgressChanged[-1][0], 100)
# fetch
fetchedContent = self.storage.fetch(
self.url + "/" + os.path.basename(f.name),
self.auth_config.id(),
Qgis.ActionStart.Immediate,
)
self.assertTrue(fetchedContent)
# Some external storage (SimpleCopy) doesn't actually need to retrieve the resource
self.assertTrue(
fetchedContent.status() == Qgis.ContentStatus.Finished
or fetchedContent.status() == Qgis.ContentStatus.Running
)
if fetchedContent.status() == Qgis.ContentStatus.Running:
spyErrorOccurred = QSignalSpy(fetchedContent.errorOccurred)
loop = QEventLoop()
fetchedContent.fetched.connect(loop.quit)
fetchedContent.errorOccurred.connect(loop.quit)
loop.exec()
self.assertEqual(len(spyErrorOccurred), 0)
self.assertEqual(fetchedContent.status(), Qgis.ContentStatus.Finished)
self.assertFalse(fetchedContent.errorString())
self.assertTrue(fetchedContent.filePath())
self.checkContent(fetchedContent.filePath(), b"New content")
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], ".txt")
# fetch again, should be cached
fetchedContent = self.storage.fetch(
self.url + "/" + os.path.basename(f.name),
self.auth_config.id(),
Qgis.ActionStart.Immediate,
)
self.assertTrue(fetchedContent)
self.assertEqual(fetchedContent.status(), Qgis.ContentStatus.Finished)
self.assertTrue(not fetchedContent.errorString())
self.assertTrue(fetchedContent.filePath())
self.checkContent(fetchedContent.filePath(), b"New content")
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], ".txt")
# fetch bad url
fetchedContent = self.storage.fetch(
self.url + "/error", self.auth_config.id(), Qgis.ActionStart.Immediate
)
self.assertTrue(fetchedContent)
# Some external storage (SimpleCopy) doesn't actually need to retrieve the resource
self.assertTrue(
fetchedContent.status() == Qgis.ContentStatus.Failed
or fetchedContent.status() == Qgis.ContentStatus.Running
)
if fetchedContent.status() == Qgis.ContentStatus.Running:
spyErrorOccurred = QSignalSpy(fetchedContent.errorOccurred)
loop = QEventLoop()
@ -278,7 +407,9 @@ class TestPyQgsExternalStorageBase():
"""
f = self.getNewFile(b"New content")
storedContent = self.storage.store(f.name, self.badUrl + os.path.basename(f.name), self.auth_config.id())
storedContent = self.storage.store(
f.name, self.badUrl + os.path.basename(f.name), self.auth_config.id()
)
self.assertTrue(storedContent)
self.assertEqual(storedContent.status(), Qgis.ContentStatus.NotStarted)
@ -305,7 +436,9 @@ class TestPyQgsExternalStorageBase():
"""
f = self.getNewFile(b"New content")
storedContent = self.storage.store(f.name, self.url + "/" + os.path.basename(f.name))
storedContent = self.storage.store(
f.name, self.url + "/" + os.path.basename(f.name)
)
self.assertTrue(storedContent)
self.assertEqual(storedContent.status(), Qgis.ContentStatus.NotStarted)
@ -334,7 +467,9 @@ class TestPyQgsExternalStorageBase():
f = self.getNewFile(b"New content")
# store
storedContent = self.storage.store(f.name, self.url + "/", self.auth_config.id())
storedContent = self.storage.store(
f.name, self.url + "/", self.auth_config.id()
)
self.assertTrue(storedContent)
self.assertEqual(storedContent.status(), Qgis.ContentStatus.NotStarted)
@ -355,7 +490,9 @@ class TestPyQgsExternalStorageBase():
self.assertEqual(spyProgressChanged[-1][0], 100)
# fetch
fetchedContent = self.storage.fetch(self.url + "/" + os.path.basename(f.name), self.auth_config.id())
fetchedContent = self.storage.fetch(
self.url + "/" + os.path.basename(f.name), self.auth_config.id()
)
self.assertTrue(fetchedContent)
self.assertEqual(fetchedContent.status(), Qgis.ContentStatus.NotStarted)
@ -372,4 +509,4 @@ class TestPyQgsExternalStorageBase():
self.assertFalse(fetchedContent.errorString())
self.assertTrue(fetchedContent.filePath())
self.checkContent(fetchedContent.filePath(), b"New content")
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], '.txt')
self.assertEqual(os.path.splitext(fetchedContent.filePath())[1], ".txt")

View File

@ -34,7 +34,7 @@ from qgis.testing import (
)
class TestPyQgsExternalStorageWebDAV(TestPyQgsExternalStorageBase, unittest.TestCase):
class TestPyQgsExternalStorageWebDav(TestPyQgsExternalStorageBase, unittest.TestCase):
storageType = "WebDAV"
badUrl = "http://nothinghere/"