KiCad PCB EDA Suite
pcm_task_manager.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 (C) 2021 Andrew Lutsenko, anlutsenko at gmail dot com
5 * Copyright (C) 1992-2021 KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20// kicad_curl_easy.h **must be** included before any wxWidgets header to avoid conflicts
21// at least on Windows/msys2
24
25#include <paths.h>
26#include "pcm_task_manager.h"
27#include <reporter.h>
28#include <wxstream_helper.h>
29
30#include <fstream>
31#include <thread>
32#include <unordered_set>
33#include <wx/dir.h>
34#include <wx/filename.h>
35#include <wx/msgdlg.h>
36#include <wx/sstream.h>
37#include <wx/wfstream.h>
38#include <wx/zipstrm.h>
39
40
42 std::forward_list<wxRegEx>& aKeepOnUpdate )
43{
44 auto compile_regex = [&]( const wxString& regex )
45 {
46 aKeepOnUpdate.emplace_front( regex, wxRE_DEFAULT );
47
48 if( !aKeepOnUpdate.front().IsValid() )
49 aKeepOnUpdate.pop_front();
50 };
51
52 std::for_each( pkg.keep_on_update.begin(), pkg.keep_on_update.end(), compile_regex );
53 std::for_each( ver.keep_on_update.begin(), ver.keep_on_update.end(), compile_regex );
54}
55
56
57void PCM_TASK_MANAGER::DownloadAndInstall( const PCM_PACKAGE& aPackage, const wxString& aVersion,
58 const wxString& aRepositoryId, const bool isUpdate )
59{
60 PCM_TASK download_task = [aPackage, aVersion, aRepositoryId, isUpdate, this]()
61 {
62 wxFileName file_path( PATHS::GetUserCachePath(), "" );
63 file_path.AppendDir( "pcm" );
64 file_path.SetFullName( wxString::Format( "%s_v%s.zip", aPackage.identifier, aVersion ) );
65
66 auto find_pkgver = std::find_if( aPackage.versions.begin(), aPackage.versions.end(),
67 [&aVersion]( const PACKAGE_VERSION& pv )
68 {
69 return pv.version == aVersion;
70 } );
71
72 if( find_pkgver == aPackage.versions.end() )
73 {
74 m_reporter->PCMReport( wxString::Format( _( "Version %s of package %s not found!" ),
75 aVersion, aPackage.identifier ),
77 return;
78 }
79
80 if( !wxDirExists( file_path.GetPath() )
81 && !wxFileName::Mkdir( file_path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
82 {
83 m_reporter->PCMReport( _( "Unable to create download directory!" ),
85 return;
86 }
87
88 int code = downloadFile( file_path.GetFullPath(), *find_pkgver->download_url );
89
90 if( code != CURLE_OK )
91 {
92 // Cleanup after ourselves and exit
93 wxRemoveFile( file_path.GetFullPath() );
94 return;
95 }
96
97 PCM_TASK install_task = [aPackage, aVersion, aRepositoryId, file_path, isUpdate, this]()
98 {
99 installDownloadedPackage( aPackage, aVersion, aRepositoryId, file_path, isUpdate );
100 };
101
102 m_install_queue.push( install_task );
103 };
104
105 m_download_queue.push( download_task );
106}
107
108
109int PCM_TASK_MANAGER::downloadFile( const wxString& aFilePath, const wxString& url )
110{
111 TRANSFER_CALLBACK callback = [&]( size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow )
112 {
113 if( dltotal > 1024 )
114 m_reporter->SetDownloadProgress( dlnow, dltotal );
115 else
116 m_reporter->SetDownloadProgress( 0.0, 0.0 );
117
118 return m_reporter->IsCancelled();
119 };
120
121 std::ofstream out( aFilePath.ToUTF8(), std::ofstream::binary );
122
123 KICAD_CURL_EASY curl;
124 curl.SetOutputStream( &out );
125 curl.SetURL( url.ToUTF8().data() );
126 curl.SetFollowRedirects( true );
127 curl.SetTransferCallback( callback, 250000L );
128
129 m_reporter->PCMReport( wxString::Format( _( "Downloading package url: '%s'" ), url ),
131
132 int code = curl.Perform();
133
134 out.close();
135
136 uint64_t download_total;
137
138 if( CURLE_OK == curl.GetTransferTotal( download_total ) )
139 m_reporter->SetDownloadProgress( download_total, download_total );
140
141 if( code != CURLE_OK && code != CURLE_ABORTED_BY_CALLBACK )
142 {
143 m_reporter->PCMReport( wxString::Format( _( "Failed to download url %s\n%s" ), url,
144 curl.GetErrorText( code ) ),
146 }
147
148 return code;
149}
150
151
153 const wxString& aVersion,
154 const wxString& aRepositoryId,
155 const wxFileName& aFilePath, const bool isUpdate )
156{
157 auto pkgver = std::find_if( aPackage.versions.begin(), aPackage.versions.end(),
158 [&aVersion]( const PACKAGE_VERSION& pv )
159 {
160 return pv.version == aVersion;
161 } );
162
163 if( pkgver == aPackage.versions.end() )
164 {
165 m_reporter->PCMReport( wxString::Format( _( "Version %s of package %s not found!" ),
166 aVersion, aPackage.identifier ),
168 return;
169 }
170
171 // wxRegEx is not CopyConstructible hence the weird choice of forward_list
172 std::forward_list<wxRegEx> keep_on_update;
173
174 if( isUpdate )
175 compile_keep_on_update_regex( aPackage, *pkgver, keep_on_update );
176
177 const std::optional<wxString>& hash = pkgver->download_sha256;
178 bool hash_match = true;
179
180 if( hash )
181 {
182 std::ifstream stream( aFilePath.GetFullPath().ToUTF8(), std::ios::binary );
183 hash_match = m_pcm->VerifyHash( stream, *hash );
184 }
185
186 if( !hash_match )
187 {
188 m_reporter->PCMReport( wxString::Format( _( "Downloaded archive hash for package "
189 "%s does not match repository entry. "
190 "This may indicate a problem with the "
191 "package, if the issue persists "
192 "report this to repository maintainers." ),
193 aPackage.identifier ),
195 }
196 else
197 {
198 if( isUpdate )
199 {
200 m_reporter->PCMReport(
201 wxString::Format( _( "Removing previous version of package '%s'." ),
202 aPackage.identifier ),
204
205 deletePackageDirectories( aPackage.identifier, keep_on_update );
206 }
207
208 m_reporter->PCMReport(
209 wxString::Format( _( "Extracting package '%s'." ), aPackage.identifier ),
211
212 if( extract( aFilePath.GetFullPath(), aPackage.identifier, true ) )
213 {
214 m_pcm->MarkInstalled( aPackage, pkgver->version, aRepositoryId );
215 // TODO register libraries.
216 }
217 else
218 {
219 // Cleanup possibly partially extracted package
221 }
222 }
223
224 if( aPackage.type == PCM_PACKAGE_TYPE::PT_COLORTHEME )
225 m_color_themes_changed.store( true );
226
227 m_reporter->PCMReport(
228 wxString::Format( _( "Removing downloaded archive '%s'." ), aFilePath.GetFullName() ),
230
231 wxRemoveFile( aFilePath.GetFullPath() );
232}
233
234
235bool PCM_TASK_MANAGER::extract( const wxString& aFilePath, const wxString& aPackageId,
236 bool isMultiThreaded )
237{
238 wxFFileInputStream stream( aFilePath );
239 wxZipInputStream zip( stream );
240
241 wxLogNull no_wx_logging;
242
243 int entries = zip.GetTotalEntries();
244 int extracted = 0;
245
246 wxArchiveEntry* entry = zip.GetNextEntry();
247
248 if( !zip.IsOk() )
249 {
250 m_reporter->PCMReport( _( "Error extracting file!" ), RPT_SEVERITY_ERROR );
251 return false;
252 }
253
254 // Namespace delimiter changed on disk to allow flat loading of Python modules
255 wxString clean_package_id = aPackageId;
256 clean_package_id.Replace( '.', '_' );
257
258 for( ; entry; entry = zip.GetNextEntry() )
259 {
260 wxArrayString path_parts =
261 wxSplit( entry->GetName(), wxFileName::GetPathSeparator(), (wxChar) 0 );
262
263 if( path_parts.size() < 2
264 || PCM_PACKAGE_DIRECTORIES.find( path_parts[0] ) == PCM_PACKAGE_DIRECTORIES.end()
265 || path_parts[path_parts.size() - 1].IsEmpty() )
266 {
267 // Ignore files in the root of the archive and files outside of package dirs.
268 continue;
269 }
270
271 // m_reporter->Report( wxString::Format( _( "Extracting file '%s'\n" ), entry->GetName() ),
272 // RPT_SEVERITY_INFO );
273
274 // Transform paths from
275 // <PackageRoot>/$folder/$contents
276 // To
277 // $KICAD6_3RD_PARTY/$folder/$package_id/$contents
278 path_parts.Insert( clean_package_id, 1 );
279 path_parts.Insert( m_pcm->Get3rdPartyPath(), 0 );
280
281 wxString fullname = wxJoin( path_parts, wxFileName::GetPathSeparator(), (wxChar) 0 );
282
283 // Ensure the target directory exists and create it if not.
284 wxString t_path = wxPathOnly( fullname );
285
286 if( !wxDirExists( t_path ) )
287 {
288 wxFileName::Mkdir( t_path, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
289 }
290
291 wxTempFileOutputStream out( fullname );
292
293 if( !( CopyStreamData( zip, out, entry->GetSize() ) && out.Commit() ) )
294 {
295 m_reporter->PCMReport( _( "Error extracting file!" ), RPT_SEVERITY_ERROR );
296 return false;
297 }
298
299 extracted++;
300 m_reporter->SetPackageProgress( extracted, entries );
301
302 if( !isMultiThreaded )
303 m_reporter->KeepRefreshing( false );
304
305 if( m_reporter->IsCancelled() )
306 break;
307 }
308
309 zip.CloseEntry();
310
311 if( m_reporter->IsCancelled() )
312 {
313 m_reporter->PCMReport( _( "Aborting package installation." ), RPT_SEVERITY_INFO );
314 return false;
315 }
316
317 m_reporter->PCMReport( _( "Extracted package\n" ), RPT_SEVERITY_INFO );
318 m_reporter->SetPackageProgress( entries, entries );
319
320 return true;
321}
322
323
324void PCM_TASK_MANAGER::InstallFromFile( wxWindow* aParent, const wxString& aFilePath )
325{
326 wxFFileInputStream stream( aFilePath );
327
328 if( !stream.IsOk() )
329 {
330 wxLogError( _( "Could not open archive file." ) );
331 return;
332 }
333
334 wxZipInputStream zip( stream );
335
336 if( !zip.IsOk() )
337 {
338 wxLogError( _( "Invalid archive file format." ) );
339 return;
340 }
341
342 nlohmann::json metadata;
343
344 for( wxArchiveEntry* entry = zip.GetNextEntry(); entry != nullptr; entry = zip.GetNextEntry() )
345 {
346 // Find and load metadata.json
347 if( entry->GetName() != "metadata.json" )
348 continue;
349
350 wxStringOutputStream strStream;
351
352 if( CopyStreamData( zip, strStream, entry->GetSize() ) )
353 {
354 try
355 {
356 metadata = nlohmann::json::parse( strStream.GetString().ToUTF8().data() );
357 m_pcm->ValidateJson( metadata );
358 }
359 catch( const std::exception& e )
360 {
361 wxLogError( wxString::Format( _( "Unable to parse package metadata:\n\n%s" ),
362 e.what() ) );
363 break;
364 }
365 }
366 }
367
368 if( metadata.empty() )
369 {
370 wxLogError( _( "Archive does not contain a valid metadata.json file" ) );
371 return;
372 }
373
374 PCM_PACKAGE package = metadata.get<PCM_PACKAGE>();
375
376 if( package.versions.size() != 1 )
377 {
378 wxLogError( _( "Archive metadata must have a single version defined" ) );
379 return;
380 }
381
382 bool isUpdate = false;
383 // wxRegEx is not CopyConstructible hence the weird choice of forward_list
384 std::forward_list<wxRegEx> keep_on_update;
385 const std::vector<PCM_INSTALLATION_ENTRY> installed_packages = m_pcm->GetInstalledPackages();
386
387 if( std::find_if( installed_packages.begin(), installed_packages.end(),
388 [&]( const PCM_INSTALLATION_ENTRY& entry )
389 {
390 return entry.package.identifier == package.identifier;
391 } )
392 != installed_packages.end() )
393 {
394 if( wxMessageBox(
396 _( "Package with identifier %s is already installed. "
397 "Would you like to update it to the version from selected file?" ),
398 package.identifier ),
399 _( "Update package" ), wxICON_EXCLAMATION | wxYES_NO, aParent )
400 == wxNO )
401 return;
402
403 isUpdate = true;
404
405 compile_keep_on_update_regex( package, package.versions[0], keep_on_update );
406 }
407
408 m_reporter = std::make_unique<DIALOG_PCM_PROGRESS>( aParent, false );
409 m_reporter->Show();
410
411 if( isUpdate )
412 {
413 m_reporter->PCMReport( wxString::Format( _( "Removing previous version of package '%s'." ),
414 package.identifier ),
416
417 deletePackageDirectories( package.identifier, keep_on_update );
418 }
419
420 if( extract( aFilePath, package.identifier, false ) )
421 m_pcm->MarkInstalled( package, package.versions[0].version, "" );
422
423 m_reporter->SetFinished();
424 m_reporter->KeepRefreshing( false );
425 m_reporter->Destroy();
426 m_reporter.reset();
427
429}
430
431
432class PATH_COLLECTOR : public wxDirTraverser
433{
434private:
435 std::vector<wxString>& m_files;
436 std::vector<wxString>& m_dirs;
437
438public:
439 explicit PATH_COLLECTOR( std::vector<wxString>& aFiles, std::vector<wxString>& aDirs ) :
440 m_files( aFiles ), m_dirs( aDirs )
441 {
442 }
443
444 wxDirTraverseResult OnFile( const wxString& aFilePath ) override
445 {
446 m_files.push_back( aFilePath );
447 return wxDIR_CONTINUE;
448 }
449
450 wxDirTraverseResult OnDir( const wxString& dirPath ) override
451 {
452 m_dirs.push_back( dirPath );
453 return wxDIR_CONTINUE;
454 }
455};
456
457
458void PCM_TASK_MANAGER::deletePackageDirectories( const wxString& aPackageId,
459 const std::forward_list<wxRegEx>& aKeep )
460{
461 // Namespace delimiter changed on disk to allow flat loading of Python modules
462 wxString clean_package_id = aPackageId;
463 clean_package_id.Replace( '.', '_' );
464
465 int path_prefix_len = m_pcm->Get3rdPartyPath().Length();
466
467 auto sort_func = []( const wxString& a, const wxString& b )
468 {
469 if( a.length() > b.length() )
470 return true;
471 if( a.length() < b.length() )
472 return false;
473
474 if( a != b )
475 return a < b;
476
477 return false;
478 };
479
480 for( const wxString& dir : PCM_PACKAGE_DIRECTORIES )
481 {
482 wxFileName d( m_pcm->Get3rdPartyPath(), "" );
483 d.AppendDir( dir );
484 d.AppendDir( clean_package_id );
485
486 if( !d.DirExists() )
487 continue;
488
489 m_reporter->PCMReport( wxString::Format( _( "Removing directory %s" ), d.GetPath() ),
491
492 if( aKeep.empty() )
493 {
494 if( !d.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
495 {
496 m_reporter->PCMReport(
497 wxString::Format( _( "Failed to remove directory %s" ), d.GetPath() ),
499 }
500 }
501 else
502 {
503 std::vector<wxString> files;
504 std::vector<wxString> dirs;
505 PATH_COLLECTOR collector( files, dirs );
506
507 wxDir( d.GetFullPath() )
508 .Traverse( collector, wxEmptyString, wxDIR_DEFAULT | wxDIR_NO_FOLLOW );
509
510 // Do a poor mans post order traversal by sorting paths in reverse length order
511 std::sort( files.begin(), files.end(), sort_func );
512 std::sort( dirs.begin(), dirs.end(), sort_func );
513
514 // Delete files that don't match any of the aKeep regexs
515 for( const wxString& file : files )
516 {
517 bool del = true;
518
519 for( const wxRegEx& re : aKeep )
520 {
521 wxString tmp = file.Mid( path_prefix_len );
522 tmp.Replace( "\\", "/" );
523
524 if( re.Matches( tmp ) )
525 {
526 // m_reporter->PCMReport( wxString::Format( _( "Keeping file '%s'." ), tmp ),
527 // RPT_SEVERITY_INFO );
528
529 del = false;
530 break;
531 }
532 }
533
534 if( del )
535 wxRemoveFile( file );
536 }
537
538 // Delete any empty dirs
539 for( const wxString& empty_dir : dirs )
540 {
541 wxFileName dname( empty_dir, "" );
542 dname.Rmdir(); // not passing any flags here will only remove empty directories
543 }
544 }
545 }
546}
547
548
550{
551 PCM_TASK task = [aPackage, this]
552 {
554
555 m_pcm->MarkUninstalled( aPackage );
556
557 if( aPackage.type == PCM_PACKAGE_TYPE::PT_COLORTHEME )
558 m_color_themes_changed.store( true );
559
560 m_reporter->PCMReport(
561 wxString::Format( _( "Package %s uninstalled" ), aPackage.identifier ),
563 };
564
565 m_install_queue.push( task );
566}
567
568
569void PCM_TASK_MANAGER::RunQueue( wxWindow* aParent )
570{
571 m_reporter = std::make_unique<DIALOG_PCM_PROGRESS>( aParent );
572
573 m_reporter->SetNumPhases( m_download_queue.size() + m_install_queue.size() );
574 m_reporter->Show();
575
576 wxSafeYield();
577
578 std::mutex mutex;
579 std::condition_variable condvar;
580 bool download_complete = false;
581
582 m_color_themes_changed.store( false );
583
584 std::thread download_thread(
585 [&]()
586 {
587 while( !m_download_queue.empty() && !m_reporter->IsCancelled() )
588 {
589 PCM_TASK task;
590 m_download_queue.pop( task );
591 task();
592 condvar.notify_all();
593 }
594
595 std::unique_lock<std::mutex> lock( mutex );
596 download_complete = true;
597 condvar.notify_all();
598 } );
599
600 std::thread install_thread(
601 [&]()
602 {
603 std::unique_lock<std::mutex> lock( mutex );
604
605 do
606 {
607 condvar.wait( lock,
608 [&]()
609 {
610 return download_complete || !m_install_queue.empty()
611 || m_reporter->IsCancelled();
612 } );
613
614 lock.unlock();
615
616 while( !m_install_queue.empty() && !m_reporter->IsCancelled() )
617 {
618 PCM_TASK task;
619 m_install_queue.pop( task );
620 task();
621 m_reporter->AdvancePhase();
622 }
623
624 lock.lock();
625
626 } while( ( !m_install_queue.empty() || !download_complete )
627 && !m_reporter->IsCancelled() );
628
629 m_reporter->PCMReport( _( "Done." ), RPT_SEVERITY_INFO );
630
631 m_reporter->SetFinished();
632 } );
633
634 m_reporter->KeepRefreshing( true );
635 m_reporter->Destroy();
636 m_reporter.reset();
637
638 download_thread.join();
639 install_thread.join();
640}
641
642
644{
645 return m_color_themes_changed.load();
646}
int Perform()
Equivalent to curl_easy_perform.
bool SetTransferCallback(const TRANSFER_CALLBACK &aCallback, size_t aInterval)
bool SetURL(const std::string &aURL)
Set the request URL.
bool SetFollowRedirects(bool aFollow)
Enable the following of HTTP(s) and other redirects, by default curl does not follow redirects.
int GetTransferTotal(uint64_t &aDownloadedBytes) const
bool SetOutputStream(const std::ostream *aOutput)
const std::string GetErrorText(int aCode)
Fetch CURL's "friendly" error string for a given error code.
static wxString GetUserCachePath()
Gets the stock (install) 3d viewer plugins path.
Definition: paths.cpp:321
PATH_COLLECTOR(std::vector< wxString > &aFiles, std::vector< wxString > &aDirs)
wxDirTraverseResult OnDir(const wxString &dirPath) override
std::vector< wxString > & m_dirs
std::vector< wxString > & m_files
wxDirTraverseResult OnFile(const wxString &aFilePath) override
std::atomic_bool m_color_themes_changed
void deletePackageDirectories(const wxString &aPackageId, const std::forward_list< wxRegEx > &aKeep={})
Delete all package files.
void InstallFromFile(wxWindow *aParent, const wxString &aFilePath)
Installs package from an archive file on disk.
SYNC_QUEUE< PCM_TASK > m_install_queue
std::shared_ptr< PLUGIN_CONTENT_MANAGER > m_pcm
void Uninstall(const PCM_PACKAGE &aPackage)
Enqueue package uninstallation.
SYNC_QUEUE< PCM_TASK > m_download_queue
void DownloadAndInstall(const PCM_PACKAGE &aPackage, const wxString &aVersion, const wxString &aRepositoryId, const bool isUpdate)
Enqueue package download and installation.
int downloadFile(const wxString &aFilePath, const wxString &aUrl)
Download URL to a file.
void installDownloadedPackage(const PCM_PACKAGE &aPackage, const wxString &aVersion, const wxString &aRepositoryId, const wxFileName &aFilePath, const bool isUpdate)
Installs downloaded package archive.
bool extract(const wxString &aFilePath, const wxString &aPackageId, bool isMultiThreaded)
Extract package archive.
void RunQueue(wxWindow *aParent)
Run queue of pending actions.
std::unique_ptr< DIALOG_PCM_PROGRESS > m_reporter
bool ColorSettingsChanged() const
bool pop(T &aReceiver)
Pop a value if the queue into the provided variable.
Definition: sync_queue.h:63
bool empty() const
Return true if the queue is empty.
Definition: sync_queue.h:82
size_t size() const
Return the size of the queue.
Definition: sync_queue.h:91
void push(T const &aValue)
Push a value onto the queue.
Definition: sync_queue.h:41
#define _(s)
nlohmann::json json
Definition: gerbview.cpp:44
std::function< int(size_t, size_t, size_t, size_t)> TRANSFER_CALLBACK
Wrapper interface around the curl_easy API/.
bool parse(std::istream &aStream, bool aVerbose)
Parse a PCB or footprint file from the given input stream.
const std::unordered_set< wxString > PCM_PACKAGE_DIRECTORIES({ "plugins", "footprints", "3dmodels", "symbols", "resources", "colors", })
< Contains list of all valid directories that get extracted from a package archive
@ PT_COLORTHEME
Definition: pcm_data.h:45
void compile_keep_on_update_regex(const PCM_PACKAGE &pkg, const PACKAGE_VERSION &ver, std::forward_list< wxRegEx > &aKeepOnUpdate)
std::function< void()> PCM_TASK
void Format(OUTPUTFORMATTER *out, int aNestLevel, int aCtl, const CPTREE &aTree)
Output a PTREE into s-expression format via an OUTPUTFORMATTER derivative.
Definition: ptree.cpp:200
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_INFO
< Package version metadata Package metadata
Definition: pcm_data.h:74
std::vector< std::string > keep_on_update
Definition: pcm_data.h:85
Definition: pcm_data.h:138
Repository reference to a resource.
Definition: pcm_data.h:95
wxString identifier
Definition: pcm_data.h:99
std::vector< PACKAGE_VERSION > versions
Definition: pcm_data.h:107
PCM_PACKAGE_TYPE type
Definition: pcm_data.h:100
std::vector< std::string > keep_on_update
Definition: pcm_data.h:106
static bool CopyStreamData(wxInputStream &inputStream, wxOutputStream &outputStream, wxFileOffset size)