Files
kicad-source-mirror/common/project/project_archiver.cpp
T
Seth Hillbrand e1c7d5714a PROJECT_ARCHIVER: exclude hidden dirs/files, fix size reporting and missing i18n
wxDIR_DEFAULT includes wxDIR_HIDDEN, so the traversal was entering hidden
directories (e.g. .git, .history) and including their files in project
backups/archives.  Pass wxDIR_FILES | wxDIR_DIRS explicitly to skip them.

Also fix two minor issues found in the same function.  First, zipBytesCnt
was read before zipstream.Close(), so the zip central directory bytes
were missing from the reported compressed size.  Also, the failure message
wasn't translated.
2026-02-24 22:12:24 -08:00

373 lines
13 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <memory>
#include <wx/dir.h>
#include <wx/filedlg.h>
#include <wx/fs_zip.h>
#include <wx/regex.h>
#include <wx/uri.h>
#include <wx/wfstream.h>
#include <wx/zipstrm.h>
#include <core/arraydim.h>
#include <macros.h>
#include <project/project_archiver.h>
#include <reporter.h>
#include <wildcards_and_files_ext.h>
#include <wxstream_helper.h>
#include <wx/log.h>
#include <kiplatform/io.h>
#include <regex>
#include <set>
#define ZipFileExtension wxT( "zip" )
class PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER : public wxDirTraverser
{
public:
PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER( const wxString& aPrjDir ) :
m_prjDir( aPrjDir )
{}
virtual wxDirTraverseResult OnFile( const wxString& aFilename ) override
{
m_files.emplace_back( aFilename );
return wxDIR_CONTINUE;
}
virtual wxDirTraverseResult OnDir( const wxString& aDirname ) override
{
return wxDIR_CONTINUE;
}
const std::vector<wxString>& GetFilesToArchive() const
{
return m_files;
}
private:
wxString m_prjDir;
std::vector<wxString> m_files;
};
PROJECT_ARCHIVER::PROJECT_ARCHIVER()
{
}
bool PROJECT_ARCHIVER::AreZipArchivesIdentical( const wxString& aZipFileA,
const wxString& aZipFileB, REPORTER& aReporter )
{
wxFFileInputStream streamA( aZipFileA );
wxFFileInputStream streamB( aZipFileB );
if( !streamA.IsOk() || !streamB.IsOk() )
{
aReporter.Report( _( "Could not open archive file." ), RPT_SEVERITY_ERROR );
return false;
}
wxZipInputStream zipStreamA = wxZipInputStream( streamA );
wxZipInputStream zipStreamB = wxZipInputStream( streamB );
std::set<wxUint32> crcsA;
std::set<wxUint32> crcsB;
for( wxZipEntry* entry = zipStreamA.GetNextEntry(); entry; entry = zipStreamA.GetNextEntry() )
{
crcsA.insert( entry->GetCrc() );
}
for( wxZipEntry* entry = zipStreamB.GetNextEntry(); entry; entry = zipStreamB.GetNextEntry() )
{
crcsB.insert( entry->GetCrc() );
}
return crcsA == crcsB;
}
// Unarchive Files code comes from wxWidgets sample/archive/archive.cpp
bool PROJECT_ARCHIVER::Unarchive( const wxString& aSrcFile, const wxString& aDestDir,
REPORTER& aReporter )
{
wxFFileInputStream stream( aSrcFile );
if( !stream.IsOk() )
{
aReporter.Report( _( "Could not open archive file." ), RPT_SEVERITY_ERROR );
return false;
}
const wxArchiveClassFactory* archiveClassFactory =
wxArchiveClassFactory::Find( aSrcFile, wxSTREAM_FILEEXT );
if( !archiveClassFactory )
{
aReporter.Report( _( "Invalid archive file format." ), RPT_SEVERITY_ERROR );
return false;
}
std::unique_ptr<wxArchiveInputStream> archiveStream( archiveClassFactory->NewStream( stream ) );
wxString fileStatus;
for( wxArchiveEntry* entry = archiveStream->GetNextEntry(); entry;
entry = archiveStream->GetNextEntry() )
{
fileStatus.Printf( _( "Extracting file '%s'." ), entry->GetName() );
aReporter.Report( fileStatus, RPT_SEVERITY_INFO );
wxString fullname = aDestDir + entry->GetName();
// Ensure the target directory exists and create it if not
wxString t_path = wxPathOnly( fullname );
if( !wxDirExists( t_path ) )
{
wxFileName::Mkdir( t_path, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
}
// Directory entries need only be created, not extracted (0 size)
if( entry->IsDir() )
continue;
wxTempFileOutputStream outputFileStream( fullname );
if( CopyStreamData( *archiveStream, outputFileStream, entry->GetSize() ) )
outputFileStream.Commit();
else
aReporter.Report( _( "Error extracting file!" ), RPT_SEVERITY_ERROR );
// Now let's set the filetimes based on what's in the zip
wxFileName outputFileName( fullname );
wxDateTime fileTime = entry->GetDateTime();
// For now we set access, mod, create to the same datetime
// create (third arg) is only used on Windows
outputFileName.SetTimes( &fileTime, &fileTime, &fileTime );
}
aReporter.Report( wxT( "Extracted project." ), RPT_SEVERITY_INFO );
return true;
}
bool PROJECT_ARCHIVER::Archive( const wxString& aSrcDir, const wxString& aDestFile,
REPORTER& aReporter, bool aVerbose, bool aIncludeExtraFiles )
{
std::set<wxString> extensions;
std::set<wxString> files; // File names without extensions such as fp-lib-table.
extensions.emplace( FILEEXT::ProjectFileExtension );
extensions.emplace( FILEEXT::ProjectLocalSettingsFileExtension );
extensions.emplace( FILEEXT::KiCadSchematicFileExtension );
extensions.emplace( FILEEXT::KiCadSymbolLibFileExtension );
extensions.emplace( FILEEXT::KiCadPcbFileExtension );
extensions.emplace( FILEEXT::KiCadFootprintFileExtension );
extensions.emplace( FILEEXT::DesignRulesFileExtension );
extensions.emplace( FILEEXT::DrawingSheetFileExtension );
extensions.emplace( FILEEXT::KiCadJobSetFileExtension );
extensions.emplace( FILEEXT::JsonFileExtension ); // for design blocks
extensions.emplace( FILEEXT::WorkbookFileExtension );
files.emplace( FILEEXT::FootprintLibraryTableFileName );
files.emplace( FILEEXT::SymbolLibraryTableFileName );
files.emplace( FILEEXT::DesignBlockLibraryTableFileName );
// List of additional file extensions that are only archived when aIncludeExtraFiles is true
if( aIncludeExtraFiles )
{
extensions.emplace( FILEEXT::LegacyProjectFileExtension );
extensions.emplace( FILEEXT::LegacySchematicFileExtension );
extensions.emplace( FILEEXT::LegacySymbolLibFileExtension );
extensions.emplace( FILEEXT::LegacySymbolDocumentFileExtension );
extensions.emplace( FILEEXT::FootprintAssignmentFileExtension );
extensions.emplace( FILEEXT::LegacyPcbFileExtension );
extensions.emplace( FILEEXT::LegacyFootprintLibPathExtension );
extensions.emplace( FILEEXT::StepFileAbrvExtension );
extensions.emplace( FILEEXT::StepFileExtension ); // 3d files
extensions.emplace( FILEEXT::VrmlFileExtension ); // 3d files
extensions.emplace( FILEEXT::GerberJobFileExtension ); // Gerber job files
extensions.emplace( FILEEXT::FootprintPlaceFileExtension ); // Our position files
extensions.emplace( FILEEXT::DrillFileExtension ); // Fab drill files
extensions.emplace( "nc" ); // Fab drill files
extensions.emplace( "xnc" ); // Fab drill files
extensions.emplace( FILEEXT::IpcD356FileExtension );
extensions.emplace( FILEEXT::ReportFileExtension );
extensions.emplace( FILEEXT::NetlistFileExtension );
extensions.emplace( FILEEXT::PythonFileExtension );
extensions.emplace( FILEEXT::PdfFileExtension );
extensions.emplace( FILEEXT::TextFileExtension );
extensions.emplace( FILEEXT::SpiceFileExtension ); // SPICE files
extensions.emplace( FILEEXT::SpiceSubcircuitFileExtension ); // SPICE files
extensions.emplace( FILEEXT::SpiceModelFileExtension ); // SPICE files
extensions.emplace( FILEEXT::IbisFileExtension );
extensions.emplace( "pkg" );
extensions.emplace( FILEEXT::GencadFileExtension );
}
// Gerber files (g?, g??, .gm12 (from protel export)).
wxRegEx gerberFiles( FILEEXT::GerberFileExtensionsRegex );
wxASSERT( gerberFiles.IsValid() );
bool success = true;
wxString msg;
wxString oldCwd = wxGetCwd();
wxFileName sourceDir( aSrcDir, wxEmptyString, wxEmptyString );
wxSetWorkingDirectory( aSrcDir );
wxFFileOutputStream ostream( aDestFile );
if( !ostream.IsOk() ) // issue to create the file. Perhaps not writable dir
{
msg.Printf( _( "Failed to create file '%s'." ), aDestFile );
aReporter.Report( msg, RPT_SEVERITY_ERROR );
return false;
}
// Use a large I/O buffer to improve compatibility with cloud-synced folders.
if( FILE* fp = ostream.GetFile()->fp() )
setvbuf( fp, nullptr, _IOFBF, KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE );
wxZipOutputStream zipstream( ostream, -1, wxConvUTF8 );
wxDir projectDir( aSrcDir );
if( !projectDir.IsOpened() )
{
if( aVerbose )
{
msg.Printf( _( "Error opening directory: '%s'." ), aSrcDir );
aReporter.Report( msg, RPT_SEVERITY_ERROR );
}
wxSetWorkingDirectory( oldCwd );
return false;
}
size_t uncompressedBytes = 0;
PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER traverser( aSrcDir );
// Do not include hidden directories (e.g. .git, .history) or files.
// wxDIR_DEFAULT includes wxDIR_HIDDEN, so specify flags explicitly.
projectDir.Traverse( traverser, wxEmptyString, wxDIR_FILES | wxDIR_DIRS );
for( const wxString& fileName : traverser.GetFilesToArchive() )
{
wxFileName fn( fileName );
wxString extLower = fn.GetExt().Lower();
wxString fileNameLower = fn.GetName().Lower();
bool archive = false;
if( !extLower.IsEmpty() )
{
if( ( extensions.find( extLower ) != extensions.end() )
|| ( aIncludeExtraFiles && gerberFiles.Matches( extLower ) ) )
archive = true;
}
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 );
}
delete infile;
}
else
{
if( aVerbose )
{
msg.Printf( _( "Failed to archive file '%s'." ), relativeFn );
aReporter.Report( msg, RPT_SEVERITY_ERROR );
}
}
}
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 );
};
if( zipstream.Close() )
{
// Read the final compressed size after Close() so the zip central directory
// bytes are included in the count.
size_t zipBytesCnt = ostream.GetSize();
msg.Printf( _( "Zip archive '%s' created (%s uncompressed, %s compressed)." ),
aDestFile,
reportSize( uncompressedBytes ),
reportSize( zipBytesCnt ) );
aReporter.Report( msg, RPT_SEVERITY_INFO );
}
else
{
msg.Printf( _( "Failed to create file '%s'." ), aDestFile );
aReporter.Report( msg, RPT_SEVERITY_ERROR );
success = false;
}
wxSetWorkingDirectory( oldCwd );
return success;
}