Merge pull request #10002 from m-kuhn/qgz-attachments

Allow adding attachments in qgz files
This commit is contained in:
Matthias Kuhn 2019-05-16 09:53:09 +02:00 committed by GitHub
commit 0804e342c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 169 additions and 10 deletions

View File

@ -974,6 +974,34 @@ provider.
Returns the current auxiliary storage.
.. versionadded:: 3.0
%End
QString attachedFile( const QString &fileName ) const;
%Docstring
Returns the path to an attached file known by ``fileName``.
.. note::
Attached files are only supported by QGZ file based projects
.. seealso:: :py:func:`collectAttachedFiles`
.. versionadded:: 3.8
%End
QgsStringMap attachedFiles() const;
%Docstring
Returns a map of all attached files with relative paths and real paths.
.. note::
Attached files are only supported by QGZ file based projects
.. seealso:: :py:func:`collectAttachedFiles`
.. seealso:: :py:func:`attachedFile`
.. versionadded:: 3.8
%End
const QgsProjectMetadata &metadata() const;
@ -1390,6 +1418,31 @@ Emitted when the project dirty status changes.
:param dirty: ``True`` if the project is in a dirty state and has pending unsaved changes.
.. versionadded:: 3.2
%End
void collectAttachedFiles( QgsStringMap &files /In,Out/ );
%Docstring
Emitted whenever the project is saved to a qgz file.
This can be used to package additional files into the qgz file by modifying the ``files`` map.
Map keys represent relative paths inside the qgz file, map values represent the path to
the source file.
In python, append additional files to the map and return the modified map.
.. code-block:: python
QgsProject.instance().collectAttachedFiles.connect(lambda files: files + ['/absolute/path/to/my/attachment.txt'])
.. note::
Only will be emitted with QGZ project files
.. seealso:: :py:func:`attachedFiles`
.. seealso:: :py:func:`attachedFile`
.. versionadded:: 3.8
%End
public slots:

View File

@ -39,7 +39,7 @@ Unzip a zip file in an output directory.
.. versionadded:: 3.0
%End
bool zip( const QString &zip, const QStringList &files );
bool zip( const QString &zip, const QStringList &files, const QString &root = QString() );
%Docstring
Zip the list of files in the zip file. If the zip file already exists or is
empty, an error is returned. If an input file does not exist, an error is
@ -47,6 +47,7 @@ also returned.
:param zip: The zip filename
:param files: The absolute path to files to embed within the zip
:param root: The root path in which the files are located. This is used to determine the relative path inside the zip file.
.. versionadded:: 3.0
%End

View File

@ -61,7 +61,7 @@ bool QgsArchive::zip( const QString &filename )
QFile tmpFile( tempPath + QDir::separator() + uuid );
// zip content
if ( ! QgsZipUtils::zip( tmpFile.fileName(), mFiles ) )
if ( ! QgsZipUtils::zip( tmpFile.fileName(), mFiles, dir() ) )
{
QString err = QObject::tr( "Unable to zip content" );
QgsMessageLog::logMessage( err, QStringLiteral( "QgsArchive" ) );

View File

@ -2763,10 +2763,26 @@ bool QgsProject::zip( const QString &filename )
return false;
}
QgsStringMap attachedFiles;
emit collectAttachedFiles( attachedFiles );
// create the archive
archive->addFile( qgsFile.fileName() );
archive->addFile( asFileName );
// add additional collected attachment files
auto attachedFilesIterator = attachedFiles.constBegin();
while ( attachedFilesIterator != attachedFiles.constEnd() )
{
QString filepath = info.path() + QDir::separator() + attachedFilesIterator.key();
QDir().mkpath( QFileInfo( filepath ).dir().path() );
if ( QFile::copy( attachedFilesIterator.value(), filepath ) )
archive->addFile( filepath );
else
QgsMessageLog::logMessage( QStringLiteral( "Could not copy file '%1' to '%2'" ).arg( attachedFilesIterator.value(), filepath ) );
++attachedFilesIterator;
}
// zip
if ( !archive->zip( filename ) )
{
@ -2956,6 +2972,21 @@ QgsAuxiliaryStorage *QgsProject::auxiliaryStorage()
return mAuxiliaryStorage.get();
}
QString QgsProject::attachedFile( const QString &fileName ) const
{
return mArchive->dir() + QDir::separator() + fileName;
}
QgsStringMap QgsProject::attachedFiles() const
{
QgsStringMap files;
QString dir = mArchive->dir();
const QStringList tempFiles = mArchive->files();
for ( const QString &tempFile : tempFiles )
files.insert( tempFile, dir + QDir::separator() + tempFile );
return files;
}
const QgsProjectMetadata &QgsProject::metadata() const
{
return mMetadata;

View File

@ -966,6 +966,25 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
QgsAuxiliaryStorage *auxiliaryStorage();
/**
* Returns the path to an attached file known by \a fileName.
*
* \note Attached files are only supported by QGZ file based projects
* \see collectAttachedFiles()
* \since QGIS 3.8
*/
QString attachedFile( const QString &fileName ) const;
/**
* Returns a map of all attached files with relative paths and real paths.
*
* \note Attached files are only supported by QGZ file based projects
* \see collectAttachedFiles()
* \see attachedFile()
* \since QGIS 3.8
*/
QgsStringMap attachedFiles() const;
/**
* Returns a reference to the project's metadata store.
* \see setMetadata()
@ -1329,6 +1348,26 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
void isDirtyChanged( bool dirty );
/**
* Emitted whenever the project is saved to a qgz file.
* This can be used to package additional files into the qgz file by modifying the \a files map.
*
* Map keys represent relative paths inside the qgz file, map values represent the path to
* the source file.
*
* In python, append additional files to the map and return the modified map.
*
* \code{.py}
* QgsProject.instance().collectAttachedFiles.connect(lambda files: files + ['/absolute/path/to/my/attachment.txt'])
* \endcode
*
* \note Only will be emitted with QGZ project files
* \see attachedFiles()
* \see attachedFile()
* \since QGIS 3.8
*/
void collectAttachedFiles( QgsStringMap &files SIP_INOUT );
public slots:
/**

View File

@ -82,6 +82,9 @@ bool QgsZipUtils::unzip( const QString &zipFilename, const QString &dir, QString
if ( zip_fread( file, buf.get(), len ) != -1 )
{
QString fileName( stat.name );
// remove leading `/` e.g. `/project.qgs` -> `project.qgs`
while ( fileName.startsWith( QDir::separator() ) )
fileName.remove( 0, 1 );
QFileInfo newFile( QDir( dir ), fileName );
// Create path for a new file if it does not exist.
@ -129,7 +132,7 @@ bool QgsZipUtils::unzip( const QString &zipFilename, const QString &dir, QString
return true;
}
bool QgsZipUtils::zip( const QString &zipFilename, const QStringList &files )
bool QgsZipUtils::zip( const QString &zipFilename, const QStringList &files, const QString &root )
{
if ( zipFilename.isEmpty() )
{
@ -153,15 +156,22 @@ bool QgsZipUtils::zip( const QString &zipFilename, const QStringList &files )
return false;
}
const QByteArray fileNamePtr = file.toUtf8();
zip_source *src = zip_source_file( z, fileNamePtr.constData(), 0, 0 );
const QByteArray filePathUtf8 = file.toUtf8();
zip_source *src = zip_source_file( z, filePathUtf8.constData(), 0, 0 );
if ( src )
{
const QByteArray fileInfoPtr = fileInfo.fileName().toUtf8();
QString fileName;
if ( root.isEmpty() || !file.startsWith( root ) )
fileName = fileInfo.fileName();
else
fileName = file.right( file.length() - root.length() );
const QByteArray fileNameUtf8 = fileName.toUtf8();
#if LIBZIP_VERSION_MAJOR < 1
int rc = ( int ) zip_add( z, fileInfoPtr.constData(), src );
int rc = ( int ) zip_add( z, fileNameUtf8.constData(), src );
#else
int rc = ( int ) zip_file_add( z, fileInfoPtr.constData(), src, 0 );
int rc = ( int ) zip_file_add( z, fileNameUtf8.constData(), src, 0 );
#endif
if ( rc == -1 )
{

View File

@ -54,9 +54,10 @@ namespace QgsZipUtils
* also returned.
* \param zip The zip filename
* \param files The absolute path to files to embed within the zip
* \param root The root path in which the files are located. This is used to determine the relative path inside the zip file.
* \since QGIS 3.0
*/
CORE_EXPORT bool zip( const QString &zip, const QStringList &files );
CORE_EXPORT bool zip( const QString &zip, const QStringList &files, const QString &root = QString() );
};
#endif //QGSZIPUTILS_H

View File

@ -43,6 +43,7 @@ class TestQgsProject : public QObject
void testLayerFlags();
void testLocalFiles();
void testLocalUrlFiles();
void testAttachedFiles();
};
void TestQgsProject::init()
@ -404,7 +405,6 @@ void TestQgsProject::testLocalFiles()
f2.close();
QgsPathResolver resolver( f.fileName( ) );
QCOMPARE( resolver.writePath( layerPath ), QString( "./" + info.baseName() + ".shp" ) ) ;
}
void TestQgsProject::testLocalUrlFiles()
@ -427,6 +427,30 @@ void TestQgsProject::testLocalUrlFiles()
}
void TestQgsProject::testAttachedFiles()
{
QTemporaryDir dir;
QString qgzFileName = dir.filePath( QStringLiteral( "project.qgz" ) );
QTemporaryFile attachedFile;
if ( attachedFile.open() )
{
QTextStream stream( &attachedFile );
stream << QStringLiteral( "success" ) << endl;
}
QgsProject prj;
connect( &prj, &QgsProject::collectAttachedFiles, this, [ &attachedFile ]( QgsStringMap & files ) { files.insert( QStringLiteral( "test/file.txt" ), attachedFile.fileName() ); } );
prj.write( qgzFileName );
prj.clear();
prj.read( qgzFileName );
QFile extractedFile( prj.attachedFile( QStringLiteral( "test/file.txt" ) ) );
extractedFile.open( QFile::ReadOnly | QFile::Text );
QTextStream in( &extractedFile );
QCOMPARE( QString::fromUtf8( extractedFile.readAll().constData() ), QStringLiteral( "success\n" ) );
}
QGSTEST_MAIN( TestQgsProject )
#include "testqgsproject.moc"