Skip to content
Permalink
Browse files

Allow adding attachments in qgz files

  • Loading branch information
m-kuhn committed May 14, 2019
1 parent 5844a0f commit a55c25bbeb89282b04209f80d8db1921695be2a3
@@ -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:
@@ -39,14 +39,15 @@ 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
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
@@ -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" ) );
@@ -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;
@@ -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( fileName )
* \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( fileName )
* \since QGIS 3.8
*/
void collectAttachedFiles( QgsStringMap &files SIP_INOUT );

public slots:

/**
@@ -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 )
{
@@ -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
@@ -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"

0 comments on commit a55c25b

Please sign in to comment.
You can’t perform that action at this time.