KiCad PCB EDA Suite
Loading...
Searching...
No Matches
project_archiver.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <memory>
21#include <wx/dir.h>
22#include <wx/filedlg.h>
23#include <wx/fs_zip.h>
24#include <wx/regex.h>
25#include <wx/uri.h>
26#include <wx/wfstream.h>
27#include <wx/zipstrm.h>
28
29#include <core/arraydim.h>
30#include <macros.h>
32#include <reporter.h>
34#include <wxstream_helper.h>
35#include <wx_filename.h>
36#include <wx/log.h>
37#include <kiplatform/io.h>
38
39#include <regex>
40#include <set>
41
42
43#define ZipFileExtension wxT( "zip" )
44
45class PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER : public wxDirTraverser
46{
47public:
48 PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER( const wxString& aPrjDir ) :
49 m_prjDir( aPrjDir )
50 {}
51
52 virtual wxDirTraverseResult OnFile( const wxString& aFilename ) override
53 {
54 m_files.emplace_back( aFilename );
55
56 return wxDIR_CONTINUE;
57 }
58
59 virtual wxDirTraverseResult OnDir( const wxString& aDirname ) override
60 {
61 return wxDIR_CONTINUE;
62 }
63
64 const std::vector<wxString>& GetFilesToArchive() const
65 {
66 return m_files;
67 }
68
69private:
70 wxString m_prjDir;
71 std::vector<wxString> m_files;
72};
73
74
78
79
80bool PROJECT_ARCHIVER::AreZipArchivesIdentical( const wxString& aZipFileA,
81 const wxString& aZipFileB, REPORTER& aReporter )
82{
83 wxFFileInputStream streamA( aZipFileA );
84 wxFFileInputStream streamB( aZipFileB );
85
86 if( !streamA.IsOk() || !streamB.IsOk() )
87 {
88 aReporter.Report( _( "Could not open archive file." ), RPT_SEVERITY_ERROR );
89 return false;
90 }
91
92 wxZipInputStream zipStreamA = wxZipInputStream( streamA );
93 wxZipInputStream zipStreamB = wxZipInputStream( streamB );
94
95 std::set<wxUint32> crcsA;
96 std::set<wxUint32> crcsB;
97
98
99 for( wxZipEntry* entry = zipStreamA.GetNextEntry(); entry; entry = zipStreamA.GetNextEntry() )
100 {
101 crcsA.insert( entry->GetCrc() );
102 }
103
104 for( wxZipEntry* entry = zipStreamB.GetNextEntry(); entry; entry = zipStreamB.GetNextEntry() )
105 {
106 crcsB.insert( entry->GetCrc() );
107 }
108
109 return crcsA == crcsB;
110}
111
112
113// Unarchive Files code comes from wxWidgets sample/archive/archive.cpp
114bool PROJECT_ARCHIVER::Unarchive( const wxString& aSrcFile, const wxString& aDestDir,
115 REPORTER& aReporter )
116{
117 wxFFileInputStream stream( aSrcFile );
118
119 if( !stream.IsOk() )
120 {
121 aReporter.Report( _( "Could not open archive file." ), RPT_SEVERITY_ERROR );
122 return false;
123 }
124
125 const wxArchiveClassFactory* archiveClassFactory =
126 wxArchiveClassFactory::Find( aSrcFile, wxSTREAM_FILEEXT );
127
128 if( !archiveClassFactory )
129 {
130 aReporter.Report( _( "Invalid archive file format." ), RPT_SEVERITY_ERROR );
131 return false;
132 }
133
134 std::unique_ptr<wxArchiveInputStream> archiveStream( archiveClassFactory->NewStream( stream ) );
135
136 wxString fileStatus;
137
138 for( wxArchiveEntry* entry = archiveStream->GetNextEntry(); entry;
139 entry = archiveStream->GetNextEntry() )
140 {
141 fileStatus.Printf( _( "Extracting file '%s'." ), entry->GetName() );
142 aReporter.Report( fileStatus, RPT_SEVERITY_INFO );
143
144 // Now validate the entry name in the archive isn't trying to escape our destination path
145 wxFileName target;
146
147 if( !WX_FILENAME::ResolveArchiveEntryPath( aDestDir, entry->GetName(), target ) )
148 {
149 aReporter.Report( wxString::Format( _( "Refusing to extract file '%s': the archive "
150 "entry points outside of the destination "
151 "directory." ),
152 entry->GetName() ),
154 return false;
155 }
156
157 wxString fullname = target.GetFullPath();
158 const bool isDir = entry->IsDir();
159
160 // Ensure the target directory exists and create it if not.
161 wxString t_path = isDir ? fullname : wxPathOnly( fullname );
162
163 if( !wxDirExists( t_path ) )
164 {
165 wxFileName::Mkdir( t_path, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
166 }
167
168 // Directory entries need only be created, not extracted (0 size)
169 if( isDir )
170 continue;
171
172 wxTempFileOutputStream outputFileStream( fullname );
173
174 if( CopyStreamData( *archiveStream, outputFileStream, entry->GetSize() ) )
175 outputFileStream.Commit();
176 else
177 aReporter.Report( _( "Error extracting file!" ), RPT_SEVERITY_ERROR );
178
179 // Now let's set the filetimes based on what's in the zip
180 wxFileName outputFileName( fullname );
181 wxDateTime fileTime = entry->GetDateTime();
182
183 // For now we set access, mod, create to the same datetime
184 // create (third arg) is only used on Windows
185 outputFileName.SetTimes( &fileTime, &fileTime, &fileTime );
186 }
187
188 aReporter.Report( wxT( "Extracted project." ), RPT_SEVERITY_INFO );
189 return true;
190}
191
192
193bool PROJECT_ARCHIVER::Archive( const wxString& aSrcDir, const wxString& aDestFile,
194 REPORTER& aReporter, bool aVerbose, bool aIncludeExtraFiles )
195{
196
197 std::set<wxString> extensions;
198 std::set<wxString> files; // File names without extensions such as fp-lib-table.
199
200 extensions.emplace( FILEEXT::ProjectFileExtension );
201 extensions.emplace( FILEEXT::ProjectLocalSettingsFileExtension );
202 extensions.emplace( FILEEXT::KiCadSchematicFileExtension );
203 extensions.emplace( FILEEXT::KiCadSymbolLibFileExtension );
204 extensions.emplace( FILEEXT::KiCadPcbFileExtension );
205 extensions.emplace( FILEEXT::KiCadFootprintFileExtension );
206 extensions.emplace( FILEEXT::DesignRulesFileExtension );
207 extensions.emplace( FILEEXT::DrawingSheetFileExtension );
208 extensions.emplace( FILEEXT::KiCadJobSetFileExtension );
209 extensions.emplace( FILEEXT::JsonFileExtension ); // for design blocks
210 extensions.emplace( FILEEXT::WorkbookFileExtension );
211
215
216 // List of additional file extensions that are only archived when aIncludeExtraFiles is true
217 if( aIncludeExtraFiles )
218 {
219 extensions.emplace( FILEEXT::LegacyProjectFileExtension );
220 extensions.emplace( FILEEXT::LegacySchematicFileExtension );
221 extensions.emplace( FILEEXT::LegacySymbolLibFileExtension );
222 extensions.emplace( FILEEXT::LegacySymbolDocumentFileExtension );
223 extensions.emplace( FILEEXT::FootprintAssignmentFileExtension );
224 extensions.emplace( FILEEXT::LegacyPcbFileExtension );
225 extensions.emplace( FILEEXT::LegacyFootprintLibPathExtension );
226 extensions.emplace( FILEEXT::StepFileAbrvExtension );
227 extensions.emplace( FILEEXT::StepFileExtension ); // 3d files
228 extensions.emplace( FILEEXT::VrmlFileExtension ); // 3d files
229 extensions.emplace( FILEEXT::GerberJobFileExtension ); // Gerber job files
230 extensions.emplace( FILEEXT::FootprintPlaceFileExtension ); // Our position files
231 extensions.emplace( FILEEXT::DrillFileExtension ); // Fab drill files
232 extensions.emplace( "nc" ); // Fab drill files
233 extensions.emplace( "xnc" ); // Fab drill files
234 extensions.emplace( FILEEXT::IpcD356FileExtension );
235 extensions.emplace( FILEEXT::ReportFileExtension );
236 extensions.emplace( FILEEXT::NetlistFileExtension );
237 extensions.emplace( FILEEXT::PythonFileExtension );
238 extensions.emplace( FILEEXT::PdfFileExtension );
239 extensions.emplace( FILEEXT::TextFileExtension );
240 extensions.emplace( FILEEXT::SpiceFileExtension ); // SPICE files
241 extensions.emplace( FILEEXT::SpiceSubcircuitFileExtension ); // SPICE files
242 extensions.emplace( FILEEXT::SpiceModelFileExtension ); // SPICE files
243 extensions.emplace( FILEEXT::IbisFileExtension );
244 extensions.emplace( "pkg" );
245 extensions.emplace( FILEEXT::GencadFileExtension );
246 }
247
248 // Gerber files (g?, g??, .gm12 (from protel export)).
249 wxRegEx gerberFiles( FILEEXT::GerberFileExtensionsRegex );
250 wxASSERT( gerberFiles.IsValid() );
251
252 bool success = true;
253 wxString msg;
254 wxString oldCwd = wxGetCwd();
255
256 wxFileName sourceDir( aSrcDir, wxEmptyString, wxEmptyString );
257
258 wxSetWorkingDirectory( aSrcDir );
259
260 wxFFileOutputStream ostream( aDestFile );
261
262 if( !ostream.IsOk() ) // issue to create the file. Perhaps not writable dir
263 {
264 msg.Printf( _( "Failed to create file '%s'." ), aDestFile );
265 aReporter.Report( msg, RPT_SEVERITY_ERROR );
266 return false;
267 }
268
269 // Use a large I/O buffer to improve compatibility with cloud-synced folders.
270 if( FILE* fp = ostream.GetFile()->fp() )
271 setvbuf( fp, nullptr, _IOFBF, KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE );
272
273 wxZipOutputStream zipstream( ostream, -1, wxConvUTF8 );
274
275 wxDir projectDir( aSrcDir );
276
277 if( !projectDir.IsOpened() )
278 {
279 msg.Printf( _( "Error opening directory: '%s'." ), aSrcDir );
280 aReporter.Report( msg, RPT_SEVERITY_ERROR );
281
282 wxSetWorkingDirectory( oldCwd );
283 return false;
284 }
285
286 size_t uncompressedBytes = 0;
287 PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER traverser( aSrcDir );
288
289 // Do not include hidden directories (e.g. .git, .history) or files.
290 // wxDIR_DEFAULT includes wxDIR_HIDDEN, so specify flags explicitly.
291 projectDir.Traverse( traverser, wxEmptyString, wxDIR_FILES | wxDIR_DIRS );
292
293 for( const wxString& fileName : traverser.GetFilesToArchive() )
294 {
295 wxFileName fn( fileName );
296 wxString extLower = fn.GetExt().Lower();
297 wxString fileNameLower = fn.GetName().Lower();
298 bool archive = false;
299
300 if( !extLower.IsEmpty() )
301 {
302 if( ( extensions.find( extLower ) != extensions.end() )
303 || ( aIncludeExtraFiles && gerberFiles.Matches( extLower ) ) )
304 archive = true;
305 }
306 else if( !fileNameLower.IsEmpty() && ( files.find( fileNameLower ) != files.end() ) )
307 {
308 archive = true;
309 }
310
311 if( !archive )
312 continue;
313
314 wxFileSystem fsFile;
315 fn.MakeRelativeTo( aSrcDir );
316
317 wxString relativeFn = fn.GetFullPath();
318
319 // Read input file and add it to the zip file:
320 wxFSFile* infile = nullptr;
321 wxString sysError;
322
323 {
324 // Failures are reported through aReporter, wx would also pop its own dialog.
325 wxLogNull suppressSysErrorPopups;
326 infile = fsFile.OpenFile( relativeFn );
327
328 if( !infile )
329 {
330 if( unsigned long code = wxSysErrorCode() )
331 sysError = wxSysErrorMsgStr( code );
332 }
333 }
334
335 if( infile )
336 {
337 zipstream.PutNextEntry( relativeFn, infile->GetModificationTime() );
338 infile->GetStream()->Read( zipstream );
339 zipstream.CloseEntry();
340
341 uncompressedBytes += infile->GetStream()->GetSize();
342
343 if( aVerbose )
344 {
345 msg.Printf( _( "Archived file '%s'." ), relativeFn );
346 aReporter.Report( msg, RPT_SEVERITY_INFO );
347 }
348
349 delete infile;
350 }
351 else
352 {
353 if( sysError.IsEmpty() )
354 msg.Printf( _( "Failed to archive file '%s'." ), relativeFn );
355 else
356 msg.Printf( _( "Failed to archive file '%s': %s" ), relativeFn, sysError );
357
358 aReporter.Report( msg, RPT_SEVERITY_ERROR );
359 }
360 }
361
362 auto reportSize =
363 []( size_t aSize ) -> wxString
364 {
365 constexpr float KB = 1024.0;
366 constexpr float MB = KB * 1024.0;
367
368 if( aSize >= MB )
369 return wxString::Format( wxT( "%0.2f MB" ), aSize / MB );
370 else if( aSize >= KB )
371 return wxString::Format( wxT( "%0.2f KB" ), aSize / KB );
372 else
373 return wxString::Format( wxT( "%zu bytes" ), aSize );
374 };
375
376 if( zipstream.Close() )
377 {
378 // Read the final compressed size after Close() so the zip central directory
379 // bytes are included in the count.
380 size_t zipBytesCnt = ostream.GetSize();
381
382 if( aVerbose )
383 {
384 msg.Printf( _( "Zip archive '%s' created (%s uncompressed, %s compressed)." ), aDestFile,
385 reportSize( uncompressedBytes ), reportSize( zipBytesCnt ) );
386 aReporter.Report( msg, RPT_SEVERITY_INFO );
387 }
388 }
389 else
390 {
391 msg.Printf( _( "Failed to create file '%s'." ), aDestFile );
392 aReporter.Report( msg, RPT_SEVERITY_ERROR );
393 success = false;
394 }
395
396 wxSetWorkingDirectory( oldCwd );
397 return success;
398}
const std::vector< wxString > & GetFilesToArchive() const
virtual wxDirTraverseResult OnDir(const wxString &aDirname) override
virtual wxDirTraverseResult OnFile(const wxString &aFilename) override
PROJECT_ARCHIVER_DIR_ZIP_TRAVERSER(const wxString &aPrjDir)
static bool Archive(const wxString &aSrcDir, const wxString &aDestFile, REPORTER &aReporter, bool aVerbose=true, bool aIncludeExtraFiles=false)
Create an archive of the project.
static bool Unarchive(const wxString &aSrcFile, const wxString &aDestDir, REPORTER &aReporter)
Extract an archive of the current project over existing files.
static bool AreZipArchivesIdentical(const wxString &aZipFileA, const wxString &aZipFileB, REPORTER &aReporter)
Compare the CRCs of all the files in zip archive to determine whether the archives are identical.
A pure virtual class used to derive REPORTER objects from.
Definition reporter.h:73
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition reporter.h:102
static bool ResolveArchiveEntryPath(const wxString &aDestDir, const wxString &aEntryName, wxFileName &aResult)
Resolve an untrusted archive entry name against the directory it is extracted into.
#define _(s)
static const std::string LegacySchematicFileExtension
static const wxString GerberFileExtensionsRegex
static const std::string NetlistFileExtension
static const std::string SymbolLibraryTableFileName
static const std::string GerberJobFileExtension
static const std::string StepFileAbrvExtension
static const std::string WorkbookFileExtension
static const std::string ReportFileExtension
static const std::string ProjectFileExtension
static const std::string FootprintPlaceFileExtension
static const std::string JsonFileExtension
static const std::string LegacyPcbFileExtension
static const std::string LegacyProjectFileExtension
static const std::string ProjectLocalSettingsFileExtension
static const std::string KiCadSchematicFileExtension
static const std::string LegacySymbolLibFileExtension
static const std::string DesignBlockLibraryTableFileName
static const std::string KiCadSymbolLibFileExtension
static const std::string SpiceFileExtension
static const std::string PdfFileExtension
static const std::string TextFileExtension
static const std::string FootprintLibraryTableFileName
static const std::string GencadFileExtension
static const std::string DrawingSheetFileExtension
static const std::string IbisFileExtension
static const std::string IpcD356FileExtension
static const std::string KiCadJobSetFileExtension
static const std::string LegacyFootprintLibPathExtension
static const std::string PythonFileExtension
static const std::string StepFileExtension
static const std::string LegacySymbolDocumentFileExtension
static const std::string FootprintAssignmentFileExtension
static const std::string DrillFileExtension
static const std::string SpiceSubcircuitFileExtension
static const std::string SpiceModelFileExtension
static const std::string DesignRulesFileExtension
static const std::string VrmlFileExtension
static const std::string KiCadFootprintFileExtension
static const std::string KiCadPcbFileExtension
This file contains miscellaneous commonly used macros and functions.
static constexpr size_t CLOUD_SYNC_BUFFER_SIZE
Buffer size for file I/O operations on cloud-synced folders.
Definition io.h:169
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_INFO
static const size_t MB
Tests for the predictive GPU buffer resize strategy guard that keeps a large-board defragmentResize()...
Definition of file extensions used in Kicad.
static bool CopyStreamData(wxInputStream &inputStream, wxOutputStream &outputStream, wxFileOffset size)