Fix project archiving.

Use simplified file extension and name checks rather than a regular
expression to determine which files to archive.

Fix incorrect use of wxFileName which generates invalid paths when the
last folder in the path contains dots.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/20431
This commit is contained in:
Wayne Stambaugh 2025-04-05 15:53:59 -04:00
parent 12c82149bf
commit 598850b1f0
2 changed files with 138 additions and 183 deletions

View File

@ -21,6 +21,7 @@
#include <wx/dir.h> #include <wx/dir.h>
#include <wx/filedlg.h> #include <wx/filedlg.h>
#include <wx/fs_zip.h> #include <wx/fs_zip.h>
#include <wx/regex.h>
#include <wx/uri.h> #include <wx/uri.h>
#include <wx/wfstream.h> #include <wx/wfstream.h>
#include <wx/zipstrm.h> #include <wx/zipstrm.h>
@ -43,33 +44,13 @@
class PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER : public wxDirTraverser class PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER : public wxDirTraverser
{ {
public: public:
PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER( const std::string& aExtRegex, const wxString& aPrjDir, wxZipOutputStream& aZipFileOutput, PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER( const wxString& aPrjDir ) :
REPORTER& aReporter, bool aVerbose ) : m_prjDir( aPrjDir )
m_zipFile( aZipFileOutput ),
m_prjDir( aPrjDir ),
m_fileExtRegex( aExtRegex, std::regex_constants::ECMAScript | std::regex_constants::icase ),
m_reporter( aReporter ),
m_errorOccurred( false ),
m_verbose( aVerbose )
{} {}
virtual wxDirTraverseResult OnFile( const wxString& aFilename ) override virtual wxDirTraverseResult OnFile( const wxString& aFilename ) override
{ {
if( std::regex_search( aFilename.ToStdString(), m_fileExtRegex ) ) m_files.emplace_back( aFilename );
{
addFileToZip( aFilename );
// Special processing for IBIS files to include the corresponding pkg file
if( aFilename.EndsWith( FILEEXT::IbisFileExtension ) )
{
wxFileName package( aFilename );
package.MakeRelativeTo( m_prjDir );
package.SetExt( wxS( "pkg" ) );
if( package.Exists() )
addFileToZip( package.GetFullPath() );
}
}
return wxDIR_CONTINUE; return wxDIR_CONTINUE;
} }
@ -79,76 +60,22 @@ public:
return wxDIR_CONTINUE; return wxDIR_CONTINUE;
} }
unsigned long GetUncompressedBytes() const const std::vector<wxString>& GetFilesToArchive() const
{ {
return m_uncompressedBytes; return m_files;
}
bool GetErrorOccurred() const
{
return m_errorOccurred;
} }
private: private:
void addFileToZip( const wxString& aFilename) wxString m_prjDir;
{ std::vector<wxString> m_files;
wxString msg;
wxFileSystem fsfile;
wxFileName curr_fn( aFilename );
curr_fn.MakeRelativeTo( m_prjDir );
wxString currFilename = curr_fn.GetFullPath();
// Read input file and add it to the zip file:
wxFSFile* infile = fsfile.OpenFile( currFilename );
if( infile )
{
m_zipFile.PutNextEntry( currFilename, infile->GetModificationTime() );
infile->GetStream()->Read( m_zipFile );
m_zipFile.CloseEntry();
m_uncompressedBytes += infile->GetStream()->GetSize();
if( m_verbose )
{
msg.Printf( _( "Archived file '%s'." ), currFilename );
m_reporter.Report( msg, RPT_SEVERITY_INFO );
}
delete infile;
}
else
{
if( m_verbose )
{
msg.Printf( _( "Failed to archive file '%s'." ), currFilename );
m_reporter.Report( msg, RPT_SEVERITY_ERROR );
}
m_errorOccurred = true;
}
}
private:
wxZipOutputStream& m_zipFile;
wxString m_prjDir;
std::regex m_fileExtRegex;
REPORTER& m_reporter;
bool m_errorOccurred; // True if an error archiving the file
bool m_verbose; // True to enable verbose logging
// Keep track of how many bytes would have been used without compression
unsigned long m_uncompressedBytes = 0;
}; };
PROJECT_ARCHIVER::PROJECT_ARCHIVER() PROJECT_ARCHIVER::PROJECT_ARCHIVER()
{ {
} }
bool PROJECT_ARCHIVER::AreZipArchivesIdentical( const wxString& aZipFileA, bool PROJECT_ARCHIVER::AreZipArchivesIdentical( const wxString& aZipFileA,
const wxString& aZipFileB, REPORTER& aReporter ) const wxString& aZipFileB, REPORTER& aReporter )
{ {
@ -238,6 +165,7 @@ bool PROJECT_ARCHIVER::Unarchive( const wxString& aSrcFile, const wxString& aDes
// Now let's set the filetimes based on what's in the zip // Now let's set the filetimes based on what's in the zip
wxFileName outputFileName( fullname ); wxFileName outputFileName( fullname );
wxDateTime fileTime = entry->GetDateTime(); wxDateTime fileTime = entry->GetDateTime();
// For now we set access, mod, create to the same datetime // For now we set access, mod, create to the same datetime
// create (third arg) is only used on Windows // create (third arg) is only used on Windows
outputFileName.SetTimes( &fileTime, &fileTime, &fileTime ); outputFileName.SetTimes( &fileTime, &fileTime, &fileTime );
@ -252,74 +180,68 @@ bool PROJECT_ARCHIVER::Archive( const wxString& aSrcDir, const wxString& aDestFi
REPORTER& aReporter, bool aVerbose, bool aIncludeExtraFiles ) REPORTER& aReporter, bool aVerbose, bool aIncludeExtraFiles )
{ {
#define EXT( ext ) "\\." + ext + "|" std::set<wxString> extensions;
#define NAME( name ) name + "|" std::set<wxString> files; // File names without extensions such as fp-lib-table.
#define EXT_NO_PIPE( ext ) "\\." + ext
#define NAME_NO_PIPE( name ) name
// List of file extensions that are always archived extensions.emplace( FILEEXT::ProjectFileExtension );
std::string fileExtensionRegex = "(" extensions.emplace( FILEEXT::ProjectLocalSettingsFileExtension );
EXT( FILEEXT::ProjectFileExtension ) extensions.emplace( FILEEXT::KiCadSchematicFileExtension );
EXT( FILEEXT::ProjectLocalSettingsFileExtension ) extensions.emplace( FILEEXT::KiCadSymbolLibFileExtension );
EXT( FILEEXT::KiCadSchematicFileExtension ) extensions.emplace( FILEEXT::KiCadPcbFileExtension );
EXT( FILEEXT::KiCadSymbolLibFileExtension ) extensions.emplace( FILEEXT::KiCadFootprintFileExtension );
EXT( FILEEXT::KiCadPcbFileExtension ) extensions.emplace( FILEEXT::DesignRulesFileExtension );
EXT( FILEEXT::KiCadFootprintFileExtension ) extensions.emplace( FILEEXT::DrawingSheetFileExtension );
EXT( FILEEXT::DesignRulesFileExtension ) extensions.emplace( FILEEXT::KiCadJobSetFileExtension );
EXT( FILEEXT::DrawingSheetFileExtension ) extensions.emplace( FILEEXT::JsonFileExtension ); // for design blocks
EXT( FILEEXT::KiCadJobSetFileExtension ) extensions.emplace( FILEEXT::WorkbookFileExtension );
EXT( FILEEXT::JsonFileExtension ) // for design blocks
EXT( FILEEXT::WorkbookFileExtension ) + files.emplace( FILEEXT::FootprintLibraryTableFileName );
NAME( FILEEXT::FootprintLibraryTableFileName ) + files.emplace( FILEEXT::SymbolLibraryTableFileName );
NAME( FILEEXT::SymbolLibraryTableFileName ) + files.emplace( FILEEXT::DesignBlockLibraryTableFileName );
NAME_NO_PIPE( FILEEXT::DesignBlockLibraryTableFileName );
// List of additional file extensions that are only archived when aIncludeExtraFiles is true // List of additional file extensions that are only archived when aIncludeExtraFiles is true
if( aIncludeExtraFiles ) if( aIncludeExtraFiles )
{ {
fileExtensionRegex += "|" extensions.emplace( FILEEXT::LegacyProjectFileExtension );
EXT( FILEEXT::LegacyProjectFileExtension ) extensions.emplace( FILEEXT::LegacySchematicFileExtension );
EXT( FILEEXT::LegacySchematicFileExtension ) extensions.emplace( FILEEXT::LegacySymbolLibFileExtension );
EXT( FILEEXT::LegacySymbolLibFileExtension ) extensions.emplace( FILEEXT::LegacySymbolDocumentFileExtension );
EXT( FILEEXT::LegacySymbolDocumentFileExtension ) extensions.emplace( FILEEXT::FootprintAssignmentFileExtension );
EXT( FILEEXT::FootprintAssignmentFileExtension ) extensions.emplace( FILEEXT::LegacyPcbFileExtension );
EXT( FILEEXT::LegacyPcbFileExtension ) extensions.emplace( FILEEXT::LegacyFootprintLibPathExtension );
EXT( FILEEXT::LegacyFootprintLibPathExtension ) extensions.emplace( FILEEXT::StepFileAbrvExtension );
EXT( FILEEXT::StepFileAbrvExtension ) // 3d files extensions.emplace( FILEEXT::StepFileExtension ); // 3d files
EXT( FILEEXT::StepFileExtension ) // 3d files extensions.emplace( FILEEXT::VrmlFileExtension ); // 3d files
EXT( FILEEXT::VrmlFileExtension ) // 3d files extensions.emplace( FILEEXT::GerberJobFileExtension ); // Gerber job files
EXT( FILEEXT::GerberFileExtensionsRegex ) // Gerber files (g?, g??, .gm12 (from protel export)) extensions.emplace( FILEEXT::FootprintPlaceFileExtension ); // Our position files
EXT( FILEEXT::GerberJobFileExtension ) // Gerber job files extensions.emplace( FILEEXT::DrillFileExtension ); // Fab drill files
EXT( FILEEXT::FootprintPlaceFileExtension ) // Our position files extensions.emplace( "nc" ); // Fab drill files
EXT( FILEEXT::DrillFileExtension ) // Fab drill files extensions.emplace( "xnc" ); // Fab drill files
EXT( "nc" ) // Fab drill files extensions.emplace( FILEEXT::IpcD356FileExtension );
EXT( "xnc" ) // Fab drill files extensions.emplace( FILEEXT::ReportFileExtension );
EXT( FILEEXT::IpcD356FileExtension ) extensions.emplace( FILEEXT::NetlistFileExtension );
EXT( FILEEXT::ReportFileExtension ) extensions.emplace( FILEEXT::PythonFileExtension );
EXT( FILEEXT::NetlistFileExtension ) extensions.emplace( FILEEXT::PdfFileExtension );
EXT( FILEEXT::PythonFileExtension ) extensions.emplace( FILEEXT::TextFileExtension );
EXT( FILEEXT::PdfFileExtension ) extensions.emplace( FILEEXT::SpiceFileExtension ); // SPICE files
EXT( FILEEXT::TextFileExtension ) extensions.emplace( FILEEXT::SpiceSubcircuitFileExtension ); // SPICE files
EXT( FILEEXT::SpiceFileExtension ) // SPICE files extensions.emplace( FILEEXT::SpiceModelFileExtension ); // SPICE files
EXT( FILEEXT::SpiceSubcircuitFileExtension ) // SPICE files extensions.emplace( FILEEXT::IbisFileExtension );
EXT( FILEEXT::SpiceModelFileExtension ) // SPICE files extensions.emplace( "pkg" );
EXT_NO_PIPE( FILEEXT::IbisFileExtension ); extensions.emplace( FILEEXT::GencadFileExtension );
} }
fileExtensionRegex += ")"; // Gerber files (g?, g??, .gm12 (from protel export)).
wxRegEx gerberFiles( FILEEXT::GerberFileExtensionsRegex );
#undef EXT wxASSERT( gerberFiles.IsValid() );
#undef NAME
#undef EXT_NO_PIPE
#undef NAME_NO_PIPE
bool success = true; bool success = true;
wxString msg; wxString msg;
wxString oldCwd = wxGetCwd(); wxString oldCwd = wxGetCwd();
wxFileName sourceDir( aSrcDir ); wxFileName sourceDir( aSrcDir, wxEmptyString, wxEmptyString );
wxSetWorkingDirectory( sourceDir.GetFullPath() ); wxSetWorkingDirectory( aSrcDir );
wxFFileOutputStream ostream( aDestFile ); wxFFileOutputStream ostream( aDestFile );
@ -334,7 +256,6 @@ bool PROJECT_ARCHIVER::Archive( const wxString& aSrcDir, const wxString& aDestFi
wxDir projectDir( aSrcDir ); wxDir projectDir( aSrcDir );
wxString currFilename; wxString currFilename;
wxArrayString files;
if( !projectDir.IsOpened() ) if( !projectDir.IsOpened() )
{ {
@ -348,58 +269,92 @@ bool PROJECT_ARCHIVER::Archive( const wxString& aSrcDir, const wxString& aDestFi
return false; return false;
} }
try size_t uncompressedBytes = 0;
PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER traverser( aSrcDir );
projectDir.Traverse( traverser );
for( const wxString& fileName : traverser.GetFilesToArchive() )
{ {
PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER traverser( fileExtensionRegex, aSrcDir, zipstream, wxFileName fn( fileName );
aReporter, aVerbose ); wxString extLower = fn.GetExt().Lower();
wxString fileNameLower = fn.GetName().Lower();
bool archive = false;
projectDir.Traverse( traverser ); if( !extLower.IsEmpty() )
success = !traverser.GetErrorOccurred();
auto reportSize =
[]( unsigned long aSize ) -> wxString
{
constexpr float KB = 1024.0;
constexpr float MB = KB * 1024.0;
if( aSize >= MB )
return wxString::Format( wxT( "%0.2f MB" ), aSize / MB );
else if( aSize >= KB )
return wxString::Format( wxT( "%0.2f KB" ), aSize / KB );
else
return wxString::Format( wxT( "%lu bytes" ), aSize );
};
size_t zipBytesCnt = ostream.GetSize();
unsigned long uncompressedBytes = traverser.GetUncompressedBytes();
if( zipstream.Close() )
{ {
msg.Printf( _( "Zip archive '%s' created (%s uncompressed, %s compressed)." ), if( ( extensions.find( extLower ) != extensions.end() )
aDestFile, || ( aIncludeExtraFiles && gerberFiles.Matches( extLower ) ) )
reportSize( uncompressedBytes ), archive = true;
reportSize( zipBytesCnt ) ); }
aReporter.Report( msg, RPT_SEVERITY_INFO ); else if( !fileNameLower.IsEmpty() && ( files.find( fileNameLower ) != files.end() ) )
{
archive = true;
}
if( !archive )
continue;
wxFileSystem fsFile;
fn.MakeRelativeTo( aSrcDir );
wxString relativeFn = fn.GetFullPath();
// Read input file and add it to the zip file:
wxFSFile* infile = fsFile.OpenFile( relativeFn );
if( infile )
{
zipstream.PutNextEntry( relativeFn, infile->GetModificationTime() );
infile->GetStream()->Read( zipstream );
zipstream.CloseEntry();
uncompressedBytes += infile->GetStream()->GetSize();
if( aVerbose )
{
msg.Printf( _( "Archived file '%s'." ), relativeFn );
aReporter.Report( msg, RPT_SEVERITY_INFO );
}
} }
else else
{ {
msg.Printf( wxT( "Failed to create file '%s'." ), aDestFile ); if( aVerbose )
aReporter.Report( msg, RPT_SEVERITY_ERROR ); {
success = false; msg.Printf( _( "Failed to archive file '%s'." ), relativeFn );
aReporter.Report( msg, RPT_SEVERITY_ERROR );
}
} }
} }
catch( const std::regex_error& e )
auto reportSize =
[]( size_t aSize ) -> wxString
{
constexpr float KB = 1024.0;
constexpr float MB = KB * 1024.0;
if( aSize >= MB )
return wxString::Format( wxT( "%0.2f MB" ), aSize / MB );
else if( aSize >= KB )
return wxString::Format( wxT( "%0.2f KB" ), aSize / KB );
else
return wxString::Format( wxT( "%zu bytes" ), aSize );
};
size_t zipBytesCnt = ostream.GetSize();
if( zipstream.Close() )
{ {
// Something bad happened here with the regex msg.Printf( _( "Zip archive '%s' created (%s uncompressed, %s compressed)." ),
wxASSERT_MSG( false, e.what() ); aDestFile,
reportSize( uncompressedBytes ),
if( aVerbose ) reportSize( zipBytesCnt ) );
{ aReporter.Report( msg, RPT_SEVERITY_INFO );
msg.Printf( _( "Error: '%s'." ), e.what() ); }
aReporter.Report( msg, RPT_SEVERITY_ERROR ); else
} {
msg.Printf( wxT( "Failed to create file '%s'." ), aDestFile );
aReporter.Report( msg, RPT_SEVERITY_ERROR );
success = false; success = false;
} }

View File

@ -1349,7 +1349,7 @@ bool SETTINGS_MANAGER::TriggerBackupIfNeeded( REPORTER& aReporter ) const
return dt; return dt;
}; };
wxFileName projectPath( Prj().GetProjectPath() ); wxFileName projectPath( Prj().GetProjectPath(), wxEmptyString, wxEmptyString );
// Skip backup if project path isn't valid or writable // Skip backup if project path isn't valid or writable
if( !projectPath.IsOk() || !projectPath.Exists() || !projectPath.IsDirWritable() ) if( !projectPath.IsOk() || !projectPath.Exists() || !projectPath.IsDirWritable() )