lib: Adding explicit default constructor for JLDirectoryEntry.
[barry.git] / desktop / src / MigrateDlg.cc
blob34a07b18a01b982278a0a978ccefdfe969cdde1b
1 ///
2 /// \file MigrateDlg.cc
3 /// Dialog for the "Migrate Device" main menu mode button...
4 /// going with a dialog instead of a mode class this time.
5 ///
7 /*
8 Copyright (C) 2012, Net Direct Inc. (http://www.netdirect.ca/)
10 This program is free software; you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation; either version 2 of the License, or
13 (at your option) any later version.
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19 See the GNU General Public License in the COPYING file at the
20 root directory of this project for more details.
23 #include "MigrateDlg.h"
24 #include "windowids.h"
25 #include "configui.h"
26 #include "barrydesktop.h"
27 #include <string>
28 #include <wx/statline.h>
29 #include <barry/barrybackup.h>
30 #include <iostream>
31 #include "wxi18n.h"
33 using namespace std;
34 using namespace OpenSync;
36 DEFINE_EVENT_TYPE(MET_THREAD_FINISHED)
37 DEFINE_EVENT_TYPE(MET_CHECK_DEST_PIN)
38 DEFINE_EVENT_TYPE(MET_SET_STATUS_MSG)
39 DEFINE_EVENT_TYPE(MET_PROMPT_PASSWORD)
40 DEFINE_EVENT_TYPE(MET_ERROR_MSG)
42 BEGIN_EVENT_TABLE(MigrateDlg, wxDialog)
43 EVT_BUTTON (Dialog_Migrate_MigrateNowButton,
44 MigrateDlg::OnMigrateNow)
45 EVT_BUTTON (Dialog_Migrate_CancelButton,
46 MigrateDlg::OnCancel)
47 EVT_CLOSE (MigrateDlg::OnCloseWindow)
48 EVT_COMMAND (wxID_ANY, MET_THREAD_FINISHED,
49 MigrateDlg::OnThreadFinished)
50 EVT_COMMAND (wxID_ANY, MET_CHECK_DEST_PIN,
51 MigrateDlg::OnCheckDestPin)
52 EVT_COMMAND (wxID_ANY, MET_SET_STATUS_MSG,
53 MigrateDlg::OnSetStatusMsg)
54 EVT_COMMAND (wxID_ANY, MET_PROMPT_PASSWORD,
55 MigrateDlg::OnPromptPassword)
56 EVT_COMMAND (wxID_ANY, MET_ERROR_MSG,
57 MigrateDlg::OnErrorMsg)
58 END_EVENT_TABLE()
61 class EventDesktopConnector : public Barry::DesktopConnector
63 MigrateDlg *m_dlg;
65 public:
66 EventDesktopConnector(MigrateDlg *dlg, const char *password,
67 const std::string &locale, const Barry::ProbeResult &result)
68 : Barry::DesktopConnector(password, locale, result)
69 , m_dlg(dlg)
73 virtual bool PasswordPrompt(const Barry::BadPassword &bp,
74 std::string &password_result);
77 bool EventDesktopConnector::PasswordPrompt(const Barry::BadPassword &bp,
78 std::string &password_result)
80 // ping the parent and wait for finish
81 wxCommandEvent event(MET_PROMPT_PASSWORD, wxID_ANY);
82 event.SetEventObject(m_dlg);
83 event.SetInt(bp.remaining_tries());
84 m_dlg->AddPendingEvent(event);
85 m_dlg->WaitForEvent();
87 password_result = m_dlg->GetPassword().utf8_str();
89 // assume that a blank password means the user wishes to quit...
90 // wxWidgets doesn't seem to handle this very well?
91 return password_result.size() > 0;
95 //////////////////////////////////////////////////////////////////////////////
96 // MigrateDlg class
98 MigrateDlg::MigrateDlg(wxWindow *parent,
99 const Barry::Probe::Results &results,
100 int current_device_index)
101 : wxDialog(parent, Dialog_GroupCfg, _W("Migrate Device"))
102 , m_results(results)
103 , m_current_device_index(current_device_index)
104 , m_migrate_thread_created(false)
105 , m_abort_flag(false)
106 , m_thread_running(false)
107 , m_source_device(0)
108 , m_dest_device(0)
109 , m_topsizer(0)
110 , m_source_combo(0)
111 , m_dest_combo(0)
112 , m_write_mode_combo(0)
113 , m_migrate_button(0)
114 , m_wipe_check(0)
115 , m_status(0)
116 , m_progress(0)
118 // setup the raw GUI
119 CreateLayout();
122 void MigrateDlg::WaitForEvent()
124 m_waiter.Wait();
127 void MigrateDlg::CreateLayout()
129 m_topsizer = new wxBoxSizer(wxVERTICAL);
131 // AddDescriptionSizer(m_topsizer);
132 AddMainSizer(m_topsizer);
133 AddStatusSizer(m_topsizer);
135 SetSizer(m_topsizer);
136 m_topsizer->SetSizeHints(this);
137 m_topsizer->Layout();
140 void MigrateDlg::AddDescriptionSizer(wxSizer *sizer)
142 sizer->Add(
143 new wxStaticText(this, wxID_ANY,
144 _W("Migrate device data from source device to target device.")),
145 0, wxALIGN_CENTRE | wxALIGN_CENTRE_VERTICAL |
146 wxTOP | wxLEFT | wxRIGHT, 10);
149 void MigrateDlg::AddMainSizer(wxSizer *sizer)
151 // add 3 main sections together into one sizer
152 wxSizer *main = new wxBoxSizer(wxHORIZONTAL);
153 Main_AddSourceSizer(main);
154 Main_AddButtonSizer(main);
155 Main_AddDestSizer(main);
157 // add main sizer to top level sizer
158 sizer->Add(main, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 10);
161 void MigrateDlg::AddStatusSizer(wxSizer *sizer)
163 sizer->Add( new wxStaticLine(this),
164 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 10);
166 sizer->Add(
167 m_status = new wxStaticText(this, wxID_ANY, _W("Ready...")),
168 0, wxEXPAND | wxLEFT | wxRIGHT, 10);
169 // Reduce font size for status text
170 wxFont font = m_status->GetFont();
171 font.SetPointSize(font.GetPointSize() - 1);
172 m_status->SetFont( font );
174 sizer->Add(
175 m_progress = new wxGauge(this, wxID_ANY, 100),
176 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10);
179 void MigrateDlg::Main_AddSourceSizer(wxSizer *sizer)
181 wxArrayString devices;
182 for( Barry::Probe::Results::const_iterator i = m_results.begin();
183 i != m_results.end(); ++i )
185 devices.Add(wxString(i->GetDisplayName().c_str(), wxConvUTF8));
188 wxSizer *source = new wxStaticBoxSizer(
189 new wxStaticBox(this, wxID_ANY, _W("Source device")),
190 wxVERTICAL
192 source->Add(
193 m_source_combo = new wxChoice(this, wxID_ANY,
194 wxDefaultPosition, wxSize(225, -1), devices),
195 0, wxALL, 5);
197 if( m_current_device_index >= 0 )
198 m_source_combo->SetSelection(m_current_device_index);
200 sizer->Add(source, 0, wxEXPAND, 0);
203 void MigrateDlg::Main_AddButtonSizer(wxSizer *sizer)
205 wxSizer *buttons = new wxBoxSizer(wxVERTICAL);
206 buttons->Add( m_migrate_button = new wxButton(this,
207 Dialog_Migrate_MigrateNowButton, _W("Migrate Now")),
208 0, wxALIGN_CENTRE, 0);
209 m_migrate_button->SetDefault();
210 buttons->AddSpacer(10);
211 buttons->Add( new wxButton(this, Dialog_Migrate_CancelButton,
212 _W("Cancel")),
213 0, wxALIGN_CENTRE, 0);
215 sizer->Add(buttons, 1, wxALIGN_CENTRE | wxLEFT | wxRIGHT, 10);
218 void MigrateDlg::Main_AddDestSizer(wxSizer *sizer)
220 wxArrayString devices;
221 devices.Add(_W("Prompt to plug in later..."));
222 for( Barry::Probe::Results::const_iterator i = m_results.begin();
223 i != m_results.end(); ++i )
225 devices.Add(wxString(i->GetDisplayName().c_str(), wxConvUTF8));
228 wxSizer *dest = new wxStaticBoxSizer(
229 new wxStaticBox(this, wxID_ANY, _W("Destination device")),
230 wxVERTICAL
232 dest->Add(
233 m_dest_combo = new wxChoice(this, wxID_ANY,
234 wxDefaultPosition, wxSize(225, -1), devices),
235 0, wxALL, 5);
236 m_dest_combo->SetSelection(0);
239 wxArrayString write_modes;
240 // TRANSLATORS: these 4 strings are write-mode options in the
241 // Migrate Device dialog
242 write_modes.Add(_W("Erase all, then restore"));
243 write_modes.Add(_W("Add new, and overwrite existing"));
244 write_modes.Add(_W("Add only, don't overwrite existing"));
245 write_modes.Add(_W("Add every record as a new entry (may cause duplicates)"));
247 dest->Add( new wxStaticText(this, wxID_ANY, _W("Write Mode:")),
248 0, wxTOP | wxLEFT | wxRIGHT, 5);
249 dest->Add( m_write_mode_combo = new wxChoice(this, wxID_ANY,
250 wxDefaultPosition, wxSize(225, -1), write_modes),
251 0, wxALL, 5);
252 m_write_mode_combo->SetSelection(0);
254 // dest->Add( m_wipe_check = wxCheckBox(maybe a checkbox for "wipe device before restore"));
256 sizer->Add(dest, 0, wxEXPAND, 0);
259 void MigrateDlg::EnableControls(bool enable)
261 m_source_combo->Enable(enable);
262 m_dest_combo->Enable(enable);
263 m_write_mode_combo->Enable(enable);
264 m_migrate_button->Enable(enable);
265 //m_wipe_check->Enable(enable);
268 void MigrateDlg::DoSafeClose()
270 // if migrate thread is running, try to close it down first...
271 // do not exit the dialog until the thread is properly stopped!
272 if( m_thread_running ) {
273 m_abort_flag = true;
275 m_status->SetLabel(_W("Waiting for thread to close..."));
277 if( m_migrate_thread_created ) {
278 void *junk;
279 pthread_join(m_migrate_thread, &junk);
280 m_migrate_thread_created = false;
284 // all activity is stopped, so close dialog
285 EndModal(wxID_CANCEL);
288 void MigrateDlg::SendEvent(int event_type)
290 wxCommandEvent event(event_type, wxID_ANY);
291 event.SetEventObject(this);
292 AddPendingEvent(event);
295 void MigrateDlg::SendStatusEvent(const wxString &msg, int pos, int max)
297 wxCommandEvent event(MET_SET_STATUS_MSG, wxID_ANY);
298 event.SetEventObject(this);
300 event.SetString(msg);
302 int value = 0;
303 if( pos == -1 )
304 value |= 0xff00;
305 else
306 value |= (pos << 8);
308 if( max == -1 )
309 value |= 0xff;
310 else
311 value |= max & 0xff;
312 event.SetInt(value);
314 AddPendingEvent(event);
317 void MigrateDlg::SendErrorMsgEvent(const wxString &msg)
319 wxCommandEvent event(MET_ERROR_MSG, wxID_ANY);
320 event.SetEventObject(this);
321 event.SetString(msg);
322 AddPendingEvent(event);
325 void MigrateDlg::OnMigrateNow(wxCommandEvent &event)
327 // gather info from dialog
328 int source_index = m_source_combo->GetSelection();
329 int dest_index = m_dest_combo->GetSelection();
330 int write_mode_index = m_write_mode_combo->GetSelection();
332 // validate options
333 if( source_index == wxNOT_FOUND || dest_index == wxNOT_FOUND ||
334 write_mode_index == wxNOT_FOUND )
336 wxMessageBox(_W("Please select a source and destination device, as well as the write mode."),
337 _W("Migration Options Needed"), wxOK | wxICON_ERROR);
338 return;
341 // do not migrate from one PIN to the same PIN
342 if( source_index == (dest_index - 1) ) {
343 wxMessageBox(_W("Cannot migrate from and to the same PIN."),
344 _W("Migration Options Error"), wxOK | wxICON_ERROR);
345 return;
348 // set the migration arguments
349 m_source_device = &m_results[source_index];
350 if( dest_index > 0 ) {
351 m_dest_device = &m_results[dest_index-1];
353 else {
354 m_dest_device = 0; // an invalid dest causes a prompt
356 switch( write_mode_index )
358 case 0:
359 m_write_mode = Barry::DeviceParser::ERASE_ALL_WRITE_ALL;
360 break;
361 case 1:
362 m_write_mode = Barry::DeviceParser::INDIVIDUAL_OVERWRITE;
363 break;
364 case 2:
365 m_write_mode = Barry::DeviceParser::ADD_BUT_NO_OVERWRITE;
366 break;
367 case 3:
368 m_write_mode = Barry::DeviceParser::ADD_WITH_NEW_ID;
369 break;
370 default:
371 wxMessageBox(_W("Invalid write mode. This should never happen. Contact the developers."),
372 _W("Internal Logic Error"), wxOK | wxICON_ERROR);
373 return;
376 // disable all buttons and controls except cancel
377 EnableControls(false);
379 // turn off the stop flag
380 m_abort_flag = false;
381 m_thread_running = false;
383 // fire up migrate thread, and let the thread close the dialog when
384 // done (can we EndModal() from a thread?)
385 int ret = pthread_create(&m_migrate_thread, NULL,
386 &MigrateDlg::MigrateThread, this);
387 if( ret != 0 ) {
388 // go back to normal
389 EnableControls(true);
390 return;
392 else {
393 m_migrate_thread_created = true;
396 // thread started... let it finish
399 void MigrateDlg::OnCancel(wxCommandEvent &event)
401 DoSafeClose();
404 void MigrateDlg::OnCloseWindow(wxCloseEvent &event)
406 DoSafeClose();
409 void MigrateDlg::OnThreadFinished(wxCommandEvent &event)
411 if( m_migrate_thread_created ) {
412 m_status->SetLabel(_W("Waiting for thread..."));
413 void *junk;
414 pthread_join(m_migrate_thread, &junk);
415 m_migrate_thread_created = false;
418 if( m_abort_flag ) {
419 // user cancelled in some way, restore GUI
420 EnableControls(true);
421 m_status->SetLabel(_W("Cancelled by user..."));
423 else {
424 // if we were not aborted, then this is success, and we
425 // can close the original dialog
426 EndModal(wxID_CANCEL);
430 void MigrateDlg::OnCheckDestPin(wxCommandEvent &event)
432 ScopeSignaler done(m_waiter);
434 if( m_dest_device && m_dest_device->m_pin.Valid() )
435 return; // nothing to do
437 // no destination pin was available, so user may need to plugin
438 // the new device right now, before continuing
439 int response = wxMessageBox(_W("Please plug in the target device now."),
440 _W("Ready for Writing"), wxOK | wxCANCEL | wxICON_INFORMATION,
441 this);
442 if( response != wxOK ) {
443 // user cancelled
444 m_abort_flag = true;
445 return;
449 wxBusyCursor wait;
450 m_status->SetLabel(_W("Scanning USB for devices..."));
452 // pause for 2 seconds to let any new devices settle down
453 wxGetApp().Yield();
454 wxSleep(2);
456 // rescan the USB and try to find the new device
457 Barry::Probe probe;
458 m_new_results = probe.GetResults();
461 // now prompt the user... create a list of PIN display names
462 wxArrayString devices;
463 for( Barry::Probe::Results::const_iterator i = m_new_results.begin();
464 i != m_new_results.end(); ++i )
466 devices.Add(wxString(i->GetDisplayName().c_str(), wxConvUTF8));
469 m_status->SetLabel(_W("User input..."));
471 do {
473 // prompt the user with this list
474 int choice = wxGetSingleChoiceIndex(_W("Please select the target device to write to:"),
475 _W("Destination PIN"), devices, this);
476 if( choice == -1 ) {
477 // user cancelled
478 m_abort_flag = true;
479 return;
482 // found the new PIN to use!
483 m_dest_device = &m_new_results[choice];
485 // check if user needs to choose again
486 if( m_dest_device->m_pin == m_source_device->m_pin ) {
487 wxMessageBox(_W("Cannot use the same device PIN as migration destination."),
488 _W("Invalid Device Selection"),
489 wxOK | wxICON_ERROR, this);
492 } while( m_dest_device->m_pin == m_source_device->m_pin );
495 void MigrateDlg::OnSetStatusMsg(wxCommandEvent &event)
497 if( event.GetString().size() ) {
498 m_status->SetLabel(event.GetString());
501 if( (unsigned int)event.GetInt() != 0xffff ) {
502 unsigned int value = (unsigned int) event.GetInt();
503 unsigned int pos = (value & 0xff00) >> 8;
504 unsigned int max = (value & 0xff);
506 if( pos != 0xff ) {
507 m_progress->SetValue(pos);
510 if( max != 0xff ) {
511 m_progress->SetRange(max);
516 void MigrateDlg::OnPromptPassword(wxCommandEvent &event)
518 ScopeSignaler done(m_waiter);
520 // create prompt based on exception data
521 wxString prompt = wxString::Format(
522 _W("Please enter device password: (%d tries remaining)"),
523 event.GetInt());
525 // ask user for device password
526 m_password = wxGetPasswordFromUser(prompt,
527 _W("Device Password"), _T(""), this);
530 void MigrateDlg::OnErrorMsg(wxCommandEvent &event)
532 ScopeSignaler done(m_waiter);
533 wxMessageBox(event.GetString(), _W("Migration Error"),
534 wxOK | wxICON_ERROR, this);
537 void* MigrateDlg::MigrateThread(void *arg)
539 MigrateDlg *us = (MigrateDlg*) arg;
541 // we are running!
542 us->m_thread_running = true;
544 try {
545 // backup source PIN
546 us->BackupSource();
548 // make sure we have a destination PIN
549 if( !us->m_abort_flag )
550 us->CheckDestPin();
552 // restore to dest PIN
553 if( !us->m_abort_flag )
554 us->RestoreToDest();
556 catch( std::exception &e ) {
557 us->SendErrorMsgEvent(wxString(e.what(), wxConvUTF8));
558 us->m_waiter.Wait();
561 // invalidate the device selection pointers, since
562 // m_new_results may not always exist
563 us->m_source_device = us->m_dest_device = 0;
565 // we are done!
566 us->m_thread_running = false;
568 // send event to let main GUI thread we're finished
569 us->SendEvent(MET_THREAD_FINISHED);
571 return 0;
574 class PeekParser : public Barry::Parser
576 std::string m_dbname;
578 public:
579 virtual void ParseRecord(const Barry::DBData &data,
580 const Barry::IConverter *ic)
582 m_dbname = data.GetDBName();
585 const std::string& GetDBName() const { return m_dbname; }
589 // This is called from the thread
590 void MigrateDlg::BackupSource()
592 // connect to the source device
593 SendStatusEvent(_W("Connecting..."));
594 EventDesktopConnector connect(this, "", "utf-8", *m_source_device);
595 if( !connect.Reconnect(2) ) {
596 // user cancelled
597 m_abort_flag = true;
598 return;
601 // fetch DBDB, for list of databases to backup... back them all up
602 // remember to save this DBDB into the class, so it is available
603 // for the restore stage
604 m_source_dbdb = connect.GetDesktop().GetDBDB();
605 unsigned int total = m_source_dbdb.GetTotalRecordCount();
607 // create DeviceBuilder for fetching all
608 Barry::DeviceBuilder builder(connect.GetDesktop());
609 builder.Add(m_source_dbdb);
611 // calculate the default backup path location, based on user name
612 // (see backup GUI for code?)
613 Barry::ConfigFile cfg(m_source_device->m_pin);
614 Barry::ConfigFile::CheckPath(cfg.GetPath());
615 m_backup_tarfile = cfg.GetPath() + "/" +
616 Barry::MakeBackupFilename(m_source_device->m_pin, "migrate");
618 // and create tarball output parser
619 Barry::Backup parser(m_backup_tarfile);
621 // create the pipe
622 Barry::Pipe pipe(builder);
624 PeekParser peeker;
626 // create tee parser, so we can see what's being written
627 Barry::TeeParser tee;
628 tee.Add(peeker);
629 tee.Add(parser);
631 // setup status bar
632 unsigned int count = 0;
633 SendStatusEvent(_T(""), 0, 100);
635 // cycle through all databases
636 while( !builder.EndOfFile() ) {
637 if( !pipe.PumpEntry(tee) )
638 continue;
640 count++;
642 // ok, first entry has been pumped, so we can use
643 // peeker to create the status message, and only once
644 ostringstream oss;
645 oss << _C("Backing up database: ")
646 << peeker.GetDBName()
647 << "...";
649 // calculate 1 to 100 percentage, based on number of
650 // databases being backed up, and update status bar too
651 int percent = count / (float)total * 100.0;
653 // send status event once
654 SendStatusEvent(wxString(oss.str().c_str(),wxConvUTF8), percent);
656 // backup the rest of the database, but don't update status
657 // for each record
658 int block = 0;
659 while( pipe.PumpEntry(tee) ) {
660 count++;
661 block++;
662 if( block >= 25 ) {
663 int percent = count / (float)total * 100.0;
664 SendStatusEvent(_T(""), percent);
665 block = 0;
668 // on each record (pump cycle?), as often as possible,
669 // check the m_abort_flag, and abort if necessary,
670 // updating the status message
671 if( m_abort_flag ) {
672 SendStatusEvent(_W("Backup aborted by user..."));
673 return;
678 // close all files, etc.
679 parser.Close();
682 // This is called from the thread
683 void MigrateDlg::CheckDestPin()
685 SendEvent(MET_CHECK_DEST_PIN);
686 m_waiter.Wait();
689 // This is called from the thread
690 void MigrateDlg::RestoreToDest()
692 // connect to the dest device
693 SendStatusEvent(_W("Connecting to target..."));
694 EventDesktopConnector connect(this, "", "utf-8", *m_dest_device);
695 if( !connect.Reconnect(2) ) {
696 // user cancelled
697 m_abort_flag = true;
698 return;
701 // fetch DBDB of dest device, for list of databases we can restore
702 // to... compare with the backup DBDB to create a list of similarly
703 // named databases which we can restore....
704 Barry::DatabaseDatabase dest_dbdb = connect.GetDesktop().GetDBDB();
706 // create the restore builder object, to read from tarball
707 Barry::Restore builder(m_backup_tarfile);
709 // add all database names from the _dest_ DBDB to the Restore
710 // filter... this way, only databases that exist on the new
711 // device will get restored
712 builder.Add(dest_dbdb);
713 unsigned int total = builder.GetRecordTotal();// counts filtered records
715 // create device parser, to write to device
716 Barry::DeviceParser parser(connect.GetDesktop(), m_write_mode);
718 // create the pipe
719 Barry::Pipe pipe(builder);
721 // setup status bar
722 unsigned int count = 0;
723 SendStatusEvent(_T(""), 0, 100);
725 // cycle through all databases
726 Barry::DBData meta; // record meta data of next tar record
727 while( !builder.EndOfFile() ) try {
728 if( !builder.GetNextMeta(meta) )
729 continue;
731 // ok, first entry has been pumped, so we can use
732 // peeker to create the status message, and only once
733 ostringstream oss;
734 oss << _C("Writing database: ")
735 << meta.GetDBName()
736 << "...";
738 // for debugging purposes in the field, display the names
739 // of the databases we restore
740 cerr << oss.str() << endl;
742 // calculate 1 to 100 percentage, based on number of
743 // databases being backed up, and update status bar too
744 int percent = count / (float)total * 100.0;
746 // send status event once
747 SendStatusEvent(wxString(oss.str().c_str(),wxConvUTF8), percent);
749 // restore the rest of the database, but don't update status
750 // for each record
751 int block = 0;
752 while( pipe.PumpEntry(parser) ) {
753 count++;
754 block++;
755 if( block >= 25 ) {
756 int percent = count / (float)total * 100.0;
757 SendStatusEvent(_T(""), percent);
758 block = 0;
761 // on each record (pump cycle?), as often as possible,
762 // check the m_abort_flag, and abort if necessary,
763 // updating the status message
764 if( m_abort_flag ) {
765 SendStatusEvent(_W("Restore aborted by user..."));
766 return;
770 catch( Barry::ReturnCodeError &e ) {
771 cerr << _C("Unable to clear or write to database: ")
772 << meta.GetDBName()
773 << ": " << e.what() << endl;
774 cerr << _C("Continuing to process remaining records...") << endl;
776 // skip the problematic database, and keep on trying
777 builder.SkipCurrentDB();