Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Implement project file attachments
  • Loading branch information
manisandro committed Jun 22, 2021
1 parent c2a1d87 commit 001d363
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 55 deletions.
40 changes: 39 additions & 1 deletion python/core/auto_generated/project/qgsproject.sip.in
Expand Up @@ -1246,7 +1246,46 @@ Returns the current auxiliary storage.
.. versionadded:: 3.0
%End

QString createAttachedFile( const QString &nameTemplate );
%Docstring
Attaches a file to the project

:param nameTemplate: Any filename template, used as a basename for attachment file, i.e. "myfile.ext"

:return: The path to the file where the contents can be written to.

.. note::

Attached files are only supported by QGZ file based projects

.. versionadded:: 3.22
%End

QStringList attachedFiles() const;
%Docstring
Returns a map of all attached files with identifier and real paths.

.. note::

Attached files are only supported by QGZ file based projects

.. seealso:: :py:func:`createAttachedFile`

.. versionadded:: 3.22
%End

bool removeAttachedFile( const QString &path );
%Docstring
Removes the attached file

:param path: Path to the attached file

:return: Whether removal succeeded.

.. seealso:: :py:func:`createAttachedFile`

.. versionadded:: 3.22
%End

const QgsProjectMetadata &metadata() const;
%Docstring
Expand Down Expand Up @@ -1811,7 +1850,6 @@ Emitted when setDirty(true) is called.
.. versionadded:: 3.20
%End


void mapScalesChanged() /Deprecated/;
%Docstring
Emitted when the list of custom project map scales changes.
Expand Down
2 changes: 1 addition & 1 deletion python/core/auto_generated/qgspathresolver.sip.in
Expand Up @@ -21,7 +21,7 @@ Resolves relative paths into absolute paths and vice versa. Used for writing
#include "qgspathresolver.h"
%End
public:
explicit QgsPathResolver( const QString &baseFileName = QString() );
explicit QgsPathResolver( const QString &baseFileName = QString(), const QString &attachmentDir = QString() );
%Docstring
Initialize path resolver with a base filename. Null filename means no conversion between relative/absolute path
%End
Expand Down
11 changes: 0 additions & 11 deletions src/app/qgisapp.cpp
Expand Up @@ -1055,7 +1055,6 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh
mSnappingUtils = new QgsMapCanvasSnappingUtils( mMapCanvas, this );
mMapCanvas->setSnappingUtils( mSnappingUtils );
connect( QgsProject::instance(), &QgsProject::snappingConfigChanged, mSnappingUtils, &QgsSnappingUtils::setConfig );
connect( QgsProject::instance(), &QgsProject::collectAttachedFiles, this, &QgisApp::generateProjectAttachedFiles );

endProfile();

Expand Down Expand Up @@ -16011,16 +16010,6 @@ void QgisApp::onSnappingConfigChanged()
mSnappingUtils->setConfig( QgsProject::instance()->snappingConfig() );
}

void QgisApp::generateProjectAttachedFiles( QgsStringMap &files )
{
QTemporaryFile *previewImage = new QTemporaryFile( QStringLiteral( "preview-XXXXXXXXXXX.png" ) );
previewImage->open();
previewImage->close();
createPreviewImage( previewImage->fileName() );
files.insert( QStringLiteral( "preview.png" ), previewImage->fileName() );
previewImage->deleteLater();
}

void QgisApp::createPreviewImage( const QString &path, const QIcon &icon )
{
// Render the map canvas
Expand Down
2 changes: 0 additions & 2 deletions src/app/qgisapp.h
Expand Up @@ -1307,8 +1307,6 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow

void onSnappingConfigChanged();

void generateProjectAttachedFiles( QgsStringMap &files );

/**
* Triggers validation of the specified \a crs.
*/
Expand Down
61 changes: 53 additions & 8 deletions src/core/project/qgsproject.cpp
Expand Up @@ -829,7 +829,7 @@ void QgsProject::clear()
mLabelingEngineSettings->clear();

mAuxiliaryStorage.reset( new QgsAuxiliaryStorage() );
mArchive->clear();
mArchive.reset( new QgsProjectArchive() );

emit labelingEngineSettingsChanged();

Expand Down Expand Up @@ -1391,8 +1391,10 @@ bool QgsProject::readProjectFile( const QString &filename, QgsProject::ReadFlags
profile.switchTask( tr( "Creating auxiliary storage" ) );
QString fileName = mFile.fileName();
std::unique_ptr<QgsAuxiliaryStorage> aStorage = std::move( mAuxiliaryStorage );
std::unique_ptr<QgsProjectArchive> archive = std::move( mArchive );
clear();
mAuxiliaryStorage = std::move( aStorage );
mArchive = std::move( archive );
mFile.setFileName( fileName );
mCachedHomePath.clear();
mProjectScope.reset();
Expand Down Expand Up @@ -2727,7 +2729,7 @@ QgsPathResolver QgsProject::pathResolver() const
filePath = fileName();
}
}
return QgsPathResolver( filePath );
return QgsPathResolver( filePath, mArchive->dir() );
}

QString QgsProject::readPath( const QString &src ) const
Expand Down Expand Up @@ -3289,27 +3291,29 @@ bool QgsProject::unzip( const QString &filename, QgsProject::ReadFlags flags )
return false;
}

// Keep the archive
mArchive = std::move( archive );

// load auxiliary storage
if ( !archive->auxiliaryStorageFile().isEmpty() )
if ( !mArchive->auxiliaryStorageFile().isEmpty() )
{
// database file is already a copy as it's been unzipped. So we don't open
// auxiliary storage in copy mode in this case
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage( archive->auxiliaryStorageFile(), false ) );
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage( mArchive->auxiliaryStorageFile(), false ) );
}
else
{
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage( *this ) );
}

// read the project file
if ( ! readProjectFile( archive->projectFile(), flags ) )
if ( ! readProjectFile( mArchive->projectFile(), flags ) )
{
setError( tr( "Cannot read unzipped qgs project file" ) );
return false;
}

// keep the archive and remove the temporary .qgs file
mArchive = std::move( archive );
// Remove the temporary .qgs file
mArchive->clearProjectFile();

return true;
Expand Down Expand Up @@ -3341,7 +3345,8 @@ bool QgsProject::zip( const QString &filename )

// save auxiliary storage
const QFileInfo info( qgsFile );
const QString asFileName = info.path() + QDir::separator() + info.completeBaseName() + "." + QgsAuxiliaryStorage::extension();
QString asExt = QStringLiteral( ".%1" ).arg( QgsAuxiliaryStorage::extension() );
const QString asFileName = info.path() + QDir::separator() + info.completeBaseName() + asExt;

bool auxiliaryStorageSavedOk = true;
if ( ! saveAuxiliaryStorage( asFileName ) )
Expand Down Expand Up @@ -3378,6 +3383,16 @@ bool QgsProject::zip( const QString &filename )
// create the archive
archive->addFile( qgsFile.fileName() );

// Add all other files
const QStringList &files = mArchive->files();
for ( const QString &file : files )
{
if ( !file.endsWith( ".qgs", Qt::CaseInsensitive ) && !file.endsWith( asExt, Qt::CaseInsensitive ) )
{
archive->addFile( file );
}
}

// zip
bool zipOk = true;
if ( !archive->zip( filename ) )
Expand Down Expand Up @@ -3603,6 +3618,36 @@ QgsAuxiliaryStorage *QgsProject::auxiliaryStorage()
return mAuxiliaryStorage.get();
}

QString QgsProject::createAttachedFile( const QString &nameTemplate )
{
QString fileName = nameTemplate;
QDir archiveDir( mArchive->dir() );
QTemporaryFile tmpFile( archiveDir.filePath( "XXXXXX_" + nameTemplate ), this );
tmpFile.setAutoRemove( false );
tmpFile.open();
mArchive->addFile( tmpFile.fileName() );
return tmpFile.fileName();
}

QStringList QgsProject::attachedFiles() const
{
QStringList attachments;
QString baseName = QFileInfo( fileName() ).baseName();
for ( const QString &file : mArchive->files() )
{
if ( QFileInfo( file ).baseName() != baseName )
{
attachments.append( file );
}
}
return attachments;
}

bool QgsProject::removeAttachedFile( const QString &path )
{
return mArchive->removeFile( path );
}

const QgsProjectMetadata &QgsProject::metadata() const
{
return mMetadata;
Expand Down
45 changes: 18 additions & 27 deletions src/core/project/qgsproject.h
Expand Up @@ -1320,25 +1320,31 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
QgsAuxiliaryStorage *auxiliaryStorage();

/**
* Returns the path to an attached file known by \a fileName.
*
* \note Not available in Python bindings
* Attaches a file to the project
* \param nameTemplate Any filename template, used as a basename for attachment file, i.e. "myfile.ext"
* \return The path to the file where the contents can be written to.
* \note Attached files are only supported by QGZ file based projects
* \see collectAttachedFiles()
* \since QGIS 3.8
* \since QGIS 3.22
*/
QString attachedFile( const QString &fileName ) const SIP_SKIP;
QString createAttachedFile( const QString &nameTemplate );

/**
* Returns a map of all attached files with relative paths and real paths.
* Returns a map of all attached files with identifier and real paths.
*
* \note Not available in Python bindings
* \note Attached files are only supported by QGZ file based projects
* \see collectAttachedFiles()
* \see attachedFile()
* \since QGIS 3.8
* \see createAttachedFile()
* \since QGIS 3.22
*/
QStringList attachedFiles() const;

/**
* Removes the attached file
* \param path Path to the attached file
* \return Whether removal succeeded.
* \see createAttachedFile()
* \since QGIS 3.22
*/
QgsStringMap attachedFiles() const SIP_SKIP;
bool removeAttachedFile( const QString &path );

/**
* Returns a reference to the project's metadata store.
Expand Down Expand Up @@ -1833,21 +1839,6 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
void dirtySet();

/**
* 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.
*
* \note Not available in Python bindings
* \note Only will be emitted with QGZ project files
* \see attachedFiles()
* \see attachedFile()
* \since QGIS 3.8
*/
void collectAttachedFiles( QgsStringMap &files SIP_INOUT ) SIP_SKIP;

/**
* Emitted when the list of custom project map scales changes.
*
Expand Down
4 changes: 2 additions & 2 deletions src/core/qgsmaplayer.cpp
Expand Up @@ -256,7 +256,7 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, QgsReadWriteCon
// set data source
mnl = layerElement.namedItem( QStringLiteral( "datasource" ) );
mne = mnl.toElement();
mDataSource = mne.text();
mDataSource = context.pathResolver().readPath( mne.text() );

// if the layer needs authentication, ensure the master password is set
QRegExp rx( "authcfg=([a-z]|[A-Z]|[0-9]){7}" );
Expand Down Expand Up @@ -453,7 +453,7 @@ bool QgsMapLayer::writeLayerXml( QDomElement &layerElement, QDomDocument &docume

// data source
QDomElement dataSource = document.createElement( QStringLiteral( "datasource" ) );
QString src = encodedSource( source(), context );
QString src = context.pathResolver().writePath( encodedSource( source(), context ) );
QDomText dataSourceText = document.createTextNode( src );
dataSource.appendChild( dataSourceText );
layerElement.appendChild( dataSource );
Expand Down
16 changes: 14 additions & 2 deletions src/core/qgspathresolver.cpp
Expand Up @@ -18,6 +18,7 @@

#include "qgis.h"
#include "qgsapplication.h"
#include <QDir>
#include <QFileInfo>
#include <QUrl>
#include <QUuid>
Expand All @@ -27,8 +28,8 @@ typedef std::vector< std::pair< QString, std::function< QString( const QString &
Q_GLOBAL_STATIC( CustomResolvers, sCustomResolvers )
Q_GLOBAL_STATIC( CustomResolvers, sCustomWriters )

QgsPathResolver::QgsPathResolver( const QString &baseFileName )
: mBaseFileName( baseFileName )
QgsPathResolver::QgsPathResolver( const QString &baseFileName, const QString &attachmentDir )
: mBaseFileName( baseFileName ), mAttachmentDir( attachmentDir )
{
}

Expand Down Expand Up @@ -56,6 +57,11 @@ QString QgsPathResolver::readPath( const QString &f ) const
// strip away "localized:" prefix, replace with actual inbuilt data folder path
return QgsApplication::localizedDataPathRegistry()->globalPath( src.mid( 10 ) ) ;
}
if ( src.startsWith( QLatin1String( "attachment:" ) ) )
{
// resolve attachment w.r.t. temporary path where project archive is extracted
return QDir( mAttachmentDir ).absoluteFilePath( src.mid( 11 ) );
}

if ( mBaseFileName.isNull() )
{
Expand Down Expand Up @@ -229,6 +235,12 @@ QString QgsPathResolver::writePath( const QString &s ) const
return QStringLiteral( "inbuilt:" ) + src.mid( QgsApplication::pkgDataPath().length() + 10 );
}

if ( !mAttachmentDir.isEmpty() && src.startsWith( mAttachmentDir ) )
{
// Replace attachment dir with "attachment:" prefix
return QStringLiteral( "attachment:" ) + QFileInfo( src ).fileName();
}

if ( mBaseFileName.isEmpty() )
{
return src;
Expand Down
4 changes: 3 additions & 1 deletion src/core/qgspathresolver.h
Expand Up @@ -32,7 +32,7 @@ class CORE_EXPORT QgsPathResolver
{
public:
//! Initialize path resolver with a base filename. Null filename means no conversion between relative/absolute path
explicit QgsPathResolver( const QString &baseFileName = QString() );
explicit QgsPathResolver( const QString &baseFileName = QString(), const QString &attachmentDir = QString() );

/**
* Prepare a filename to save it to the project file.
Expand Down Expand Up @@ -284,6 +284,8 @@ class CORE_EXPORT QgsPathResolver
private:
//! path to a file that is the base for relative path resolution
QString mBaseFileName;
//! path where attached files are stored
QString mAttachmentDir;
};

#endif // QGSPATHRESOLVER_H

0 comments on commit 001d363

Please sign in to comment.