/********************************************************************** Audacity: A Digital Audio Editor ProjectFileIO.cpp Paul Licameli split from AudacityProject.cpp **********************************************************************/ #include "ProjectFileIO.h" #include #include #include "AutoRecovery.h" #include "FileNames.h" #include "Project.h" #include "ProjectFileIORegistry.h" #include "ProjectSettings.h" #include "Tags.h" #include "ViewInfo.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; static const int ProjectFilePageSize = 4096; // 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 = "PRAGMA application_id = %d;" "PRAGMA user_version = %d;" "PRAGMA page_size = %d;" "PRAGMA journal_mode = DELETE;" "" // CREATE SQL project // doc is a variable sized XML text string. // it is the former Audacity .aup file // One instance only. "CREATE TABLE IF NOT EXISTS project" "(" " id INTEGER PRIMARY KEY," " doc TEXT" ");" "" // 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 float32 numbers. // The blocks may be partially empty. // The quantity of valid data in the blocks is // provided in the project XML. // // sampleformat was once used to specify whether floats // or ints for the data, but is no longer used. // // 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" ");"; // 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() { sqlite3_initialize(); } ~SQLiteIniter() { sqlite3_shutdown(); } }; static SQLiteIniter sqliteIniter; 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 ) { return project.AttachedObjects::Get< ProjectFileIO >( sFileIOKey ); } const ProjectFileIO &ProjectFileIO::Get( const AudacityProject &project ) { return Get( const_cast< AudacityProject & >( project ) ); } ProjectFileIO::ProjectFileIO(AudacityProject &project) : mProject(project) { mDB = nullptr; mRecovered = false; mModified = false; mTemporary = true; UpdatePrefs(); } ProjectFileIO::~ProjectFileIO() { 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()) { // Always remove the journal now that the DB is closed wxRemoveFile(filename + wxT("-journal")); // At this point, we are shutting down cleanly and if the project file is // still in the temp directory it means that the user has chosen not to // save it. So, delete it. if (mTemporary) { wxFileName temp(FileNames::TempDir()); if (temp == wxPathOnly(filename)) { wxRemoveFile(filename); } } } } } sqlite3 *ProjectFileIO::DB() { if (mDB) { return mDB; } return OpenDB(); } 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")); return nullptr; } if (!CheckVersion()) { CloseDB(); return nullptr; } mTemporary = temp; SetFileName(fileName); return mDB; } bool ProjectFileIO::CloseDB() { int rc; if (mDB) { rc = sqlite3_close(mDB); if (rc != SQLITE_OK) { SetDBError(XO("Failed to close the project file")); } else { wxRemoveFile(mFileName + wxT("-journal")); } 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; } wxRemoveFile(mFileName + wxT("-journal")); } } return true; } bool ProjectFileIO::CleanDB() { auto db = DB(); int rc; AutoSave(); wxString destpath = wxFileName::CreateTempFileName(mFileName + ".xxxxx"); if (CopyTo(destpath)) { if (CloseDB()) { // This can be removed even if we fail below since the DB is closed wxRemoveFile(mFileName + wxT("-journal")); wxString tmppath = wxFileName::CreateTempFileName(mFileName + ".xxxxx"); if (wxRename(mFileName, tmppath) == 0) { if (wxRename(destpath, mFileName) == 0) { wxRemoveFile(tmppath); // Success return true; } if (wxRename(tmppath, mFileName) == 0) { wxRemoveFile(destpath); } else { SetError(XO("Could not rename %s back to %s during cleaning").Format(tmppath, mFileName)); } } else { SetError(XO("Could not rename %s during cleaning").Format(mFileName)); } } } // AUD3 TODO COMPILAIN return false; } /* 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, wxString *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; } wxString ProjectFileIO::GetValue(const char *sql) { auto getresult = [&](wxString *result, int cols, char **vals, char **names) { if (cols == 1 && vals[0]) { result->append(vals[0]); return SQLITE_OK; } return SQLITE_ABORT; }; wxString value; int rc = Exec(sql, getresult, &value); if (rc != SQLITE_OK) { // Message already captured } return value; } bool ProjectFileIO::GetBlob(const char *sql, wxMemoryBuffer &buffer) { auto db = DB(); int rc; sqlite3_stmt *stmt = nullptr; auto cleanup = finally([&] { if (stmt) { sqlite3_finalize(stmt); } }); rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0); if (rc != SQLITE_OK) { SetDBError( XO("Unable to prepare project file command:\n\n%s").Format(sql) ); return false; } rc = sqlite3_step(stmt); 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 long count = -1; GetValue("SELECT Count(*) FROM sqlite_master WHERE type='table';").ToLong(&count); if (count == -1) { return false; } // If the return count is zero, then there are no tables defined, so this // must be a new project file. if (count == 0) { return InstallSchema(); } // Check for our application ID long appid = -1; GetValue("PRAGMA application_ID;").ToLong(&appid); if (appid == -1) { return false; } // It's a database that SQLite recognizes, but it's not one of ours if (appid != ProjectFileID) { SetError(XO("This is not an Audacity project file")); return false; } // Get the project file version long version = -1; GetValue("PRAGMA user_version;").ToLong(&version); if (version == -1) { return false; } // 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() { auto db = DB(); int rc; char sql[1024]; sqlite3_snprintf(sizeof(sql), sql, ProjectFileSchema, ProjectFileID, ProjectFileVersion, ProjectFilePageSize); 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; } bool ProjectFileIO::CopyTo(const FilePath &destpath) { auto db = DB(); int rc; bool success = true; ProgressResult res = ProgressResult::Success; sqlite3 *destdb = nullptr; /* Open the database file identified by zFilename. */ rc = sqlite3_open(destpath, &destdb); if (rc == SQLITE_OK) { sqlite3_backup *backup = sqlite3_backup_init(destdb, "main", db, "main"); if (backup) { /* i18n-hint: This title appears on a dialog that indicates the progress in doing something.*/ ProgressDialog progress(XO("Progress"), XO("Saving project")); do { int remaining = sqlite3_backup_remaining(backup); int total = sqlite3_backup_pagecount(backup); if (progress.Update(total - remaining, total) != ProgressResult::Success) { SetError( XO("Copy processs cancelled.") ); success = false; break; } rc = sqlite3_backup_step(backup, 12); } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED); // The return code from finish() will reflect errors from step() rc = sqlite3_backup_finish(backup); if (rc != SQLITE_OK) { SetDBError( XO("The copy process failed for:\n\n%s").Format(destpath) ); success = false; } } else { SetDBError( XO("Unable to initiate the backup process.") ); } // Close the DB rc = sqlite3_close(destdb); if (rc != SQLITE_OK) { SetDBError( XO("Failed to successfully close the destination project file:\n\n%s") ); success = false; } // Always delete the journal wxRemoveFile(destpath + wxT("-journal")); if (!success) { wxRemoveFile(destpath); } } else { SetDBError( XO("Unable to open the destination project file:\n\n%s").Format(destpath) ); success = false; } return success; } void ProjectFileIO::UpdatePrefs() { SetProjectTitle(); } // Pass a number in to show project number, or -1 not to. void ProjectFileIO::SetProjectTitle(int number) { auto &project = mProject; 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) { mFileName = fileName; if (mTemporary) { mProject.SetProjectName({}); } else { mProject.SetProjectName(wxFileName(mFileName).GetName()); } SetProjectTitle(); } bool ProjectFileIO::HandleXMLTag(const wxChar *tag, const wxChar **attrs) { auto &project = mProject; 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 fn = ProjectFileIORegistry::Lookup(tag); if (fn) { return fn(mProject); } return nullptr; } void ProjectFileIO::WriteXMLHeader(XMLWriter &xmlFile) const { xmlFile.Write(wxT("\n")); xmlFile.Write(wxT("\n")); } void ProjectFileIO::WriteXML(XMLWriter &xmlFile, const WaveTrackArray *tracks) // may throw { auto &proj = mProject; auto &tracklist = 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; if (tracks) { for (auto track : *tracks) { track->WriteXML(xmlFile); } } else { tracklist.Any().Visit([&](Track *t) { t->WriteXML(xmlFile); }); } xmlFile.EndTag(wxT("project")); //TIMER_STOP( xml_writer_timer ); } bool ProjectFileIO::AutoSave(const WaveTrackArray *tracks) { AutoSaveFile autosave; WriteXMLHeader(autosave); WriteXML(autosave, tracks); return AutoSave(autosave); } bool ProjectFileIO::AutoSave(const AutoSaveFile &autosave) { auto db = DB(); int rc; mModified = true; // 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 autosave(id, dict, doc) VALUES(1, ?1, ?2)" " ON CONFLICT(id) DO UPDATE SET %sdoc = ?2;", autosave.DictChanged() ? "dict = ?1, " : ""); sqlite3_stmt *stmt = nullptr; auto cleanup = finally([&] { if (stmt) { sqlite3_finalize(stmt); } }); rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0); 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 sqlite3_bind_blob(stmt, 1, dict.GetData(), dict.GetDataLen(), SQLITE_STATIC); sqlite3_bind_blob(stmt, 2, data.GetData(), data.GetDataLen(), SQLITE_STATIC); 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; } bool ProjectFileIO::AutoSaveDelete() { auto db = DB(); int rc; 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; } return true; } bool ProjectFileIO::LoadProject(const FilePath &fileName) { bool success = false; // OpenDB() will change mFileName if the file opens/verifies successfully, // so we must set it back to what it was if any errors are encountered here. wxString oldFilename = mFileName; auto cleanup = finally([&] { if (!success) { CloseDB(); SetFileName(oldFilename); } }); // Open the project file if (!OpenDB(fileName)) { return false; } // Get the XML document...either from the project or autosave wxString doc; // Get the autosave doc, if any bool wasAutosave = false; wxMemoryBuffer buffer; if (GetBlob("SELECT dict || doc FROM autosave WHERE id = 1;", buffer)) { doc = AutoSaveFile::Decode(buffer); wasAutosave = true; } // Otherwise, get the project doc else { doc = GetValue("SELECT doc FROM project;"); } if (doc.empty()) { return false; } XMLFileReader xmlFile; success = xmlFile.ParseString(this, doc); if (!success) { SetError( XO("Unable to parse project information.") ); mLibraryError = xmlFile.GetErrorStr(); return false; } // Remember if it was recovered or not mRecovered = wasAutosave; 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 not long count = 0; GetValue("SELECT Count(*) FROM project;").ToLong(&count); mTemporary = (count != 1); SetFileName(fileName); return true; } bool ProjectFileIO::SaveProject(const FilePath &fileName) { wxString origName; bool wasTemp = false; bool success = false; // Should probably simply all of the by using renames. But, one benefit // of using CopyTo() for new file saves, is that it will be VACUUMED at // the same time. auto restore = finally([&] { if (!origName.empty()) { if (success) { // The Save was successful, so remove the original file if // it was a temporary file if (wasTemp) { wxRemoveFile(origName); } } else { // Close the new database CloseDB(); // Reopen the original database if (OpenDB(origName)) { // 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) { if (!CopyTo(fileName)) { 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; // Close the original project file and open the new one if (!CloseDB() || !OpenDB(fileName)) { return false; } } auto db = DB(); int rc; XMLStringWriter doc; WriteXMLHeader(doc); WriteXML(doc); // Always use an ID of 1. This will replace any existing row. char sql[256]; sqlite3_snprintf(sizeof(sql), sql, "INSERT INTO project(id, doc) VALUES(1, ?1)" " ON CONFLICT(id) DO UPDATE SET doc = ?1;"); sqlite3_stmt *stmt = nullptr; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0); if (rc != SQLITE_OK) { SetDBError( XO("Unable to prepare project file command:\n\n%s").Format(sql) ); return false; } // BIND SQL project rc = sqlite3_bind_text(stmt, 1, doc, -1, SQLITE_STATIC); if (rc != SQLITE_OK) { SetDBError( XO("Unable to bind to project file document.") ); return false; } rc = sqlite3_step(stmt); // This will free the statement sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { SetDBError( XO("Failed to save project file information.") ); return false; } // We need to remove the autosave info from the file since it is now // clean and unmodified. Otherwise, it would be considered "recovered" // when next opened. AutoSaveDelete(); // 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::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 mLastError; } void ProjectFileIO::SetError(const TranslatableString & msg) { mLastError = msg; mLibraryError = {}; } void ProjectFileIO::SetDBError(const TranslatableString & msg) { mLastError = msg; if (mDB) { mLibraryError = Verbatim(sqlite3_errmsg(mDB)); } }