Add helper methods to get filename from content disposition headers

This commit is contained in:
Nyall Dawson 2022-06-20 09:46:58 +10:00
parent 4c16521431
commit 75355386c2
11 changed files with 150 additions and 0 deletions

View File

@ -60,6 +60,13 @@ Optionally, authentication configuration can be set via the ``authcfg`` argument
Returns a reference to the network reply
:return: QNetworkReply for fetched URL content
%End
QString contentDispositionFilename() const;
%Docstring
Returns the associated filename from the reply's content disposition header, if present.
.. versionadded:: 3.28
%End
QString contentAsString() const;

View File

@ -75,6 +75,13 @@ May return ``None`` if the request has not yet completed.
the :py:func:`QgsNetworkContentFetcherTask.fetched()` signal.
%End
QString contentDispositionFilename() const;
%Docstring
Returns the associated filename from the reply's content disposition header, if present.
.. versionadded:: 3.28
%End
QString contentAsString() const;
%Docstring
Returns the fetched content as a string

View File

@ -125,6 +125,20 @@ can only be done once.
Blocking network requests (see :py:class:`QgsBlockingNetworkRequest`) will automatically populate this content.
.. seealso:: :py:func:`setContent`
%End
static QString extractFilenameFromContentDispositionHeader( QNetworkReply *reply );
%Docstring
Extracts the filename component of the content disposition header from a network ``reply``.
.. versionadded:: 3.28
%End
static QString extractFileNameFromContentDispositionHeader( const QString &header );
%Docstring
Extracts the filename component of the content disposition header from the ``header``.
.. versionadded:: 3.28
%End
};

View File

@ -96,6 +96,11 @@ QNetworkReply *QgsNetworkContentFetcher::reply()
return mReply;
}
QString QgsNetworkContentFetcher::contentDispositionFilename() const
{
return mReply ? QgsNetworkReplyContent::extractFilenameFromContentDispositionHeader( mReply ) : QString();
}
QString QgsNetworkContentFetcher::contentAsString() const
{
if ( !mContentLoaded || !mReply )

View File

@ -73,6 +73,13 @@ class CORE_EXPORT QgsNetworkContentFetcher : public QObject
*/
QNetworkReply *reply();
/**
* Returns the associated filename from the reply's content disposition header, if present.
*
* \since QGIS 3.28
*/
QString contentDispositionFilename() const;
/**
* Returns the fetched content as a string
* \returns string containing network content

View File

@ -18,6 +18,7 @@
#include "qgsnetworkcontentfetchertask.h"
#include "qgsnetworkcontentfetcher.h"
#include "qgsnetworkreply.h"
#include <QEventLoop>
QgsNetworkContentFetcherTask::QgsNetworkContentFetcherTask( const QUrl &url, const QString &authcfg, QgsTask::Flags flags, const QString &description )
@ -90,6 +91,11 @@ QNetworkReply *QgsNetworkContentFetcherTask::reply()
return mFetcher ? mFetcher->reply() : nullptr;
}
QString QgsNetworkContentFetcherTask::contentDispositionFilename() const
{
return mFetcher ? mFetcher->contentDispositionFilename() : QString();
}
QString QgsNetworkContentFetcherTask::contentAsString() const
{
return mFetcher ? mFetcher->contentAsString() : QString();

View File

@ -87,6 +87,13 @@ class CORE_EXPORT QgsNetworkContentFetcherTask : public QgsTask
*/
QNetworkReply *reply();
/**
* Returns the associated filename from the reply's content disposition header, if present.
*
* \since QGIS 3.28
*/
QString contentDispositionFilename() const;
/**
* Returns the fetched content as a string
*

View File

@ -15,6 +15,8 @@
#include "qgsnetworkreply.h"
#include <QNetworkReply>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
QgsNetworkReplyContent::QgsNetworkReplyContent( QNetworkReply *reply )
: mError( reply->error() )
@ -75,3 +77,38 @@ QByteArray QgsNetworkReplyContent::rawHeader( const QByteArray &headerName ) con
}
return QByteArray();
}
QString QgsNetworkReplyContent::extractFilenameFromContentDispositionHeader( QNetworkReply *reply )
{
if ( !reply )
return QString();
return extractFileNameFromContentDispositionHeader( reply->header( QNetworkRequest::ContentDispositionHeader ).toString() );
}
QString QgsNetworkReplyContent::extractFileNameFromContentDispositionHeader( const QString &header )
{
const thread_local QRegularExpression rx( QStringLiteral( R"""(filename[^;\n]*=\s*(UTF-\d['"]*)?((['"]).*?[.]$\2|[^;\n]*)?)""" ), QRegularExpression::PatternOption::CaseInsensitiveOption );
QRegularExpressionMatchIterator i = rx.globalMatch( header, 0 );
QString res;
// we want the last match here, as that will have the UTF filename when present
while ( i.hasNext() )
{
const QRegularExpressionMatch match = i.next();
res = match.captured( 2 );
}
if ( res.startsWith( '"' ) )
{
res = res.mid( 1 );
if ( res.endsWith( '"' ) )
res.chop( 1 );
}
if ( !res.isEmpty() )
{
res = QUrl::fromPercentEncoding( res.toUtf8() );
}
return res;
}

View File

@ -157,6 +157,20 @@ class CORE_EXPORT QgsNetworkReplyContent
*/
QByteArray content() const { return mContent; }
/**
* Extracts the filename component of the content disposition header from a network \a reply.
*
* \since QGIS 3.28
*/
static QString extractFilenameFromContentDispositionHeader( QNetworkReply *reply );
/**
* Extracts the filename component of the content disposition header from the \a header.
*
* \since QGIS 3.28
*/
static QString extractFileNameFromContentDispositionHeader( const QString &header );
private:
QNetworkReply::NetworkError mError = QNetworkReply::NoError;

View File

@ -220,6 +220,7 @@ ADD_PYTHON_TEST(PyQgsNetworkAccessManager test_qgsnetworkaccessmanager.py)
ADD_PYTHON_TEST(PyQgsNetworkContentFetcher test_qgsnetworkcontentfetcher.py)
ADD_PYTHON_TEST(PyQgsNetworkContentFetcherRegistry test_qgsnetworkcontentfetcherregistry.py)
ADD_PYTHON_TEST(PyQgsNetworkContentFetcherTask test_qgsnetworkcontentfetchertask.py)
ADD_PYTHON_TEST(PyQgsNetworkReply test_qgsnetworkreply.py)
ADD_PYTHON_TEST(PyQgsNullSymbolRenderer test_qgsnullsymbolrenderer.py)
ADD_PYTHON_TEST(PyQgsNumericFormat test_qgsnumericformat.py)
ADD_PYTHON_TEST(PyQgsNumericFormatGui test_qgsnumericformatgui.py)

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsNetworkReplyContent
.. 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.
"""
from builtins import chr
from builtins import str
__author__ = 'Nyall Dawson'
__date__ = '20/06/2022'
__copyright__ = 'Copyright 2022, The QGIS Project'
import qgis # NOQA
import os
from qgis.testing import unittest, start_app
from qgis.core import QgsNetworkReplyContent
from utilities import unitTestDataPath
from qgis.PyQt.QtCore import QUrl
from qgis.PyQt.QtNetwork import QNetworkReply, QNetworkRequest
import socketserver
import threading
import http.server
app = start_app()
class TestQgsNetworkReply(unittest.TestCase):
def test_content_disposition_filename(self):
self.assertEqual(QgsNetworkReplyContent.extractFileNameFromContentDispositionHeader('x'), '')
self.assertEqual(QgsNetworkReplyContent.extractFileNameFromContentDispositionHeader('attachment; filename=content.txt'), 'content.txt')
self.assertEqual(
QgsNetworkReplyContent.extractFileNameFromContentDispositionHeader("attachment; filename*=UTF-8''filename.txt"), 'filename.txt')
self.assertEqual(
QgsNetworkReplyContent.extractFileNameFromContentDispositionHeader('attachment; filename="EURO rates"; filename*=utf-8\'\'%e2%82%ac%20rates'), '€ rates')
self.assertEqual(
QgsNetworkReplyContent.extractFileNameFromContentDispositionHeader('attachment; filename="omáèka.jpg"'), 'omáèka.jpg')
if __name__ == "__main__":
unittest.main()