Skip to content
Permalink
Browse files
Merge pull request #43526 from kadas-albireo/attachments
Implement project file attachments
  • Loading branch information
manisandro committed Jun 24, 2021
2 parents b8d0b2b + 8c94858 commit 23b4786cc8b1d51532f9a78080ce9775391b2181
@@ -1246,7 +1246,38 @@ 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.

.. versionadded:: 3.22
%End

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

.. 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
@@ -1811,7 +1842,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.
@@ -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
@@ -1056,7 +1056,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();

@@ -16012,16 +16011,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
@@ -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.
*/
@@ -375,7 +375,7 @@ QgsProject::QgsProject( QObject *parent )
, mDisplaySettings( new QgsProjectDisplaySettings( this ) )
, mRootGroup( new QgsLayerTree )
, mLabelingEngineSettings( new QgsLabelingEngineSettings )
, mArchive( new QgsProjectArchive() )
, mArchive( new QgsArchive() )
, mAuxiliaryStorage( new QgsAuxiliaryStorage() )
{
mProperties.setName( QStringLiteral( "properties" ) );
@@ -829,7 +829,7 @@ void QgsProject::clear()
mLabelingEngineSettings->clear();

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

emit labelingEngineSettingsChanged();

@@ -1294,6 +1294,16 @@ bool QgsProject::read( QgsProject::ReadFlags flags )
else
{
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage( *this ) );
QFileInfo finfo( mFile.fileName() );
QString attachmentsZip = finfo.absoluteDir().absoluteFilePath( QStringLiteral( "%1_attachments.zip" ).arg( finfo.completeBaseName() ) );
if ( QFile( attachmentsZip ).exists() )
{
std::unique_ptr<QgsArchive> archive( new QgsArchive() );
if ( archive->unzip( attachmentsZip ) )
{
mArchive = std::move( archive );
}
}
returnValue = readProjectFile( mFile.fileName(), flags );
}

@@ -1391,8 +1401,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<QgsArchive> archive = std::move( mArchive );
clear();
mAuxiliaryStorage = std::move( aStorage );
mArchive = std::move( archive );
mFile.setFileName( fileName );
mCachedHomePath.clear();
mProjectScope.reset();
@@ -2160,15 +2172,31 @@ bool QgsProject::write()
// saved
const bool asOk = saveAuxiliaryStorage();
const bool writeOk = writeProjectFile( mFile.fileName() );
bool attachmentsOk = true;
if ( !mArchive->files().isEmpty() )
{
QFileInfo finfo( mFile.fileName() );
QString attachmentsZip = finfo.absoluteDir().absoluteFilePath( QStringLiteral( "%1_attachments.zip" ).arg( finfo.completeBaseName() ) );
attachmentsOk = mArchive->zip( attachmentsZip );
}

// errors raised during writing project file are more important
if ( !asOk && writeOk )
if ( ( !asOk || !attachmentsOk ) && writeOk )
{
const QString err = mAuxiliaryStorage->errorString();
setError( tr( "Unable to save auxiliary storage ('%1')" ).arg( err ) );
QStringList errorMessage;
if ( !asOk )
{
const QString err = mAuxiliaryStorage->errorString();
errorMessage.append( tr( "Unable to save auxiliary storage ('%1')" ).arg( err ) );
}
if ( !attachmentsOk )
{
errorMessage.append( tr( "Unable to save attachments archive" ) );
}
setError( errorMessage.join( "\n" ) );
}

return asOk && writeOk;
return asOk && writeOk && attachmentsOk;
}
}

@@ -2727,7 +2755,7 @@ QgsPathResolver QgsProject::pathResolver() const
filePath = fileName();
}
}
return QgsPathResolver( filePath );
return QgsPathResolver( filePath, mArchive->dir() );
}

QString QgsProject::readPath( const QString &src ) const
@@ -3289,28 +3317,30 @@ 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 ( !static_cast<QgsProjectArchive *>( mArchive.get() )->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( static_cast<QgsProjectArchive *>( mArchive.get() )->auxiliaryStorageFile(), false ) );
}
else
{
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage( *this ) );
}

// read the project file
if ( ! readProjectFile( archive->projectFile(), flags ) )
if ( ! readProjectFile( static_cast<QgsProjectArchive *>( mArchive.get() )->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 );
mArchive->clearProjectFile();
// Remove the temporary .qgs file
static_cast<QgsProjectArchive *>( mArchive.get() )->clearProjectFile();

return true;
}
@@ -3341,7 +3371,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 ) )
@@ -3355,9 +3386,9 @@ bool QgsProject::zip( const QString &filename )
{
mArchive.reset( new QgsProjectArchive() );
mArchive->unzip( mFile.fileName() );
mArchive->clearProjectFile();
static_cast<QgsProjectArchive *>( mArchive.get() )->clearProjectFile();

const QString auxiliaryStorageFile = mArchive->auxiliaryStorageFile();
const QString auxiliaryStorageFile = static_cast<QgsProjectArchive *>( mArchive.get() )->auxiliaryStorageFile();
if ( ! auxiliaryStorageFile.isEmpty() )
{
archive->addFile( auxiliaryStorageFile );
@@ -3378,6 +3409,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 ) )
@@ -3603,6 +3644,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;
@@ -1320,25 +1320,29 @@ 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
* \note Attached files are only supported by QGZ file based projects
* \see collectAttachedFiles()
* \since QGIS 3.8
* 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.
* \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.
@@ -1833,21 +1837,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.
*
@@ -2039,7 +2028,7 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera

QVariantMap mCustomVariables;

std::unique_ptr<QgsProjectArchive> mArchive;
std::unique_ptr<QgsArchive> mArchive;

std::unique_ptr<QgsAuxiliaryStorage> mAuxiliaryStorage;

@@ -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}" );
@@ -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 );

0 comments on commit 23b4786

Please sign in to comment.