diff --git a/src/ActiveProjects.cpp b/src/ActiveProjects.cpp new file mode 100644 index 000000000..a8b4f04bf --- /dev/null +++ b/src/ActiveProjects.cpp @@ -0,0 +1,98 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + ActiveProjects.cpp + +********************************************************************//** + +\class ActiveProjects +\brief Manages a list of active projects + +*//********************************************************************/ + +#include "Audacity.h" +#include "ActiveProjects.h" +#include "prefs.h" + +#include + +FilePaths ActiveProjects::GetAll() +{ + FilePaths files; + + wxString key; + long ndx; + + wxString configPath = gPrefs->GetPath(); + gPrefs->SetPath(wxT("/ActiveProjects")); + + bool more = gPrefs->GetFirstEntry(key, ndx); + while (more) + { + wxFileName path = gPrefs->Read(key, wxT("")); + + files.Add(path.GetFullPath()); + + more = gPrefs->GetNextEntry(key, ndx); + } + gPrefs->SetPath(configPath); + + return files; +} + +void ActiveProjects::Add(const FilePath &path) +{ + wxString key = Find(path); + + if (key.empty()) + { + int i = 0; + do + { + key.Printf(wxT("/ActiveProjects/%d"), ++i); + } while (gPrefs->HasEntry(key)); + + gPrefs->Write(key, path); + gPrefs->Flush(); + } +} + +void ActiveProjects::Remove(const FilePath &path) +{ + wxString key = Find(path); + + if (!key.empty()) + { + gPrefs->DeleteEntry(wxT("/ActiveProjects/" + key)); + gPrefs->Flush(); + } +} + +wxString ActiveProjects::Find(const FilePath &path) +{ + bool found = false; + + wxString key; + long ndx; + + wxString configPath = gPrefs->GetPath(); + gPrefs->SetPath(wxT("/ActiveProjects")); + + bool more = gPrefs->GetFirstEntry(key, ndx); + while (more) + { + if (gPrefs->Read(key, wxT("")).IsSameAs(path)) + { + found = true; + break; + } + + more = gPrefs->GetNextEntry(key, ndx); + } + + gPrefs->SetPath(configPath); + + return found ? key : wxT(""); +} + diff --git a/src/ActiveProjects.h b/src/ActiveProjects.h new file mode 100644 index 000000000..71d8e12dc --- /dev/null +++ b/src/ActiveProjects.h @@ -0,0 +1,25 @@ +/********************************************************************** + + Audacity: A Digital Audio Editor + + ActiveProjects.h + +**********************************************************************/ + +#ifndef __AUDACITY_ACTIVE_PROJECTS__ +#define __AUDACITY_ACTIVE_PROJECTS__ + +#include "Audacity.h" +#include "audacity/Types.h" + +#include + +namespace ActiveProjects +{ + FilePaths GetAll(); + void Add(const FilePath &path); + void Remove(const FilePath &path); + wxString Find(const FilePath &path); +}; + +#endif diff --git a/src/AutoRecoveryDialog.cpp b/src/AutoRecoveryDialog.cpp index 6b95519f9..293167ec5 100644 --- a/src/AutoRecoveryDialog.cpp +++ b/src/AutoRecoveryDialog.cpp @@ -10,60 +10,76 @@ Paul Licameli split from AutoRecovery.cpp #include "AutoRecoveryDialog.h" +#include "ActiveProjects.h" #include "FileNames.h" #include "ProjectManager.h" #include "ShuttleGui.h" #include "widgets/AudacityMessageBox.h" #include "widgets/wxPanelWrapper.h" +#include #include #include #include #include +#define USE_CHECKBOXES + enum { - ID_RECOVER_ALL = 10000, - ID_RECOVER_NONE, - ID_QUIT_AUDACITY, + ID_QUIT_AUDACITY = 10000, + ID_DISCARD_SELECTED, + ID_RECOVER_SELECTED, ID_FILE_LIST }; class AutoRecoveryDialog final : public wxDialogWrapper { public: - AutoRecoveryDialog(const FilePaths &files); + AutoRecoveryDialog(); + + bool HasRecoverables() const; + FilePaths GetRecoverables(); private: void PopulateOrExchange(ShuttleGui &S); void PopulateList(); void OnQuitAudacity(wxCommandEvent &evt); - void OnRecoverNone(wxCommandEvent &evt); - void OnRecoverAll(wxCommandEvent &evt); + void OnDiscardSelected(wxCommandEvent &evt); + void OnRecoverSelected(wxCommandEvent &evt); - const FilePaths & mFiles; + FilePaths mFiles; wxListCtrl *mFileList; public: DECLARE_EVENT_TABLE() }; -AutoRecoveryDialog::AutoRecoveryDialog(const FilePaths &files) -: wxDialogWrapper(nullptr, -1, XO("Automatic Crash Recovery"), +BEGIN_EVENT_TABLE(AutoRecoveryDialog, wxDialogWrapper) + EVT_BUTTON(ID_QUIT_AUDACITY, AutoRecoveryDialog::OnQuitAudacity) + EVT_BUTTON(ID_DISCARD_SELECTED, AutoRecoveryDialog::OnDiscardSelected) + EVT_BUTTON(ID_RECOVER_SELECTED, AutoRecoveryDialog::OnRecoverSelected) +END_EVENT_TABLE() + +AutoRecoveryDialog::AutoRecoveryDialog() +: wxDialogWrapper(nullptr, wxID_ANY, XO("Automatic Crash Recovery"), wxDefaultPosition, wxDefaultSize, - wxDEFAULT_DIALOG_STYLE & (~wxCLOSE_BOX)), // no close box - mFiles(files) + wxDEFAULT_DIALOG_STYLE & (~wxCLOSE_BOX)) // no close box { SetName(); ShuttleGui S(this, eIsCreating); PopulateOrExchange(S); } -BEGIN_EVENT_TABLE(AutoRecoveryDialog, wxDialogWrapper) - EVT_BUTTON(ID_RECOVER_ALL, AutoRecoveryDialog::OnRecoverAll) - EVT_BUTTON(ID_RECOVER_NONE, AutoRecoveryDialog::OnRecoverNone) - EVT_BUTTON(ID_QUIT_AUDACITY, AutoRecoveryDialog::OnQuitAudacity) -END_EVENT_TABLE() +bool AutoRecoveryDialog::HasRecoverables() const +{ + return mFiles.size() > 0; +} + +FilePaths AutoRecoveryDialog::GetRecoverables() +{ + return mFiles; +} void AutoRecoveryDialog::PopulateOrExchange(ShuttleGui &S) { @@ -76,9 +92,18 @@ void AutoRecoveryDialog::PopulateOrExchange(ShuttleGui &S) S.StartStatic(XO("Recoverable projects")); { - mFileList = S.Id(ID_FILE_LIST) - /*i18n-hint: (noun). It's the name of the project to recover.*/ - .AddListControlReportMode( { XO("Name") } ); + mFileList = S.Id(ID_FILE_LIST).AddListControlReportMode( + { +#if defined(USE_CHECKBOXES) + /*i18n-hint: (verb). It instruct the user to select items.*/ + XO("Select"), +#endif + /*i18n-hint: (noun). It's the name of the project to recover.*/ + XO("Name") + }); +#if defined(USE_CHECKBOXES) + mFileList->EnableCheckBoxes(); +#endif PopulateList(); } S.EndStatic(); @@ -90,8 +115,8 @@ void AutoRecoveryDialog::PopulateOrExchange(ShuttleGui &S) S.StartHorizontalLay(); { S.Id(ID_QUIT_AUDACITY).AddButton(XXO("Quit Audacity")); - S.Id(ID_RECOVER_NONE).AddButton(XXO("Discard Projects")); - S.Id(ID_RECOVER_ALL).AddButton(XXO("Recover Projects")); + S.Id(ID_DISCARD_SELECTED).AddButton(XXO("Discard Selected")); + S.Id(ID_RECOVER_SELECTED).AddButton(XXO("Recover Selected")); } S.EndHorizontalLay(); } @@ -109,14 +134,46 @@ void AutoRecoveryDialog::PopulateOrExchange(ShuttleGui &S) void AutoRecoveryDialog::PopulateList() { + wxString tempdir = FileNames::TempDir(); + wxString pattern = wxT("*.") + FileNames::UnsavedProjectExtension(); + FilePaths files; + + wxDir::GetAllFiles(tempdir, &files, pattern, wxDIR_FILES); + + FilePaths active = ActiveProjects::GetAll(); + mFileList->DeleteAllItems(); - for (int i = 0, cnt = mFiles.size(); i < cnt; ++i) + long item = 0; + for (auto file : active) { - mFileList->InsertItem(i, wxFileName{ mFiles[i] }.GetName()); + wxFileName fn = file; + if (fn.FileExists()) + { + FilePath fullPath = fn.GetFullPath(); + if (files.Index(fullPath) == wxNOT_FOUND) + { + files.push_back(fullPath); + } + } } + for (auto file : files) + { + wxFileName fn = file; + + mFiles.push_back(fn.GetFullPath()); + mFileList->InsertItem(item, wxT("")); + mFileList->SetItem(item, 1, fn.GetName()); + item++; + } + +#if defined(USE_CHECKBOXES) + mFileList->SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); + mFileList->SetColumnWidth(1, wxLIST_AUTOSIZE); +#else mFileList->SetColumnWidth(0, wxLIST_AUTOSIZE); +#endif } void AutoRecoveryDialog::OnQuitAudacity(wxCommandEvent & WXUNUSED(event)) @@ -124,40 +181,41 @@ void AutoRecoveryDialog::OnQuitAudacity(wxCommandEvent & WXUNUSED(event)) EndModal(ID_QUIT_AUDACITY); } -void AutoRecoveryDialog::OnRecoverNone(wxCommandEvent & WXUNUSED(event)) +void AutoRecoveryDialog::OnDiscardSelected(wxCommandEvent & WXUNUSED(event)) { int ret = AudacityMessageBox( - XO("Are you sure you want to discard all recoverable projects?\n\nChoosing \"Yes\" discards all recoverable projects immediately."), + XO("Are you sure you want to discard the selected projects?\n\n" + "Choosing \"Yes\" permanently deletes the selected projects immediately."), XO("Confirm Discard Projects"), wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT, this); - if (ret == wxYES) - EndModal(ID_RECOVER_NONE); -} - -void AutoRecoveryDialog::OnRecoverAll(wxCommandEvent & WXUNUSED(event)) -{ - EndModal(ID_RECOVER_ALL); -} - -//////////////////////////////////////////////////////////////////////////// - -static FilePaths HaveFilesToRecover() -{ - wxString tempdir = FileNames::TempDir(); - wxString pattern = wxT("*.") + FileNames::UnsavedProjectExtension(); - FilePaths files; - - wxDir::GetAllFiles(tempdir, &files, pattern, wxDIR_FILES); - - return files; -} - -static bool RemoveAllAutoSaveFiles(const FilePaths &files) -{ - for (int i = 0, cnt = files.size(); i < cnt; ++i) + if (ret == wxNO) { - FilePath file = files[i]; + return; + } + +#define USE_CHECKBOXES +#if defined(USE_CHECKBOXES) +#define state wxLIST_STATE_DONTCARE +#else +#define state wxLIST_STATE_SELECTED +#endif + + long item = -1; + while (true) + { + item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, state); + if (item == wxNOT_FOUND) + { + break; + } +#if defined(USE_CHECKBOXES) + if (!mFileList->IsItemChecked(item)) + { + continue; + } +#endif + FilePath file = mFiles[item]; if (wxRemoveFile(file)) { @@ -175,12 +233,55 @@ static bool RemoveAllAutoSaveFiles(const FilePaths &files) { wxRemoveFile(file + wxT("-journal")); } + + ActiveProjects::Remove(file); } } - return true; + PopulateList(); + + if (mFileList->GetItemCount() == 0) + { + EndModal(ID_DISCARD_SELECTED); + } } +void AutoRecoveryDialog::OnRecoverSelected(wxCommandEvent & WXUNUSED(event)) +{ +#define USE_CHECKBOXES +#if defined(USE_CHECKBOXES) +#define state wxLIST_STATE_DONTCARE +#else +#define state wxLIST_STATE_SELECTED +#endif + + FilePaths files; + + long item = -1; + while (true) + { + item = mFileList->GetNextItem(item, wxLIST_NEXT_ALL, state); + if (item == wxNOT_FOUND) + { + break; + } +#if defined(USE_CHECKBOXES) + if (!mFileList->IsItemChecked(item)) + { + continue; + } +#endif + + files.push_back(mFiles[item]); + } + + mFiles = files; + + EndModal(ID_RECOVER_SELECTED); +} + +//////////////////////////////////////////////////////////////////////////// + static bool RecoverAllProjects(const FilePaths &files, AudacityProject **pproj) { @@ -197,57 +298,69 @@ static bool RecoverAllProjects(const FilePaths &files, *pproj = NULL; } - // Open project. When an auto-save file has been opened successfully, - // the opened auto-save file is automatically deleted and a NEW one - // is created. - (void) ProjectManager::OpenProject(proj, files[i], false); + // Open project. + if (ProjectManager::OpenProject(proj, files[i], false) == nullptr) + { + return false; + } + + ActiveProjects::Remove(files[i]); } return true; } -bool ShowAutoRecoveryDialogIfNeeded(AudacityProject **pproj, - bool *didRecoverAnything) +bool ShowAutoRecoveryDialogIfNeeded(AudacityProject **pproj, bool *didRecoverAnything) { if (didRecoverAnything) - *didRecoverAnything = false; - - FilePaths files = HaveFilesToRecover(); - if (files.size()) { - // Under wxGTK3, the auto recovery dialog will not get - // the focus since the project window hasn't been allowed - // to completely initialize. - // - // Yielding seems to allow the initialization to complete. - // - // Additionally, it also corrects a sizing issue in the dialog - // related to wxWidgets bug: - // - // http://trac.wxwidgets.org/ticket/16440 - // - // This must be done before "dlg" is declared. - wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI); + *didRecoverAnything = false; + } - int ret = AutoRecoveryDialog(files).ShowModal(); + bool success = true; + // Under wxGTK3, the auto recovery dialog will not get + // the focus since the project window hasn't been allowed + // to completely initialize. + // + // Yielding seems to allow the initialization to complete. + // + // Additionally, it also corrects a sizing issue in the dialog + // related to wxWidgets bug: + // + // http://trac.wxwidgets.org/ticket/16440 + // + // This must be done before "dlg" is declared. + wxEventLoopBase::GetActive()->YieldFor(wxEVT_CATEGORY_UI); + + AutoRecoveryDialog dialog; + + if (dialog.HasRecoverables()) + { + int ret = dialog.ShowModal(); + switch (ret) { - case ID_RECOVER_NONE: - return RemoveAllAutoSaveFiles(files); + case ID_DISCARD_SELECTED: + success = true; + break; - case ID_RECOVER_ALL: - if (didRecoverAnything) - *didRecoverAnything = true; - return RecoverAllProjects(files, pproj); + case ID_RECOVER_SELECTED: + success = RecoverAllProjects(dialog.GetRecoverables(), pproj); + if (success) + { + if (didRecoverAnything) + { + *didRecoverAnything = true; + } + } + break; default: // This includes ID_QUIT_AUDACITY return false; } - } else - { - // Nothing to recover, move along - return true; } + + return success; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 80e59fe55..12e27202d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -63,6 +63,8 @@ list( APPEND SOURCES PRIVATE AColor.cpp AColor.h + ActiveProjects.cpp + ActiveProjects.h AboutDialog.cpp AboutDialog.h AdornedRulerPanel.cpp diff --git a/src/ProjectFileIO.cpp b/src/ProjectFileIO.cpp index 2f18b3b9d..1b886e3ce 100644 --- a/src/ProjectFileIO.cpp +++ b/src/ProjectFileIO.cpp @@ -18,6 +18,7 @@ Paul Licameli split from AudacityProject.cpp #include #include +#include "ActiveProjects.h" #include "DBConnection.h" #include "FileNames.h" #include "Project.h" @@ -1178,8 +1179,18 @@ void ProjectFileIO::SetFileName(const FilePath &fileName) { auto &project = mProject; + if (!mFileName.empty()) + { + ActiveProjects::Remove(mFileName); + } + mFileName = fileName; + if (!mFileName.empty()) + { + ActiveProjects::Add(mFileName); + } + if (mTemporary) { project.SetProjectName({}); diff --git a/src/ProjectFileIO.cpp.bkp.cpp b/src/ProjectFileIO.cpp.bkp.cpp new file mode 100644 index 000000000..016112768 --- /dev/null +++ b/src/ProjectFileIO.cpp.bkp.cpp @@ -0,0 +1,2513 @@ +/********************************************************************** + +Audacity: A Digital Audio Editor + +ProjectFileIO.cpp + +Paul Licameli split from AudacityProject.cpp + +**********************************************************************/ + +#include "ProjectFileIO.h" + + +#include +#include +#include +#include +#include +#include + +#include "FileNames.h" +#include "Project.h" +#include "ProjectFileIORegistry.h" +#include "ProjectSerializer.h" +#include "ProjectSettings.h" +#include "SampleBlock.h" +#include "Sequence.h" +#include "Tags.h" +#include "TimeTrack.h" +#include "ViewInfo.h" +#include "WaveClip.h" +#include "WaveTrack.h" +#include "widgets/AudacityMessageBox.h" +#include "widgets/NumericTextCtrl.h" +#include "widgets/ProgressDialog.h" +#include "xml/XMLFileReader.h" + +wxDEFINE_EVENT(EVT_PROJECT_TITLE_CHANGE, wxCommandEvent); + +static const int ProjectFileID = ('A' << 24 | 'U' << 16 | 'D' << 8 | 'Y'); +static const int ProjectFileVersion = 1; + +// Navigation: +// +// Bindings are marked out in the code by, e.g. +// BIND SQL sampleblocks +// A search for "BIND SQL" will find all bindings. +// A search for "SQL sampleblocks" will find all SQL related +// to sampleblocks. + +static const char *ProjectFileSchema = + // These are persistent and not connection based + // + // See the CMakeList.txt for the SQLite lib for more + // settings. + "PRAGMA .application_id = %d;" + "PRAGMA .user_version = %d;" + "" + // project is a binary representation of an XML file. + // it's in binary for speed. + // One instance only. id is always 1. + // dict is a dictionary of fieldnames. + // doc is the binary representation of the XML + // in the doc, fieldnames are replaced by 2 byte dictionary + // index numbers. + // This is all opaque to SQLite. It just sees two + // big binary blobs. + // There is no limit to document blob size. + // dict will be smallish, with an entry for each + // kind of field. + "CREATE TABLE IF NOT EXISTS .project" + "(" + " id INTEGER PRIMARY KEY," + " dict BLOB," + " doc BLOB" + ");" + "" + // CREATE SQL autosave + // autosave is a binary representation of an XML file. + // it's in binary for speed. + // One instance only. id is always 1. + // dict is a dictionary of fieldnames. + // doc is the binary representation of the XML + // in the doc, fieldnames are replaced by 2 byte dictionary + // index numbers. + // This is all opaque to SQLite. It just sees two + // big binary blobs. + // There is no limit to document blob size. + // dict will be smallish, with an entry for each + // kind of field. + "CREATE TABLE IF NOT EXISTS .autosave" + "(" + " id INTEGER PRIMARY KEY," + " dict BLOB," + " doc BLOB" + ");" + "" + // CREATE SQL tags + // tags is not used (yet) + "CREATE TABLE IF NOT EXISTS .tags" + "(" + " name TEXT," + " value BLOB" + ");" + "" + // CREATE SQL sampleblocks + // 'samples' are fixed size blocks of int16, int32 or float32 numbers. + // The blocks may be partially empty. + // The quantity of valid data in the blocks is + // provided in the project XML. + // + // sampleformat specifies the format of the samples stored. + // + // blockID is a 64 bit number. + // + // summin to summary64K are summaries at 3 distance scales. + "CREATE TABLE IF NOT EXISTS .sampleblocks" + "(" + " blockid INTEGER PRIMARY KEY AUTOINCREMENT," + " sampleformat INTEGER," + " summin REAL," + " summax REAL," + " sumrms REAL," + " summary256 BLOB," + " summary64k BLOB," + " samples BLOB" + ");"; + +// Configuration to provide "safe" connections +static const char *SafeConfig = + "PRAGMA .locking_mode = SHARED;" + "PRAGMA .synchronous = NORMAL;" + "PRAGMA .journal_mode = WAL;" + "PRAGMA .wal_autocheckpoint = 0;"; + +// Configuration to provide "Fast" connections +static const char *FastConfig = + "PRAGMA .locking_mode = SHARED;" + "PRAGMA .synchronous = OFF;" + "PRAGMA .journal_mode = OFF;"; + +// This singleton handles initialization/shutdown of the SQLite library. +// It is needed because our local SQLite is built with SQLITE_OMIT_AUTOINIT +// defined. +// +// It's safe to use even if a system version of SQLite is used that didn't +// have SQLITE_OMIT_AUTOINIT defined. +class SQLiteIniter +{ +public: + SQLiteIniter() + { + // Enable URI filenames for all connections + mRc = sqlite3_config(SQLITE_CONFIG_URI, 1); + + if (mRc == SQLITE_OK) + { + mRc = sqlite3_initialize(); + } + +#if !defined(__WXMSW__) + if (mRc == SQLITE_OK) + { + // Use the "unix-excl" VFS to make access to the DB exclusive. This gets + // rid of the "-shm" shared memory file. + // + // Though it shouldn't, it doesn't matter if this fails. + auto vfs = sqlite3_vfs_find("unix-excl"); + if (vfs) + { + sqlite3_vfs_register(vfs, 1); + } + } +#endif + } + ~SQLiteIniter() + { + // This function must be called single-threaded only + // It returns a value, but there's nothing we can do with it + (void) sqlite3_shutdown(); + } + int mRc; +}; + +bool ProjectFileIO::InitializeSQL() +{ + static SQLiteIniter sqliteIniter; + return sqliteIniter.mRc == SQLITE_OK; +} + +static void RefreshAllTitles(bool bShowProjectNumbers ) +{ + for ( auto pProject : AllProjects{} ) { + if ( !GetProjectFrame( *pProject ).IsIconized() ) { + ProjectFileIO::Get( *pProject ).SetProjectTitle( + bShowProjectNumbers ? pProject->GetProjectNumber() : -1 ); + } + } +} + +TitleRestorer::TitleRestorer( + wxTopLevelWindow &window, AudacityProject &project ) +{ + if( window.IsIconized() ) + window.Restore(); + window.Raise(); // May help identifying the window on Mac + + // Construct this project's name and number. + sProjName = project.GetProjectName(); + if ( sProjName.empty() ) { + sProjName = _(""); + UnnamedCount = std::count_if( + AllProjects{}.begin(), AllProjects{}.end(), + []( const AllProjects::value_type &ptr ){ + return ptr->GetProjectName().empty(); + } + ); + if ( UnnamedCount > 1 ) { + sProjNumber.Printf( + _("[Project %02i] "), project.GetProjectNumber() + 1 ); + RefreshAllTitles( true ); + } + } + else + UnnamedCount = 0; +} + +TitleRestorer::~TitleRestorer() { + if( UnnamedCount > 1 ) + RefreshAllTitles( false ); +} + +static const AudacityProject::AttachedObjects::RegisteredFactory sFileIOKey{ + []( AudacityProject &parent ){ + auto result = std::make_shared< ProjectFileIO >( parent ); + return result; + } +}; + +ProjectFileIO &ProjectFileIO::Get( AudacityProject &project ) +{ + auto &result = project.AttachedObjects::Get< ProjectFileIO >( sFileIOKey ); + return result; +} + +const ProjectFileIO &ProjectFileIO::Get( const AudacityProject &project ) +{ + return Get( const_cast< AudacityProject & >( project ) ); +} + +ProjectFileIO::ProjectFileIO(AudacityProject &) +{ + mPrevDB = nullptr; + mDB = nullptr; + + mRecovered = false; + mModified = false; + mTemporary = true; + mBypass = false; + + UpdatePrefs(); +} + +void ProjectFileIO::Init( AudacityProject &project ) +{ + // This step can't happen in the ctor of ProjectFileIO because ctor of + // AudacityProject wasn't complete + mpProject = project.shared_from_this(); + + // Kick off the checkpoint thread + mCheckpointThread = std::thread([this]{ CheckpointThread(); }); +} + +ProjectFileIO::~ProjectFileIO() +{ + wxASSERT_MSG(mDB == nullptr, wxT("Project file was not closed at shutdown")); + + // Tell the checkpoint thread to shutdown + { + std::lock_guard guard(mCheckpointMutex); + mCheckpointStop = true; + mCheckpointCondition.notify_one(); + } + + // And wait for it to do so + mCheckpointThread.join(); +} + +void ProjectFileIO::CheckpointThread() +{ + mCheckpointStop = false; + + while (true) + { + { + // Wait for work or the stop signal + std::unique_lock lock(mCheckpointMutex); + mCheckpointCondition.wait(lock, + [&] + { + return mCheckpointWaitingPages || mCheckpointStop; + }); + + // Requested to stop, so bail + if (mCheckpointStop) + { + break; + } + + // Capture the number of pages that need checkpointing and reset + mCheckpointCurrentPages = mCheckpointWaitingPages; + mCheckpointWaitingPages = 0; + + // Lock out others while the checkpoint is running + mCheckpointActive.lock(); + } + + // Open another connection to the DB to prevent blocking the main thread. + sqlite3 *db = nullptr; + if (sqlite3_open(mFileName, &db) == SQLITE_OK) + { + // Configure it to be safe + Config(db, SafeConfig); + + // And kick off the checkpoint. This may not checkpoint ALL frames + // in the WAL. They'll be gotten the next time around. + sqlite3_wal_checkpoint_v2(db, nullptr, SQLITE_CHECKPOINT_PASSIVE, nullptr, nullptr); + + // All done. + sqlite3_close(db); + + // Reset + mCheckpointCurrentPages = 0; + + // Checkpoint is complete + mCheckpointActive.unlock(); + } + } + + return; +} + +int ProjectFileIO::CheckpointHook(void *data, sqlite3 *db, const char *schema, int pages) +{ + // Get access to our object + ProjectFileIO *that = static_cast(data); + + // Qeuue the database pointer for our checkpoint thread to process + std::lock_guard guard(that->mCheckpointMutex); + that->mCheckpointWaitingPages = pages; + that->mCheckpointCondition.notify_one(); + + return SQLITE_OK; +} + +sqlite3 *ProjectFileIO::DB() +{ + if (!mDB) + { + if (!OpenDB()) + { + throw SimpleMessageBoxException + { + XO("Failed to open the project's database") + }; + } + } + + return mDB; +} + +// Put the current database connection aside, keeping it open, so that +// another may be opened with OpenDB() +void ProjectFileIO::SaveConnection() +{ + // Should do nothing in proper usage, but be sure not to leak a connection: + DiscardConnection(); + + mPrevDB = mDB; + mPrevFileName = mFileName; + + mDB = nullptr; + SetFileName({}); +} + +// Close any set-aside connection +void ProjectFileIO::DiscardConnection() +{ + if ( mPrevDB ) + { + auto rc = sqlite3_close( mPrevDB ); + if ( rc != SQLITE_OK ) + { + // Store an error message + SetDBError( + XO("Failed to successfully close the source project file") + ); + } + mPrevDB = nullptr; + mPrevFileName.clear(); + } +} + +// Close any current connection and switch back to using the saved +void ProjectFileIO::RestoreConnection() +{ + if ( mDB ) + { + auto rc = sqlite3_close( mDB ); + if ( rc != SQLITE_OK ) + { + // Store an error message + SetDBError( + XO("Failed to successfully close the destination project file") + ); + } + } + mDB = mPrevDB; + SetFileName(mPrevFileName); + + mPrevDB = nullptr; + mPrevFileName.clear(); +} + +void ProjectFileIO::UseConnection( sqlite3 *db, const FilePath &filePath ) +{ + wxASSERT(mDB == nullptr); + mDB = db; + SetFileName( filePath ); +} + +void ProjectFileIO::Config(sqlite3 *db, const char *config, const wxString &schema) +{ + int rc; + + wxString sql = config; + + if (schema.empty()) + { + sql.Replace(wxT("."), wxT("")); + } + else + { + sql.Replace(wxT(""), schema); + } + + rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + { + // This non-fatal...for now + SetDBError(XO("Failed to set connection configuration")); + } + + return; +} + +sqlite3 *ProjectFileIO::OpenDB(FilePath fileName) +{ + wxASSERT(mDB == nullptr); + bool temp = false; + + if (fileName.empty()) + { + fileName = GetFileName(); + if (fileName.empty()) + { + fileName = FileNames::UnsavedProjectFileName(); + temp = true; + } + else + { + temp = false; + } + } + + int rc = sqlite3_open(fileName, &mDB); + if (rc != SQLITE_OK) + { + SetDBError(XO("Failed to open project file")); + // sqlite3 docs say you should close anyway to avoid leaks + sqlite3_close( mDB ); + mDB = nullptr; + return nullptr; + } + + // Ensure attached DB connection gets configured + Config(mDB, SafeConfig); + + if (!CheckVersion()) + { + CloseDB(); + return nullptr; + } + + mTemporary = temp; + + SetFileName(fileName); + + // Install our checkpoint hook + sqlite3_wal_hook(mDB, CheckpointHook, this); + + return mDB; +} + +bool ProjectFileIO::CloseDB() +{ + int rc; + + if (mDB) + { + // Uninstall our checkpoint hook so that no additional checkpoints + // are sent our way. (Though this shouldn't really happen.) + sqlite3_wal_hook(mDB, nullptr, nullptr); + + // Display a progress dialog if there's active or pending checkpoints + if (mCheckpointWaitingPages || mCheckpointCurrentPages) + { + TranslatableString title = XO("Checkpointing project"); + + // Get access to the active tracklist + auto pProject = mpProject.lock(); + if (pProject) + { + title = XO("Checkpointing %s").Format(pProject->GetProjectName()); + } + + wxGenericProgressDialog pd(title.Translation(), + XO("This may take several seconds").Translation(), + 300000, // range + nullptr, // parent + wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_SMOOTH); + + while (mCheckpointWaitingPages || mCheckpointCurrentPages) + { + wxMilliSleep(50); + pd.Pulse(); + } + } + + // Close the DB + rc = sqlite3_close(mDB); + if (rc != SQLITE_OK) + { + SetDBError(XO("Failed to close the project file")); + } + + mDB = nullptr; + SetFileName({}); + } + + return true; +} + +bool ProjectFileIO::DeleteDB() +{ + wxASSERT(mDB == nullptr); + + if (mTemporary && !mFileName.empty()) + { + wxFileName temp(FileNames::TempDir()); + if (temp == wxPathOnly(mFileName)) + { + if (!wxRemoveFile(mFileName)) + { + SetError(XO("Failed to close the project file")); + + return false; + } + } + } + + return true; +} + +bool ProjectFileIO::TransactionStart(const wxString &name) +{ + char* errmsg = nullptr; + + int rc = sqlite3_exec(DB(), + wxT("SAVEPOINT ") + name + wxT(";"), + nullptr, + nullptr, + &errmsg); + + if (errmsg) + { + SetDBError( + XO("Failed to create savepoint:\n\n%s").Format(name) + ); + sqlite3_free(errmsg); + } + + return rc == SQLITE_OK; +} + +bool ProjectFileIO::TransactionCommit(const wxString &name) +{ + char* errmsg = nullptr; + + int rc = sqlite3_exec(DB(), + wxT("RELEASE ") + name + wxT(";"), + nullptr, + nullptr, + &errmsg); + + if (errmsg) + { + SetDBError( + XO("Failed to release savepoint:\n\n%s").Format(name) + ); + sqlite3_free(errmsg); + } + + return rc == SQLITE_OK; +} + +bool ProjectFileIO::TransactionRollback(const wxString &name) +{ + char* errmsg = nullptr; + + int rc = sqlite3_exec(DB(), + wxT("ROLLBACK TO ") + name + wxT(";"), + nullptr, + nullptr, + &errmsg); + + if (errmsg) + { + SetDBError( + XO("Failed to release savepoint:\n\n%s").Format(name) + ); + sqlite3_free(errmsg); + } + + return rc == SQLITE_OK; +} + +/* static */ +int ProjectFileIO::ExecCallback(void *data, int cols, char **vals, char **names) +{ + ExecParm *parms = static_cast(data); + return parms->func(parms->result, cols, vals, names); +} + +int ProjectFileIO::Exec(const char *query, ExecCB callback, ExecResult &result) +{ + char *errmsg = nullptr; + ExecParm ep = {callback, result}; + + int rc = sqlite3_exec(DB(), query, ExecCallback, &ep, &errmsg); + + if (errmsg) + { + SetDBError( + XO("Failed to execute a project file command:\n\n%s").Format(query) + ); + mLibraryError = Verbatim(errmsg); + sqlite3_free(errmsg); + } + + return rc; +} + +bool ProjectFileIO::Query(const char *sql, ExecResult &result) +{ + result.clear(); + + auto getresult = [](ExecResult &result, int cols, char **vals, char **names) + { + std::vector row; + + for (int i = 0; i < cols; ++i) + { + row.push_back(vals[i]); + } + + result.push_back(row); + + return SQLITE_OK; + }; + + int rc = Exec(sql, getresult, result); + if (rc != SQLITE_OK) + { + return false; + } + + return true; +} + +bool ProjectFileIO::GetValue(const char *sql, wxString &result) +{ + result.clear(); + + ExecResult holder; + if (!Query(sql, holder)) + { + return false; + } + + // Return the first column in the first row, if any + if (holder.size() && holder[0].size()) + { + result.assign(holder[0][0]); + } + + return true; +} + +bool ProjectFileIO::GetBlob(const char *sql, wxMemoryBuffer &buffer) +{ + auto db = DB(); + int rc; + + buffer.Clear(); + + sqlite3_stmt *stmt = nullptr; + auto cleanup = finally([&] + { + if (stmt) + { + sqlite3_finalize(stmt); + } + }); + + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Unable to prepare project file command:\n\n%s").Format(sql) + ); + return false; + } + + rc = sqlite3_step(stmt); + + // A row wasn't found...not an error + if (rc == SQLITE_DONE) + { + return true; + } + + if (rc != SQLITE_ROW) + { + SetDBError( + XO("Failed to retrieve data from the project file.\nThe following command failed:\n\n%s").Format(sql) + ); + // AUD TODO handle error + return false; + } + + const void *blob = sqlite3_column_blob(stmt, 0); + int size = sqlite3_column_bytes(stmt, 0); + + buffer.AppendData(blob, size); + + return true; +} + +bool ProjectFileIO::CheckVersion() +{ + auto db = DB(); + int rc; + + // Install our schema if this is an empty DB + wxString result; + if (!GetValue("SELECT Count(*) FROM sqlite_master WHERE type='table';", result)) + { + return false; + } + + // If the return count is zero, then there are no tables defined, so this + // must be a new project file. + if (wxStrtol(result, nullptr, 10) == 0) + { + return InstallSchema(db); + } + + // Check for our application ID + if (!GetValue("PRAGMA application_ID;", result)) + { + return false; + } + + // It's a database that SQLite recognizes, but it's not one of ours + if (wxStrtoul(result, nullptr, 10) != ProjectFileID) + { + SetError(XO("This is not an Audacity project file")); + return false; + } + + // Get the project file version + if (!GetValue("PRAGMA user_version;", result)) + { + return false; + } + + long version = wxStrtol(result, nullptr, 10); + + // Project file version is higher than ours. We will refuse to + // process it since we can't trust anything about it. + if (version > ProjectFileVersion) + { + SetError( + XO("This project was created with a newer version of Audacity:\n\nYou will need to upgrade to process it") + ); + return false; + } + + // Project file is older than ours, ask the user if it's okay to + // upgrade. + if (version < ProjectFileVersion) + { + return UpgradeSchema(); + } + + return true; +} + +bool ProjectFileIO::InstallSchema(sqlite3 *db, const char *schema /* = "main" */) +{ + int rc; + + wxString sql; + sql.Printf(ProjectFileSchema, ProjectFileID, ProjectFileVersion); + sql.Replace("", schema); + + rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Unable to initialize the project file") + ); + return false; + } + + return true; +} + +bool ProjectFileIO::UpgradeSchema() +{ + return true; +} + +// The orphan block handling should be removed once autosave and related +// blocks become part of the same transaction. + +// An SQLite function that takes a blockid and looks it up in a set of +// blockids captured during project load. If the blockid isn't found +// in the set, it will be deleted. +void ProjectFileIO::InSet(sqlite3_context *context, int argc, sqlite3_value **argv) +{ + BlockIDs *blockids = (BlockIDs *) sqlite3_user_data(context); + SampleBlockID blockid = sqlite3_value_int64(argv[0]); + + sqlite3_result_int(context, blockids->find(blockid) != blockids->end()); +} + +bool ProjectFileIO::CheckForOrphans(BlockIDs &blockids) +{ + auto db = DB(); + int rc; + + auto cleanup = finally([&] + { + // Remove our function, whether it was successfully defined or not. + sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr, nullptr, nullptr, nullptr); + }); + + // Add the function used to verify each rows blockid against the set of active blockids + rc = sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, &blockids, InSet, nullptr, nullptr); + if (rc != SQLITE_OK) + { + wxLogDebug(wxT("Unable to add 'inset' function")); + return false; + } + + // Delete all rows that are orphaned + rc = sqlite3_exec(db, "DELETE FROM sampleblocks WHERE NOT inset(blockid);", nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + { + wxLogWarning(XO("Cleanup of orphan blocks failed").Translation()); + return false; + } + + // Mark the project recovered if we deleted any rows + int changes = sqlite3_changes(db); + if (changes > 0) + { + wxLogInfo(XO("Total orphan blocks deleted %d").Translation(), changes); + mRecovered = true; + } + + return true; +} + +sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath, + const TranslatableString &msg, + bool prune /* = false */, + const std::shared_ptr &tracks/* = nullptr */) +{ + // Get access to the active tracklist + auto pProject = mpProject.lock(); + if (!pProject) + { + return nullptr; + } + auto &tracklist = tracks ? *tracks : TrackList::Get(*pProject); + + BlockIDs blockids; + + // Collect all active blockids + if (prune) + { + for (auto wt : tracklist.Any()) + { + // Scan all clips within current track + for (const auto &clip : wt->GetAllClips()) + { + // Scan all blockfiles within current clip + auto blocks = clip->GetSequenceBlockArray(); + for (const auto &block : *blocks) + { + blockids.insert(block.sb->GetBlockID()); + } + } + } + } + // Collect ALL blockids + else + { + ExecResult holder; + if (!Query("SELECT blockid FROM sampleblocks;", holder)) + { + return nullptr; + } + + for (auto block : holder) + { + SampleBlockID blockid; + block[0].ToLongLong(&blockid); + + blockids.insert(blockid); + } + } + + // Create the project doc + ProjectSerializer doc; + WriteXMLHeader(doc); + WriteXML(doc, false, tracks); + + auto db = DB(); + sqlite3 *destdb = nullptr; + bool success = false; + int rc; + ProgressResult res = ProgressResult::Success; + + // Cleanup in case things go awry + auto cleanup = finally([&] + { + if (!success) + { + sqlite3_close(destdb); + + sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr); + + wxRemoveFile(destpath); + } + }); + + // Attach the destination database + wxString sql; + sql.Printf("ATTACH DATABASE '%s' AS outbound;", destpath); + + rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Unable to attach destination database") + ); + return nullptr; + } + + // Ensure attached DB connection gets configured + Config(db, FastConfig, "outbound"); + + // Install our schema into the new database + if (!InstallSchema(db, "outbound")) + { + // Message already set + return nullptr; + } + + // Copy over tags (not really used yet) + rc = sqlite3_exec(db, + "INSERT INTO outbound.tags SELECT * FROM main.tags;", + nullptr, + nullptr, + nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Failed to copy tags") + ); + + return nullptr; + } + + { + // Ensure statement gets cleaned up + sqlite3_stmt *stmt = nullptr; + auto cleanup = finally([&] + { + if (stmt) + { + sqlite3_finalize(stmt); + } + }); + + // Prepare the statement only once + rc = sqlite3_prepare_v2(db, + "INSERT INTO outbound.sampleblocks" + " SELECT * FROM main.sampleblocks" + " WHERE blockid = ?;", + -1, + &stmt, + nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Unable to prepare project file command:\n\n%s").Format(sql) + ); + return nullptr; + } + + /* i18n-hint: This title appears on a dialog that indicates the progress + in doing something.*/ + ProgressDialog progress(XO("Progress"), msg, pdlgHideStopButton); + ProgressResult result = ProgressResult::Success; + + wxLongLong_t count = 0; + wxLongLong_t total = blockids.size(); + + // Start a transaction. Since we're running without a journal, + // this really doesn't provide rollback. It just prevents SQLite + // from auto committing after each step through the loop. + sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr); + + // Copy sample blocks from the main DB to the outbound DB + for (auto blockid : blockids) + { + // BIND blockid parameter + if (sqlite3_bind_int64(stmt, 1, blockid) != SQLITE_OK) + { + THROW_INCONSISTENCY_EXCEPTION; + } + + // Process it + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) + { + SetDBError( + XO("Failed to update the project file.\nThe following command failed:\n\n%s").Format(sql) + ); + return nullptr; + } + + // BIND blockid parameter + if (sqlite3_reset(stmt) != SQLITE_OK) + { + THROW_INCONSISTENCY_EXCEPTION; + } + + result = progress.Update(++count, total); + if (result != ProgressResult::Success) + { + // Note that we're not setting success, so the finally + // block above will take care of cleaning up + return nullptr; + } + } + + // See BEGIN above... + sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr); + } + + // Detach the destination database + rc = sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Destination project could not be detached") + ); + + return nullptr; + } + + // Open the newly created database + rc = sqlite3_open(destpath, &destdb); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Failed to open copy of project file") + ); + + return nullptr; + } + + // Ensure attached DB connection gets configured + Config(destdb, SafeConfig); + + // Write the project doc + if (!WriteDoc("project", doc, destdb)) + { + return false; + } + + // Tell cleanup everything is good to go + success = true; + + return destdb; +} + +bool ProjectFileIO::ShouldVacuum(const std::shared_ptr &tracks) +{ + std::set active; + unsigned long long current = 0; + + // Scan all wave tracks + for (auto wt : tracks->Any()) + { + // Scan all clips within current track + for (const auto &clip : wt->GetAllClips()) + { + // Scan all blockfiles within current clip + auto blocks = clip->GetSequenceBlockArray(); + for (const auto &block : *blocks) + { + const auto &sb = block.sb; + auto blockid = sb->GetBlockID(); + + // Accumulate space used by the block if the blocckid has not + // yet been seen + if (active.count(blockid) == 0) + { + current += sb->GetSpaceUsage(); + + active.insert(blockid); + } + } + } + } + + // Get the number of blocks and total length from the project file. + ExecResult holder; + if (!Query("SELECT Count(*), Sum(Length(summary256)) + Sum(Length(summary64k)) + Sum(Length(samples)) FROM sampleblocks;", holder)) + { + // Shouldn't vacuum since we don't have the full picture + return false; + } + + // Verify we got the results we asked for + if (holder.size() != 1 || holder[0].size() != 2) + { + // Shouldn't vacuum since we don't have the full picture + return false; + } + + // Convert + unsigned long long blockcount = 0; + holder[0][0].ToULongLong(&blockcount); + + unsigned long long total = 0; + holder[0][1].ToULongLong(&total); + + // Remember if we had unused blocks in the project file + mHadUnused = (blockcount > active.size()); + + // Let's make a percentage...should be plenty of head room + current *= 100; + + wxLogDebug(wxT("used = %lld total = %lld %lld\n"), current, total, current / total); + if (current / total > 80) + { + wxLogDebug(wxT("not vacuuming")); + return false; + } + wxLogDebug(wxT("vacuuming")); + + return true; +} + +void ProjectFileIO::Vacuum(const std::shared_ptr &tracks) +{ + // Haven't vacuumed yet + mWasVacuumed = false; + + // Assume we do until we found out othersize. That way cleanup at project + // close time will still occur + mHadUnused = true; + + // Don't vacuum if this is a temporary project or if it's deteremined there not + // enough unused blocks to make it worthwhile + if (IsTemporary() || !ShouldVacuum(tracks)) + { + // Delete the AutoSave doc it if exists + if (IsModified()) + { + // PRL: not clear what to do if the following fails, but the worst should + // be, the project may reopen in its present state as a recovery file, not + // at the last saved state. + (void) AutoSaveDelete(); + } + + return; + } +#if 1 + wxString fileName = mFileName + "_vacuum"; + wxString origName; + bool wasTemp = false; + bool success = false; + + // Should probably simplify all of the following by using renames. + + auto restore = finally([&] + { + if (!origName.empty()) + { + if (success) + { + // The Save was successful, so now it is safe to abandon the + // original connection + DiscardConnection(); + + // And also remove the original file if it was a temporary file + if (wasTemp) + { + wxRemoveFile(origName); + } + } + else + { + // Close the new database and go back to using the original + // connection + RestoreConnection(); + + // And delete the new database + wxRemoveFile(fileName); + } + } + }); + + // Do NOT prune here since we need to retain the Undo history + // after we switch to the new file. + auto newDB = CopyTo(fileName, XO("Compacting project"), true); + if (!newDB) + { + return; + } + + // Remember the original project filename and temporary status. Only do + // this after a successful copy so the "finally" block above doesn't monkey + // with the files. + origName = mFileName; + wasTemp = mTemporary; + + // Save the original database connection and try to switch to a new one + // (also ensuring closing of one of the connections, with the cooperation + // of the finally above) + SaveConnection(); + + // Make the new connection "safe" + Config(newDB, SafeConfig); + + // And make it the active project file + UseConnection(newDB, fileName); + + // Install our checkpoint hook + sqlite3_wal_hook(mDB, CheckpointHook, this); + + ProjectSerializer doc; + WriteXMLHeader(doc); + WriteXML(doc); + + if (!WriteDoc("project", doc)) + { + return false; + } + + // Autosave no longer needed + AutoSaveDelete(); + + // Reaching this point defines success and all the rest are no-fail + // operations: + + // No longer modified + mModified = false; + + // No longer recovered + mRecovered = false; + + // No longer a temporary project + mTemporary = false; + + // Adjust the title + SetProjectTitle(); + + // Tell the finally block to behave + success = true; + +#else + // Create the project doc + ProjectSerializer doc; + WriteXMLHeader(doc); + WriteXML(doc, false, tracks); + + wxString origName = mFileName; + wxString tempName = origName + "_vacuum"; + + // Must close the database to rename it + if (!CloseDB()) + { + return; + } + + // Shouldn't need to do this, but doesn't hurt. + wxRemoveFile(tempName); + + // If we can't rename the original to temporary, backout + if (!wxRenameFile(origName, tempName)) + { + OpenDB(origName); + + return; + } + + // If we can't reopen the original database using the temporary name, backout + sqlite3 *tempDB = nullptr; + if (sqlite3_open(tempName, &tempDB) != SQLITE_OK) + { + SetDBError(XO("Failed to open project file")); + // sqlite3 docs say you should close anyway to avoid leaks + sqlite3_close( tempDB ); + + wxRenameFile(tempName, origName); + + OpenDB(origName); + + return; + } + UseConnection(tempDB, tempName); + + // Ensure connection gets configured + Config(mDB, SafeConfig); + + // Copy the original database to a new database while pruning unused sample blocks + auto newDB = CopyTo(origName, XO("Compacting project"), true, tracks); + + // Close handle to the original database + CloseDB(); + + // Reestablish the original name. No need to reopen as it will happen later, + // if needed. + UseConnection(newDB, origName); + + // If the copy failed or we aren't able to write the project doc, backout + if (!newDB || !WriteDoc("project", doc, newDB)) + { + // Close the new database + sqlite3_close(newDB); + + // AUD3 warn user somehow + wxRemoveFile(origName); + + // AUD3 warn user somehow + wxRenameFile(tempName, origName); + + UseConnection(nullptr, origName); + + return; + } + + wxRemoveFile(tempName); + + // Remember that we vacuumed + mWasVacuumed = true; +#endif + return; +} + +bool ProjectFileIO::WasVacuumed() +{ + return mWasVacuumed; +} + +bool ProjectFileIO::HadUnused() +{ + return mHadUnused; +} + +void ProjectFileIO::UpdatePrefs() +{ + SetProjectTitle(); +} + +// Pass a number in to show project number, or -1 not to. +void ProjectFileIO::SetProjectTitle(int number) +{ + auto pProject = mpProject.lock(); + if (! pProject ) + return; + + auto &project = *pProject; + auto pWindow = project.GetFrame(); + if (!pWindow) + { + return; + } + auto &window = *pWindow; + wxString name = project.GetProjectName(); + + // If we are showing project numbers, then we also explicitly show "" if there + // is none. + if (number >= 0) + { + /* i18n-hint: The %02i is the project number, the %s is the project name.*/ + name = wxString::Format(_("[Project %02i] Audacity \"%s\""), number + 1, + name.empty() ? "" : (const char *)name); + } + // If we are not showing numbers, then shows as 'Audacity'. + else if (name.empty()) + { + name = _TS("Audacity"); + } + + if (mRecovered) + { + name += wxT(" "); + /* i18n-hint: E.g this is recovered audio that had been lost.*/ + name += _("(Recovered)"); + } + + if (name != window.GetTitle()) + { + window.SetTitle( name ); + window.SetName(name); // to make the nvda screen reader read the correct title + + project.QueueEvent( + safenew wxCommandEvent{ EVT_PROJECT_TITLE_CHANGE } ); + } +} + +const FilePath &ProjectFileIO::GetFileName() const +{ + return mFileName; +} + +void ProjectFileIO::SetFileName(const FilePath &fileName) +{ + auto pProject = mpProject.lock(); + if (! pProject ) + return; + auto &project = *pProject; + + mFileName = fileName; + + if (mTemporary) + { + project.SetProjectName({}); + } + else + { + project.SetProjectName(wxFileName(mFileName).GetName()); + } + + SetProjectTitle(); +} + +bool ProjectFileIO::HandleXMLTag(const wxChar *tag, const wxChar **attrs) +{ + auto pProject = mpProject.lock(); + if (! pProject ) + return false; + auto &project = *pProject; + auto &window = GetProjectFrame(project); + auto &viewInfo = ViewInfo::Get(project); + auto &settings = ProjectSettings::Get(project); + + wxString fileVersion; + wxString audacityVersion; + int requiredTags = 0; + long longVpos = 0; + + // loop through attrs, which is a null-terminated list of + // attribute-value pairs + while (*attrs) + { + const wxChar *attr = *attrs++; + const wxChar *value = *attrs++; + + if (!value || !XMLValueChecker::IsGoodString(value)) + { + break; + } + + if (viewInfo.ReadXMLAttribute(attr, value)) + { + // We need to save vpos now and restore it below + longVpos = std::max(longVpos, long(viewInfo.vpos)); + continue; + } + + else if (!wxStrcmp(attr, wxT("version"))) + { + fileVersion = value; + requiredTags++; + } + + else if (!wxStrcmp(attr, wxT("audacityversion"))) + { + audacityVersion = value; + requiredTags++; + } + + else if (!wxStrcmp(attr, wxT("rate"))) + { + double rate; + Internat::CompatibleToDouble(value, &rate); + settings.SetRate( rate ); + } + + else if (!wxStrcmp(attr, wxT("snapto"))) + { + settings.SetSnapTo(wxString(value) == wxT("on") ? true : false); + } + + else if (!wxStrcmp(attr, wxT("selectionformat"))) + { + settings.SetSelectionFormat( + NumericConverter::LookupFormat( NumericConverter::TIME, value) ); + } + + else if (!wxStrcmp(attr, wxT("audiotimeformat"))) + { + settings.SetAudioTimeFormat( + NumericConverter::LookupFormat( NumericConverter::TIME, value) ); + } + + else if (!wxStrcmp(attr, wxT("frequencyformat"))) + { + settings.SetFrequencySelectionFormatName( + NumericConverter::LookupFormat( NumericConverter::FREQUENCY, value ) ); + } + + else if (!wxStrcmp(attr, wxT("bandwidthformat"))) + { + settings.SetBandwidthSelectionFormatName( + NumericConverter::LookupFormat( NumericConverter::BANDWIDTH, value ) ); + } + } // while + + if (longVpos != 0) + { + // PRL: It seems this must happen after SetSnapTo + viewInfo.vpos = longVpos; + } + + if (requiredTags < 2) + { + return false; + } + + // Parse the file version from the project + int fver; + int frel; + int frev; + if (!wxSscanf(fileVersion, wxT("%i.%i.%i"), &fver, &frel, &frev)) + { + return false; + } + + // Parse the file version Audacity was build with + int cver; + int crel; + int crev; + wxSscanf(wxT(AUDACITY_FILE_FORMAT_VERSION), wxT("%i.%i.%i"), &cver, &crel, &crev); + + if (cver < fver || crel < frel || crev < frev) + { + /* i18n-hint: %s will be replaced by the version number.*/ + auto msg = XO("This file was saved using Audacity %s.\nYou are using Audacity %s. You may need to upgrade to a newer version to open this file.") + .Format(audacityVersion, AUDACITY_VERSION_STRING); + + AudacityMessageBox( + msg, + XO("Can't open project file"), + wxOK | wxICON_EXCLAMATION | wxCENTRE, + &window); + + return false; + } + + if (wxStrcmp(tag, wxT("project"))) + { + return false; + } + + // All other tests passed, so we succeed + return true; +} + +XMLTagHandler *ProjectFileIO::HandleXMLChild(const wxChar *tag) +{ + auto pProject = mpProject.lock(); + if (! pProject ) + return nullptr; + auto &project = *pProject; + auto fn = ProjectFileIORegistry::Lookup(tag); + if (fn) + { + return fn(project); + } + + return nullptr; +} + +void ProjectFileIO::WriteXMLHeader(XMLWriter &xmlFile) const +{ + xmlFile.Write(wxT("\n")); + + xmlFile.Write(wxT("\n")); +} + +void ProjectFileIO::WriteXML(XMLWriter &xmlFile, + bool recording /* = false */, + const std::shared_ptr &tracks /* = nullptr */) +// may throw +{ + auto pProject = mpProject.lock(); + if (! pProject ) + THROW_INCONSISTENCY_EXCEPTION; + auto &proj = *pProject; + auto &tracklist = tracks ? *tracks : TrackList::Get(proj); + auto &viewInfo = ViewInfo::Get(proj); + auto &tags = Tags::Get(proj); + const auto &settings = ProjectSettings::Get(proj); + + //TIMER_START( "AudacityProject::WriteXML", xml_writer_timer ); + + xmlFile.StartTag(wxT("project")); + xmlFile.WriteAttr(wxT("xmlns"), wxT("http://audacity.sourceforge.net/xml/")); + + xmlFile.WriteAttr(wxT("version"), wxT(AUDACITY_FILE_FORMAT_VERSION)); + xmlFile.WriteAttr(wxT("audacityversion"), AUDACITY_VERSION_STRING); + + viewInfo.WriteXMLAttributes(xmlFile); + xmlFile.WriteAttr(wxT("rate"), settings.GetRate()); + xmlFile.WriteAttr(wxT("snapto"), settings.GetSnapTo() ? wxT("on") : wxT("off")); + xmlFile.WriteAttr(wxT("selectionformat"), + settings.GetSelectionFormat().Internal()); + xmlFile.WriteAttr(wxT("frequencyformat"), + settings.GetFrequencySelectionFormatName().Internal()); + xmlFile.WriteAttr(wxT("bandwidthformat"), + settings.GetBandwidthSelectionFormatName().Internal()); + + tags.WriteXML(xmlFile); + + unsigned int ndx = 0; + tracklist.Any().Visit([&](Track *t) + { + auto useTrack = t; + if ( recording ) { + // When append-recording, there is a temporary "shadow" track accumulating + // changes and displayed on the screen but it is not yet part of the + // regular track list. That is the one that we want to back up. + // SubstitutePendingChangedTrack() fetches the shadow, if the track has + // one, else it gives the same track back. + useTrack = t->SubstitutePendingChangedTrack().get(); + } + else if ( useTrack->GetId() == TrackId{} ) { + // This is a track added during a non-appending recording that is + // not yet in the undo history. The UndoManager skips backing it up + // when pushing. Don't auto-save it. + return; + } + useTrack->WriteXML(xmlFile); + }); + + xmlFile.EndTag(wxT("project")); + + //TIMER_STOP( xml_writer_timer ); +} + +bool ProjectFileIO::AutoSave(bool recording) +{ + ProjectSerializer autosave; + WriteXMLHeader(autosave); + WriteXML(autosave, recording); + + if (WriteDoc("autosave", autosave)) + { + mModified = true; + return true; + } + + return false; +} + +bool ProjectFileIO::AutoSaveDelete(sqlite3 *db /* = nullptr */) +{ + int rc; + + if (!db) + { + db = DB(); + } + + rc = sqlite3_exec(db, "DELETE FROM autosave;", nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Failed to remove the autosave information from the project file.") + ); + return false; + } + + mModified = false; + + return true; +} + +bool ProjectFileIO::WriteDoc(const char *table, + const ProjectSerializer &autosave, + sqlite3 *db /* = nullptr */) +{ + int rc; + + if (!db) + { + db = DB(); + } + + // For now, we always use an ID of 1. This will replace the previously + // writen row every time. + char sql[256]; + sqlite3_snprintf(sizeof(sql), + sql, + "INSERT INTO %s(id, dict, doc) VALUES(1, ?1, ?2)" + " ON CONFLICT(id) DO UPDATE SET dict = ?1, doc = ?2;", + table); + + sqlite3_stmt *stmt = nullptr; + auto cleanup = finally([&] + { + if (stmt) + { + sqlite3_finalize(stmt); + } + }); + + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Unable to prepare project file command:\n\n%s").Format(sql) + ); + return false; + } + + const wxMemoryBuffer &dict = autosave.GetDict(); + const wxMemoryBuffer &data = autosave.GetData(); + + // BIND SQL autosave + // Might return SQL_MISUSE which means it's our mistake that we violated + // preconditions; should return SQL_OK which is 0 + if ( + sqlite3_bind_blob(stmt, 1, dict.GetData(), dict.GetDataLen(), SQLITE_STATIC) || + sqlite3_bind_blob(stmt, 2, data.GetData(), data.GetDataLen(), SQLITE_STATIC) + ) + { + THROW_INCONSISTENCY_EXCEPTION; + } + + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) + { + SetDBError( + XO("Failed to update the project file.\nThe following command failed:\n\n%s").Format(sql) + ); + return false; + } + + return true; +} + +// Importing an AUP3 project into an AUP3 project is a bit different than +// normal importing since we need to copy data from one DB to the other +// while adjusting the sample block IDs to represent the newly assigned +// IDs. +bool ProjectFileIO::ImportProject(const FilePath &fileName) +{ + // Get access to the current project file + auto db = DB(); + + bool success = false; + bool restore = true; + int rc; + + // Ensure the inbound database gets detached + auto detach = finally([&] + { + sqlite3_exec(db, "DETACH DATABASE inbound;", nullptr, nullptr, nullptr); + }); + + // Attach the inbound project file + wxString sql; + sql.Printf("ATTACH DATABASE 'file:%s?immutable=1&mode=ro' AS inbound;", fileName); + + rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Unable to attach %s project file").Format(fileName) + ); + + return false; + } + + // We need either the autosave or project docs from the inbound AUP3 + wxMemoryBuffer buffer; + + // Get the autosave doc, if any + if (!GetBlob("SELECT dict || doc FROM inbound.project WHERE id = 1;", buffer)) + { + // Error already set + return false; + } + + // If we didn't have an autosave doc, load the project doc instead + if (buffer.GetDataLen() == 0) + { + if (!GetBlob("SELECT dict || doc FROM inbound.autosave WHERE id = 1;", buffer)) + { + // Error already set + return false; + } + + // Missing both the autosave and project docs...this shouldn't happen!!! + if (buffer.GetDataLen() > 0) + { + SetError(XO("Unable to load project or autosave documents")); + return false; + } + } + + wxString project; + BlockIDs blockids; + + // Decode it while capturing the associated sample blockids + project = ProjectSerializer::Decode(buffer, blockids); + if (project.size() == 0) + { + SetError(XO("Unable to decode project document")); + + return false; + } + + // Parse the project doc + wxStringInputStream in(project); + wxXmlDocument doc; + if (!doc.Load(in)) + { + return false; + } + + // Get the root ("project") node + wxXmlNode *root = doc.GetRoot(); + wxASSERT(root->GetName().IsSameAs(wxT("project"))); + + // Soft delete all non-essential attributes to prevent updating the active + // project. This takes advantage of the knowledge that when a project is + // parsed, unrecognized attributes are simply ignored. + // + // This is necessary because we don't want any of the active project settings + // to be modified by the inbound project. + for (wxXmlAttribute *attr = root->GetAttributes(); attr; attr = attr->GetNext()) + { + wxString name = attr->GetName(); + if (!name.IsSameAs(wxT("version")) && !name.IsSameAs(wxT("audacityversion"))) + { + attr->SetName(name + wxT("_deleted")); + } + } + + // Recursively find and collect all waveblock nodes + std::vector blocknodes; + std::function findblocks = [&](wxXmlNode *node) + { + while (node) + { + if (node->GetName().IsSameAs(wxT("waveblock"))) + { + blocknodes.push_back(node); + } + else + { + findblocks(node->GetChildren()); + } + + node = node->GetNext(); + } + }; + + // Get access to the active tracklist + auto pProject = mpProject.lock(); + if (!pProject) + { + return false; + } + auto &tracklist = TrackList::Get(*pProject); + + // Search for a timetrack and remove it if the project already has one + if (*tracklist.Any().begin()) + { + // Find a timetrack and remove it if it exists + for (wxXmlNode *node = doc.GetRoot()->GetChildren(); node; node = node->GetNext()) + { + if (node->GetName().IsSameAs(wxT("timetrack"))) + { + AudacityMessageBox( + XO("The active project already has a time track and one was encountered in the project being imported, bypassing imported time track."), + XO("Project Import"), + wxOK | wxICON_EXCLAMATION | wxCENTRE, + &GetProjectFrame(*pProject)); + + root->RemoveChild(node); + break; + } + } + } + + // Find all waveblocks in all wavetracks + for (wxXmlNode *node = doc.GetRoot()->GetChildren(); node; node = node->GetNext()) + { + if (node->GetName().IsSameAs(wxT("wavetrack"))) + { + findblocks(node->GetChildren()); + } + } + + { + // Cleanup... + sqlite3_stmt *stmt = nullptr; + auto cleanup = finally([&] + { + // Ensure the prepared statement gets cleaned up + if (stmt) + { + sqlite3_finalize(stmt); + } + }); + + // Prepare the statement to copy the sample block from the inbound project to the + // active project. All columns other than the blockid column gets copied. + wxString columns(wxT("sampleformat, summin, summax, sumrms, summary256, summary64k, samples")); + sql.Printf("INSERT INTO main.sampleblocks (%s)" + " SELECT %s" + " FROM inbound.sampleblocks" + " WHERE blockid = ?;", + columns, + columns); + + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Unable to prepare project file command:\n\n%s").Format(sql) + ); + return false; + } + + /* i18n-hint: This title appears on a dialog that indicates the progress + in doing something.*/ + ProgressDialog progress(XO("Progress"), XO("Importing project"), pdlgHideStopButton); + ProgressResult result = ProgressResult::Success; + + wxLongLong_t count = 0; + wxLongLong_t total = blocknodes.size(); + + // Copy all the sample blocks from the inbound project file into + // the active one, while remembering which were copied. + std::vector copied; + for (auto node : blocknodes) + { + // If the user cancelled the import or the import failed for some other reason + // make sure to back out the blocks copied to the active project file + auto backout = finally([&] + { + if (result == ProgressResult::Cancelled || result == ProgressResult::Failed) + { + for (auto blockid : copied) + { + wxString sql; + sql.Printf("DELETE FROM main.sampleblocks WHERE blockid = %lld", blockid); + + rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + { + // This is non-fatal...it'll just get cleaned up the next + // time the project is opened. + SetDBError( + XO("Failed to delete block while cancelling import") + ); + } + } + } + }); + + // Find the blockid attribute...it should always be there + wxXmlAttribute *attr = node->GetAttributes(); + while (attr && !attr->GetName().IsSameAs(wxT("blockid"))) + { + attr = attr->GetNext(); + } + wxASSERT(attr != nullptr); + + // And get the blockid + SampleBlockID blockid; + attr->GetValue().ToLongLong(&blockid); + + // BIND blockid parameter + if (sqlite3_bind_int64(stmt, 1, blockid) != SQLITE_OK) + { + THROW_INCONSISTENCY_EXCEPTION; + } + + // Process it + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) + { + SetDBError( + XO("Failed to import sample block.\nThe following command failed:\n\n%s").Format(sql) + ); + return false; + } + + // Replace the original blockid with the new one + attr->SetValue(wxString::Format(wxT("%lld"), sqlite3_last_insert_rowid(db))); + + // Reset the statement for the next iteration + if (sqlite3_reset(stmt) != SQLITE_OK) + { + THROW_INCONSISTENCY_EXCEPTION; + } + + // Remember that we copied this node in case the user cancels + result = progress.Update(++count, total); + if (result != ProgressResult::Success) + { + break; + } + } + + // Bail if the import was cancelled or failed. If the user stopped the + // import or it completed, then we continue on. + if (result == ProgressResult::Cancelled || result == ProgressResult::Failed) + { + return false; + } + + // Copy over tags...likely to produce duplicates...needs work once used + rc = sqlite3_exec(db, + "INSERT INTO main.tags SELECT * FROM inbound.tags;", + nullptr, + nullptr, + nullptr); + if (rc != SQLITE_OK) + { + SetDBError( + XO("Failed to import tags") + ); + + return false; + } + } + + // Recreate the project doc with the revisions we've made above + wxStringOutputStream output; + doc.Save(output); + + // Now load the document as normal + XMLFileReader xmlFile; + if (!xmlFile.ParseString(this, output.GetString())) + { + SetError( + XO("Unable to parse project information.") + ); + mLibraryError = xmlFile.GetErrorStr(); + + return false; + } + + return true; +} + +bool ProjectFileIO::LoadProject(const FilePath &fileName) +{ + bool success = false; + + auto cleanup = finally([&] + { + if (!success) + { + RestoreConnection(); + } + }); + + SaveConnection(); + + // Open the project file + if (!OpenDB(fileName)) + { + return false; + } + + BlockIDs blockids; + wxString project; + wxMemoryBuffer buffer; + bool usedAutosave = true; + + // Get the autosave doc, if any + if (!GetBlob("SELECT dict || doc FROM autosave WHERE id = 1;", buffer)) + { + // Error already set + return false; + } + + // If we didn't have an autosave doc, load the project doc instead + if (buffer.GetDataLen() == 0) + { + usedAutosave = false; + + if (!GetBlob("SELECT dict || doc FROM project WHERE id = 1;", buffer)) + { + // Error already set + return false; + } + + if (buffer.GetDataLen() == 0) + { + SetError(XO("Unable to load project or autosave documents")); + return false; + } + } + + // Decode it while capturing the associated sample blockids + project = ProjectSerializer::Decode(buffer, blockids); + if (project.empty()) + { + SetError(XO("Unable to decode project document")); + + return false; + } + + // Check for orphans blocks...sets mRecovered if any were deleted + if (blockids.size() > 0) + { + if (!CheckForOrphans(blockids)) + { + return false; + } + } + + XMLFileReader xmlFile; + + // Load 'er up + success = xmlFile.ParseString(this, project); + if (!success) + { + SetError( + XO("Unable to parse project information.") + ); + mLibraryError = xmlFile.GetErrorStr(); + return false; + } + + // Remember if we used autosave or not + if (usedAutosave) + { + mRecovered = true; + } + + // Mark the project modified if we recovered it + if (mRecovered) + { + mModified = true; + } + + // A previously saved project will have a document in the project table, so + // we use that knowledge to determine if this file is an unsaved/temporary + // file or a permanent project file + wxString result; + if (!GetValue("SELECT Count(*) FROM project;", result)) + { + return false; + } + + mTemporary = (wxStrtol(result, nullptr, 10) != 1); + + SetFileName(fileName); + + DiscardConnection(); + + return true; +} + +bool ProjectFileIO::SaveProject(const FilePath &fileName) +{ + wxString origName; + bool wasTemp = false; + bool success = false; + + // Should probably simplify all of the following by using renames. + + auto restore = finally([&] + { + if (!origName.empty()) + { + if (success) + { + // The Save was successful, so now it is safe to abandon the + // original connection + DiscardConnection(); + + // And also remove the original file if it was a temporary file + if (wasTemp) + { + wxRemoveFile(origName); + } + } + else + { + // Close the new database and go back to using the original + // connection + RestoreConnection(); + + // And delete the new database + wxRemoveFile(fileName); + } + } + }); + + // If we're saving to a different file than the current one, then copy the + // current to the new file and make it the active file. + if (mFileName != fileName) + { + // Do NOT prune here since we need to retain the Undo history + // after we switch to the new file. + auto newDB = CopyTo(fileName, XO("Saving project")); + if (!newDB) + { + return false; + } + + // Remember the original project filename and temporary status. Only do + // this after a successful copy so the "finally" block above doesn't monkey + // with the files. + origName = mFileName; + wasTemp = mTemporary; + + // Save the original database connection and try to switch to a new one + // (also ensuring closing of one of the connections, with the cooperation + // of the finally above) + SaveConnection(); + + // Make the new connection "safe" + Config(newDB, SafeConfig); + + // And make it the active project file + UseConnection(newDB, fileName); + + // Install our checkpoint hook + sqlite3_wal_hook(mDB, CheckpointHook, this); + } + else + { + ProjectSerializer doc; + WriteXMLHeader(doc); + WriteXML(doc); + + if (!WriteDoc("project", doc)) + { + return false; + } + + // Autosave no longer needed + AutoSaveDelete(); + } + + // Reaching this point defines success and all the rest are no-fail + // operations: + + // No longer modified + mModified = false; + + // No longer recovered + mRecovered = false; + + // No longer a temporary project + mTemporary = false; + + // Adjust the title + SetProjectTitle(); + + // Tell the finally block to behave + success = true; + + return true; +} + +bool ProjectFileIO::SaveCopy(const FilePath& fileName) +{ + auto db = CopyTo(fileName, XO("Backing up project"), true); + if (!db) + { + return false; + } + + // All good...close the database + (void) sqlite3_close(db); + + return true; +} + +bool ProjectFileIO::CloseProject() +{ + if (mDB) + { + // Save the filename since CloseDB() will clear it + wxString filename = mFileName; + + // Not much we can do if this fails. The user will simply get + // the recovery dialog upon next restart. + if (CloseDB()) + { + // If this is a temporary project, we no longer want to keep the + // project file. + if (mTemporary) + { + // This is just a safety check. + wxFileName temp(FileNames::TempDir()); + if (temp == wxPathOnly(filename)) + { + wxRemoveFile(filename); + } + } + } + } + + return true; +} + +bool ProjectFileIO::IsModified() const +{ + return mModified; +} + +bool ProjectFileIO::IsTemporary() const +{ + return mTemporary; +} + +bool ProjectFileIO::IsRecovered() const +{ + return mRecovered; +} + +void ProjectFileIO::Reset() +{ + wxASSERT_MSG(mDB == nullptr, wxT("Resetting project with open project file")); + + mModified = false; + mRecovered = false; + + SetFileName({}); +} + +wxLongLong ProjectFileIO::GetFreeDiskSpace() +{ + // make sure it's open and the path is defined + auto db = DB(); + + wxLongLong freeSpace; + if (wxGetDiskSpace(wxPathOnly(mFileName), NULL, &freeSpace)) + { + return freeSpace; + } + + return -1; +} + +const TranslatableString & ProjectFileIO::GetLastError() const +{ + return mLastError; +} + +const TranslatableString & ProjectFileIO::GetLibraryError() const +{ + return mLibraryError; +} + +void ProjectFileIO::SetError(const TranslatableString &msg) +{ + mLastError = msg; + mLibraryError = {}; +} + +void ProjectFileIO::SetDBError(const TranslatableString &msg) +{ + mLastError = msg; + wxLogDebug(wxT("SQLite error: %s"), mLastError.Debug()); + printf(" Lib error: %s", mLastError.Debug().mb_str().data()); + + if (mDB) + { + mLibraryError = Verbatim(sqlite3_errmsg(mDB)); + wxLogDebug(wxT(" Lib error: %s"), mLibraryError.Debug()); + printf(" Lib error: %s", mLibraryError.Debug().mb_str().data()); + } + abort(); + wxASSERT(false); +} + +void ProjectFileIO::SetBypass() +{ + // Determine if we can bypass sample block deletes during shutdown. + // + // IMPORTANT: + // If the project was vacuumed, then we MUST bypass further + // deletions since the new file doesn't have the blocks that the + // Sequences expect to be there. + mBypass = true; + + // Only permanent project files need cleaning at shutdown + if (!IsTemporary() && !WasVacuumed()) + { + // If we still have unused blocks, then we must not bypass deletions + // during shutdown. Otherwise, we would have orphaned blocks the next time + // the project is opened. + // + // An example of when dead blocks will exist is when a user opens a permanent + // project, adds a track (with samples) to it, and chooses not to save the + // changes. + if (HadUnused()) + { + mBypass = false; + } + } + + return; +} + +bool ProjectFileIO::ShouldBypass() +{ + return mBypass; +} + +AutoCommitTransaction::AutoCommitTransaction(ProjectFileIO &projectFileIO, + const char *name) +: mIO(projectFileIO), + mName(name) +{ + mInTrans = mIO.TransactionStart(mName); + // Must throw +} + +AutoCommitTransaction::~AutoCommitTransaction() +{ + if (mInTrans) + { + // Can't check return status...should probably throw an exception here + if (!Commit()) + { + // must throw + } + } +} + +bool AutoCommitTransaction::Commit() +{ + wxASSERT(mInTrans); + + mInTrans = !mIO.TransactionCommit(mName); + + return mInTrans; +} + +bool AutoCommitTransaction::Rollback() +{ + wxASSERT(mInTrans); + + mInTrans = !mIO.TransactionCommit(mName); + + return mInTrans; +} diff --git a/src/ProjectFileIO.h.bkp.h b/src/ProjectFileIO.h.bkp.h new file mode 100644 index 000000000..b839e0d2e --- /dev/null +++ b/src/ProjectFileIO.h.bkp.h @@ -0,0 +1,278 @@ +/********************************************************************** + +Audacity: A Digital Audio Editor + +ProjectFileIO.h + +Paul Licameli split from AudacityProject.h + +**********************************************************************/ + +#ifndef __AUDACITY_PROJECT_FILE_IO__ +#define __AUDACITY_PROJECT_FILE_IO__ + +#include +#include +#include +#include +#include +#include + +#include "ClientData.h" // to inherit +#include "Prefs.h" // to inherit +#include "xml/XMLTagHandler.h" // to inherit + +struct sqlite3; +struct sqlite3_context; +struct sqlite3_value; + +class AudacityProject; +class AutoCommitTransaction; +class ProjectSerializer; +class SqliteSampleBlock; +class TrackList; +class WaveTrack; + +using WaveTrackArray = std::vector < std::shared_ptr < WaveTrack > >; + +// From SampleBlock.h +using SampleBlockID = long long; + +///\brief Object associated with a project that manages reading and writing +/// of Audacity project file formats, and autosave +class ProjectFileIO final + : public ClientData::Base + , public XMLTagHandler + , private PrefsListener + , public std::enable_shared_from_this +{ +public: + // Call this static function once before constructing any instances of this + // class. Reinvocations have no effect. Return value is true for success. + static bool InitializeSQL(); + + static ProjectFileIO &Get( AudacityProject &project ); + static const ProjectFileIO &Get( const AudacityProject &project ); + + explicit ProjectFileIO( AudacityProject &project ); + // unfortunate two-step construction needed because of + // enable_shared_from_this + void Init( AudacityProject &project ); + + ProjectFileIO( const ProjectFileIO & ) PROHIBITED; + ProjectFileIO &operator=( const ProjectFileIO & ) PROHIBITED; + ~ProjectFileIO(); + + // It seems odd to put this method in this class, but the results do depend + // on what is discovered while opening the file, such as whether it is a + // recovery file + void SetProjectTitle(int number = -1); + // Should be empty or a fully qualified file name + + const FilePath &GetFileName() const; + void SetFileName( const FilePath &fileName ); + + bool IsModified() const; + bool IsTemporary() const; + bool IsRecovered() const; + + void Reset(); + + bool AutoSave(bool recording = false); + bool AutoSaveDelete(sqlite3 *db = nullptr); + + bool ImportProject(const FilePath &fileName); + bool LoadProject(const FilePath &fileName); + bool SaveProject(const FilePath &fileName); + bool SaveCopy(const FilePath& fileName); + bool CloseProject(); + + wxLongLong GetFreeDiskSpace(); + + const TranslatableString &GetLastError() const; + const TranslatableString &GetLibraryError() const; + + // Provides a means to bypass "DELETE"s at shutdown if the database + // is just going to be deleted anyway. This prevents a noticable + // delay caused by SampleBlocks being deleted when the Sequences that + // own them are deleted. + // + // This is definitely hackage territory. While this ability would + // still be needed, I think handling it in a DB abstraction might be + // a tad bit cleaner. + // + // For it's usage, see: + // SqliteSampleBlock::~SqliteSampleBlock() + // ProjectManager::OnCloseWindow() + void SetBypass(); + bool ShouldBypass(); + + // Remove all unused space within a project file + void Vacuum(const std::shared_ptr &tracks); + + // The last vacuum check did actually vacuum the project file if true + bool WasVacuumed(); + + // The last vacuum check found unused blocks in the project file + bool HadUnused(); + + bool TransactionStart(const wxString &name); + bool TransactionCommit(const wxString &name); + bool TransactionRollback(const wxString &name); + +private: + void WriteXMLHeader(XMLWriter &xmlFile) const; + void WriteXML(XMLWriter &xmlFile, bool recording = false, const std::shared_ptr &tracks = nullptr) /* not override */; + + // XMLTagHandler callback methods + bool HandleXMLTag(const wxChar *tag, const wxChar **attrs) override; + XMLTagHandler *HandleXMLChild(const wxChar *tag) override; + + void UpdatePrefs() override; + + using ExecResult = std::vector>; + using ExecCB = std::function; + struct ExecParm + { + ExecCB func; + ExecResult &result; + }; + static int ExecCallback(void *data, int cols, char **vals, char **names); + int Exec(const char *query, ExecCB callback, ExecResult &result); + + // The opening of the database may be delayed until demanded. + // Returns a non-null pointer to an open database, or throws an exception + // if opening fails. + sqlite3 *DB(); + + // Put the current database connection aside, keeping it open, so that + // another may be opened with OpenDB() + void SaveConnection(); + + // Close any set-aside connection + void DiscardConnection(); + + // Close any current connection and switch back to using the saved + void RestoreConnection(); + + // Use a connection that is already open rather than invoke OpenDB + void UseConnection(sqlite3 *db, const FilePath &filePath); + + // Make sure the connection/schema combo is configured the way we want + void Config(sqlite3 *db, const char *config, const wxString &schema = wxT("main")); + + sqlite3 *OpenDB(FilePath fileName = {}); + bool CloseDB(); + bool DeleteDB(); + + bool Query(const char *sql, ExecResult &result); + + bool GetValue(const char *sql, wxString &value); + bool GetBlob(const char *sql, wxMemoryBuffer &buffer); + + bool CheckVersion(); + bool InstallSchema(sqlite3 *db, const char *schema = "main"); + bool UpgradeSchema(); + + // Write project or autosave XML (binary) documents + bool WriteDoc(const char *table, const ProjectSerializer &autosave, sqlite3 *db = nullptr); + + // Application defined function to verify blockid exists is in set of blockids + using BlockIDs = std::set; + static void InSet(sqlite3_context *context, int argc, sqlite3_value **argv); + + // Checks for orphan blocks. This will go away at a future date + bool CheckForOrphans(BlockIDs &blockids); + + // Return a database connection if successful, which caller must close + sqlite3 *CopyTo(const FilePath &destpath, + const TranslatableString &msg, + bool prune = false, + const std::shared_ptr &tracks = nullptr); + + void SetError(const TranslatableString & msg); + void SetDBError(const TranslatableString & msg); + + bool ShouldVacuum(const std::shared_ptr &tracks); + + void CheckpointThread(); + static int CheckpointHook(void *that, sqlite3 *db, const char *schema, int pages); + +private: + // non-static data members + std::weak_ptr mpProject; + + // The project's file path + FilePath mFileName; + + // Has this project been recovered from an auto-saved version + bool mRecovered; + + // Has this project been modified + bool mModified; + + // Is this project still a temporary/unsaved project + bool mTemporary; + + // Bypass transactions if database will be deleted after close + bool mBypass; + + // Project was vacuumed last time Vacuum() ran + bool mWasVacuumed; + + // Project had unused blocks during last Vacuum() + bool mHadUnused; + + sqlite3 *mPrevDB; + FilePath mPrevFileName; + + sqlite3 *mDB; + TranslatableString mLastError; + TranslatableString mLibraryError; + + std::thread mCheckpointThread; + std::condition_variable mCheckpointCondition; + std::mutex mCheckpointMutex; + std::mutex mCheckpointActive; + std::mutex mCheckpointClose; + std::atomic_bool mCheckpointStop; + uint64_t mCheckpointWaitingPages; + uint64_t mCheckpointCurrentPages; + + friend SqliteSampleBlock; + friend AutoCommitTransaction; +}; + +class AutoCommitTransaction +{ +public: + AutoCommitTransaction(ProjectFileIO &projectFileIO, const char *name); + ~AutoCommitTransaction(); + + bool Commit(); + bool Rollback(); + +private: + ProjectFileIO &mIO; + bool mInTrans; + wxString mName; +}; + +class wxTopLevelWindow; + +// TitleRestorer restores project window titles to what they were, in its destructor. +class TitleRestorer{ +public: + TitleRestorer( wxTopLevelWindow &window, AudacityProject &project ); + ~TitleRestorer(); + wxString sProjNumber; + wxString sProjName; + size_t UnnamedCount; +}; + +// This event is emitted by the project when there is a change +// in its title +wxDECLARE_EXPORTED_EVENT(AUDACITY_DLL_API, + EVT_PROJECT_TITLE_CHANGE, wxCommandEvent); + +#endif