audacia/src/ProjectFileIO.cpp

2702 lines
77 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
ProjectFileIO.cpp
Paul Licameli split from AudacityProject.cpp
**********************************************************************/
#include "ProjectFileIO.h"
#include <atomic>
#include <sqlite3.h>
#include <wx/crt.h>
#include <wx/frame.h>
#include <wx/progdlg.h>
#include <wx/sstream.h>
#include <wx/xml/xml.h>
#include "ActiveProjects.h"
#include "CodeConversions.h"
#include "DBConnection.h"
#include "Project.h"
#include "ProjectFileIORegistry.h"
#include "ProjectSerializer.h"
#include "ProjectSettings.h"
#include "SampleBlock.h"
#include "Tags.h"
#include "TempDirectory.h"
#include "ViewInfo.h"
#include "WaveTrack.h"
#include "widgets/AudacityMessageBox.h"
#include "widgets/ErrorDialog.h"
#include "widgets/NumericTextCtrl.h"
#include "widgets/ProgressDialog.h"
#include "wxFileNameWrapper.h"
#include "xml/XMLFileReader.h"
#include "SentryHelper.h"
// Don't change this unless the file format changes
// in an irrevocable way
#define AUDACITY_FILE_FORMAT_VERSION "1.3.0"
#undef NO_SHM
#if !defined(__WXMSW__)
#define NO_SHM
#endif
wxDEFINE_EVENT(EVT_PROJECT_TITLE_CHANGE, wxCommandEvent);
wxDEFINE_EVENT( EVT_CHECKPOINT_FAILURE, wxCommandEvent);
wxDEFINE_EVENT( EVT_RECONNECTION_FAILURE, wxCommandEvent);
// Used to convert 4 byte-sized values into an integer for use in SQLite
// PRAGMA statements. These values will be store in the database header.
//
// Note that endianness is not an issue here since SQLite integers are
// architecture independent.
#define PACK(b1, b2, b3, b4) ((b1 << 24) | (b2 << 16) | (b3 << 8) | b4)
// The ProjectFileID is stored in the SQLite database header to identify the file
// as an Audacity project file. It can be used by applications that identify file
// types, such as the Linux "file" command.
static const int ProjectFileID = PACK('A', 'U', 'D', 'Y');
// The "ProjectFileVersion" represents the version of Audacity at which a specific
// database schema was used. It is assumed that any changes to the database schema
// will require a new Audacity version so if schema changes are required set this
// to the new release being produced.
//
// This version is checked before accessing any tables in the database since there's
// no guarantee what tables exist. If it's found that the database is newer than the
// currently running Audacity, an error dialog will be displayed informing the user
// that they need a newer version of Audacity.
//
// Note that this is NOT the "schema_version" that SQLite maintains. The value
// specified here is stored in the "user_version" field of the SQLite database
// header.
static const int ProjectFileVersion = PACK(3, 0, 0, 0);
// 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 <schema>.application_id = %d;"
"PRAGMA <schema>.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 <schema>.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 <schema>.autosave"
"("
" id INTEGER PRIMARY KEY,"
" dict BLOB,"
" doc 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 blob.
//
// sampleformat specifies the format of the samples stored.
//
// blockID is a 64 bit number.
//
// Rows are immutable -- never updated after addition, but may be
// deleted.
//
// summin to summary64K are summaries at 3 distance scales.
"CREATE TABLE IF NOT EXISTS <schema>.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()
{
// Enable URI filenames for all connections
mRc = sqlite3_config(SQLITE_CONFIG_URI, 1);
if (mRc == SQLITE_OK)
{
mRc = sqlite3_config(SQLITE_CONFIG_LOG, LogCallback, nullptr);
if (mRc == SQLITE_OK)
{
mRc = sqlite3_initialize();
}
}
#ifdef NO_SHM
if (mRc == SQLITE_OK)
{
// Use the "unix-excl" VFS to make access to the DB exclusive. This gets
// rid of the "<database name>-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();
}
static void LogCallback(void *WXUNUSED(arg), int code, const char *msg)
{
wxLogMessage("sqlite3 message: (%d) %s", code, msg);
}
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 = _("<untitled>");
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 &project)
: mProject{ project }
, mpErrors{ std::make_shared<DBConnectionErrors>() }
{
mPrevConn = nullptr;
mRecovered = false;
mModified = false;
mTemporary = true;
UpdatePrefs();
}
ProjectFileIO::~ProjectFileIO()
{
}
bool ProjectFileIO::HasConnection() const
{
auto &connectionPtr = ConnectionPtr::Get( mProject );
return connectionPtr.mpConnection != nullptr;
}
DBConnection &ProjectFileIO::GetConnection()
{
auto &curConn = CurrConn();
if (!curConn)
{
if (!OpenConnection())
{
throw SimpleMessageBoxException
{
ExceptionType::Internal,
XO("Failed to open the project's database"),
XO("Warning"),
"Error:_Disk_full_or_not_writable"
};
}
}
return *curConn;
}
wxString ProjectFileIO::GenerateDoc()
{
auto &trackList = TrackList::Get( mProject );
XMLStringWriter doc;
WriteXMLHeader(doc);
WriteXML(doc, false, trackList.empty() ? nullptr : &trackList);
return doc;
}
sqlite3 *ProjectFileIO::DB()
{
return GetConnection().DB();
}
/*!
@pre *CurConn() does not exist
@post *CurConn() exists or return value is false
*/
bool ProjectFileIO::OpenConnection(FilePath fileName /* = {} */)
{
auto &curConn = CurrConn();
wxASSERT(!curConn);
bool isTemp = false;
if (fileName.empty())
{
fileName = GetFileName();
if (fileName.empty())
{
fileName = TempDirectory::UnsavedProjectFileName();
isTemp = true;
}
}
else
{
// If this project resides in the temporary directory, then we'll mark it
// as temporary.
wxFileName temp(TempDirectory::TempDir(), wxT(""));
wxFileName file(fileName);
file.SetFullName(wxT(""));
if (file == temp)
{
isTemp = true;
}
}
// Pass weak_ptr to project into DBConnection constructor
curConn = std::make_unique<DBConnection>(
mProject.shared_from_this(), mpErrors, [this]{ OnCheckpointFailure(); } );
auto rc = curConn->Open(fileName);
if (rc != SQLITE_OK)
{
// Must use SetError() here since we do not have an active DB
SetError(
XO("Failed to open database file:\n\n%s").Format(fileName),
{},
rc
);
curConn.reset();
return false;
}
if (!CheckVersion())
{
CloseConnection();
curConn.reset();
return false;
}
mTemporary = isTemp;
SetFileName(fileName);
return true;
}
bool ProjectFileIO::CloseConnection()
{
auto &curConn = CurrConn();
if (!curConn)
return false;
if (!curConn->Close())
{
return false;
}
curConn.reset();
SetFileName({});
return true;
}
// Put the current database connection aside, keeping it open, so that
// another may be opened with OpenConnection()
void ProjectFileIO::SaveConnection()
{
// Should do nothing in proper usage, but be sure not to leak a connection:
DiscardConnection();
mPrevConn = std::move(CurrConn());
mPrevFileName = mFileName;
mPrevTemporary = mTemporary;
SetFileName({});
}
// Close any set-aside connection
void ProjectFileIO::DiscardConnection()
{
if (mPrevConn)
{
if (!mPrevConn->Close())
{
// Store an error message
SetDBError(
XO("Failed to discard connection")
);
}
// If this is a temporary project, we no longer want to keep the
// project file.
if (mPrevTemporary)
{
// This is just a safety check.
wxFileName temp(TempDirectory::TempDir(), wxT(""));
wxFileName file(mPrevFileName);
file.SetFullName(wxT(""));
if (file == temp)
{
if (!RemoveProject(mPrevFileName))
{
wxLogMessage("Failed to remove temporary project %s", mPrevFileName);
}
}
}
mPrevConn = nullptr;
mPrevFileName.clear();
}
}
// Close any current connection and switch back to using the saved
void ProjectFileIO::RestoreConnection()
{
auto &curConn = CurrConn();
if (curConn)
{
if (!curConn->Close())
{
// Store an error message
SetDBError(
XO("Failed to restore connection")
);
}
}
curConn = std::move(mPrevConn);
SetFileName(mPrevFileName);
mTemporary = mPrevTemporary;
mPrevFileName.clear();
}
void ProjectFileIO::UseConnection(Connection &&conn, const FilePath &filePath)
{
auto &curConn = CurrConn();
wxASSERT(!curConn);
curConn = std::move(conn);
SetFileName(filePath);
}
static int ExecCallback(void *data, int cols, char **vals, char **names)
{
auto &cb = *static_cast<const ProjectFileIO::ExecCB *>(data);
// Be careful not to throw anything across sqlite3's stack frames.
return GuardedCall<int>(
[&]{ return cb(cols, vals, names); },
MakeSimpleGuard( 1 )
);
}
int ProjectFileIO::Exec(const char *query, const ExecCB &callback)
{
char *errmsg = nullptr;
const void *ptr = &callback;
int rc = sqlite3_exec(DB(), query, ExecCallback,
const_cast<void*>(ptr), &errmsg);
if (rc != SQLITE_ABORT && errmsg)
{
ADD_EXCEPTION_CONTEXT("sqlite3.query", query);
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
SetDBError(
XO("Failed to execute a project file command:\n\n%s").Format(query),
Verbatim(errmsg),
rc
);
}
if (errmsg)
{
sqlite3_free(errmsg);
}
return rc;
}
bool ProjectFileIO::Query(const char *sql, const ExecCB &callback)
{
int rc = Exec(sql, callback);
// SQLITE_ABORT is a non-error return only meaning the callback
// stopped the iteration of rows early
if ( !(rc == SQLITE_OK || rc == SQLITE_ABORT) )
{
return false;
}
return true;
}
bool ProjectFileIO::GetValue(const char *sql, wxString &result)
{
// Retrieve the first column in the first row, if any
result.clear();
auto cb = [&result](int cols, char **vals, char **){
if (cols > 0)
result = vals[0];
// Stop after one row
return 1;
};
return Query(sql, cb);
}
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)
{
ADD_EXCEPTION_CONTEXT("sqlite3.query", sql);
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::GetBlob::prepare");
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)
{
ADD_EXCEPTION_CONTEXT("sqlite3.query", sql);
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::GetBlob::step");
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))
{
// Bug 2718 workaround for a better error message:
// If at this point we get SQLITE_CANTOPEN, then the directory is read-only
if (GetLastErrorCode() == SQLITE_CANTOPEN)
{
SetError(
/* i18n-hint: An error message. */
XO("Project is in a read only directory\n(Unable to create the required temporary files)"),
GetLibraryError()
);
}
return false;
}
// If the return count is zero, then there are no tables defined, so this
// must be a new project file.
if (wxStrtol<char **>(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<char **>(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<char **>(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 open 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>", 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()
{
// To do
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::DeleteBlocks(const BlockIDs &blockids, bool complement)
{
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 row's blockid against the set of active blockids
const void *p = &blockids;
rc = sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, const_cast<void*>(p), InSet, nullptr, nullptr);
if (rc != SQLITE_OK)
{
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::DeleteBlocks::create_function");
/* i18n-hint: An error message. Don't translate inset or blockids.*/
SetDBError(XO("Unable to add 'inset' function (can't verify blockids)"));
return false;
}
// Delete all rows in the set, or not in it
// This is the first command that writes to the database, and so we
// do more informative error reporting than usual, if it fails.
auto sql = wxString::Format(
"DELETE FROM sampleblocks WHERE %sinset(blockid);",
complement ? "NOT " : "" );
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
{
ADD_EXCEPTION_CONTEXT("sqlite3.query", sql.ToStdString());
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::GetBlob");
if( rc==SQLITE_READONLY)
/* i18n-hint: An error message. Don't translate blockfiles.*/
SetDBError(XO("Project is read only\n(Unable to work with the blockfiles)"));
else if( rc==SQLITE_LOCKED)
/* i18n-hint: An error message. Don't translate blockfiles.*/
SetDBError(XO("Project is locked\n(Unable to work with the blockfiles)"));
else if( rc==SQLITE_BUSY)
/* i18n-hint: An error message. Don't translate blockfiles.*/
SetDBError(XO("Project is busy\n(Unable to work with the blockfiles)"));
else if( rc==SQLITE_CORRUPT)
/* i18n-hint: An error message. Don't translate blockfiles.*/
SetDBError(XO("Project is corrupt\n(Unable to work with the blockfiles)"));
else if( rc==SQLITE_PERM)
/* i18n-hint: An error message. Don't translate blockfiles.*/
SetDBError(XO("Some permissions issue\n(Unable to work with the blockfiles)"));
else if( rc==SQLITE_IOERR)
/* i18n-hint: An error message. Don't translate blockfiles.*/
SetDBError(XO("A disk I/O error\n(Unable to work with the blockfiles)"));
else if( rc==SQLITE_AUTH)
/* i18n-hint: An error message. Don't translate blockfiles.*/
SetDBError(XO("Not authorized\n(Unable to work with the blockfiles)"));
else
/* i18n-hint: An error message. Don't translate blockfiles.*/
SetDBError(XO("Unable to work with the blockfiles"));
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;
}
bool ProjectFileIO::CopyTo(const FilePath &destpath,
const TranslatableString &msg,
bool isTemporary,
bool prune /* = false */,
const std::vector<const TrackList *> &tracks /* = {} */)
{
auto pConn = CurrConn().get();
if (!pConn)
return false;
// Get access to the active tracklist
auto pProject = &mProject;
SampleBlockIDSet blockids;
// Collect all active blockids
if (prune)
{
for (auto trackList : tracks)
if (trackList)
InspectBlocks( *trackList, {}, &blockids );
}
// Collect ALL blockids
else
{
auto cb = [&blockids](int cols, char **vals, char **){
SampleBlockID blockid;
wxString{ vals[0] }.ToLongLong(&blockid);
blockids.insert(blockid);
return 0;
};
if (!Query("SELECT blockid FROM sampleblocks;", cb))
{
// Error message already captured.
return false;
}
}
// Create the project doc
ProjectSerializer doc;
WriteXMLHeader(doc);
WriteXML(doc, false, tracks.empty() ? nullptr : tracks[0]);
auto db = DB();
Connection destConn = nullptr;
bool success = false;
int rc = SQLITE_OK;
ProgressResult res = ProgressResult::Success;
// Cleanup in case things go awry
auto cleanup = finally([&]
{
if (!success)
{
if (destConn)
{
destConn->Close();
destConn = nullptr;
}
// Rollback transaction in case one was active.
// If this fails (probably due to memory or disk space), the transaction will
// (presumably) stil be active, so further updates to the project file will
// fail as well. Not really much we can do about it except tell the user.
auto result = sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
// Only capture the error if there wasn't a previous error
if (result != SQLITE_OK && (rc == SQLITE_DONE || rc == SQLITE_OK))
{
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT(
"sqlite3.context", "ProjectGileIO::CopyTo.cleanup");
SetDBError(
XO("Failed to rollback transaction during import")
);
}
// And detach the outbound DB in case (if it's attached). Don't check for
// errors since it may not be attached. But, if it is and the DETACH fails,
// subsequent CopyTo() actions will fail until Audacity is relaunched.
sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr);
// RemoveProject not necessary to clean up attached database
wxRemoveFile(destpath);
}
});
// Attach the destination database
wxString sql;
wxString dbName = destpath;
// Bug 2793: Quotes in name need escaping for sqlite3.
dbName.Replace( "'", "''");
sql.Printf("ATTACH DATABASE '%s' AS outbound;", dbName.ToUTF8());
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
XO("Unable to attach destination database")
);
return false;
}
// Ensure attached DB connection gets configured
//
// NOTE: Between the above attach and setting the mode here, a normal DELETE
// mode journal will be used and will briefly appear in the filesystem.
if ( pConn->FastMode("outbound") != SQLITE_OK)
{
SetDBError(
XO("Unable to switch to fast journaling mode")
);
return false;
}
// Install our schema into the new database
if (!InstallSchema(db, "outbound"))
{
// Message already set
return false;
}
{
// Ensure statement gets cleaned up
sqlite3_stmt *stmt = nullptr;
auto cleanup = finally([&]
{
if (stmt)
{
// No need to check return code
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)
{
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT(
"sqlite3.context", "ProjectGileIO::CopyTo.prepare");
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"), 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.
//
// Also note that we will have an open transaction if we fail
// while copying the blocks. This is fine since we're just going
// to delete the database anyway.
sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr);
// Copy sample blocks from the main DB to the outbound DB
for (auto blockid : blockids)
{
// Bind statement parameters
rc = sqlite3_bind_int64(stmt, 1, blockid);
if (rc != SQLITE_OK)
{
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT(
"sqlite3.context", "ProjectGileIO::CopyTo.bind");
SetDBError(
XO("Failed to bind SQL parameter")
);
return false;
}
// Process it
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
{
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT(
"sqlite3.context", "ProjectGileIO::CopyTo.step");
SetDBError(
XO("Failed to update the project file.\nThe following command failed:\n\n%s").Format(sql)
);
return false;
}
// Reset statement to beginning
if (sqlite3_reset(stmt) != SQLITE_OK)
{
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT(
"sqlite3.context", "ProjectGileIO::CopyTo.reset");
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 false;
}
}
// Write the doc.
//
// If we're compacting a temporary project (user initiated from the File
// menu), then write the doc to the "autosave" table since temporary
// projects do not have a "project" doc.
if (!WriteDoc(isTemporary ? "autosave" : "project", doc, "outbound"))
{
return false;
}
// 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)
{
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::CopyTo::detach");
SetDBError(
XO("Destination project could not be detached")
);
return false;
}
// Tell cleanup everything is good to go
success = true;
return true;
}
bool ProjectFileIO::ShouldCompact(const std::vector<const TrackList *> &tracks)
{
SampleBlockIDSet active;
unsigned long long current = 0;
{
auto fn = BlockSpaceUsageAccumulator( current );
for (auto pTracks : tracks)
if (pTracks)
InspectBlocks( *pTracks, fn,
&active // Visit unique blocks only
);
}
// Get the number of blocks and total length from the project file.
unsigned long long total = GetTotalUsage();
unsigned long long blockcount = 0;
auto cb = [&blockcount](int cols, char **vals, char **)
{
// Convert
wxString(vals[0]).ToULongLong(&blockcount);
return 0;
};
if (!Query("SELECT Count(*) FROM sampleblocks;", cb) || blockcount == 0)
{
// Shouldn't compact since we don't have the full picture
return false;
}
// 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"), current, total, total ? current / total : 0);
if (!total || current / total > 80)
{
wxLogDebug(wxT("not compacting"));
return false;
}
wxLogDebug(wxT("compacting"));
return true;
}
Connection &ProjectFileIO::CurrConn()
{
auto &connectionPtr = ConnectionPtr::Get( mProject );
return connectionPtr.mpConnection;
}
const std::vector<wxString> &ProjectFileIO::AuxiliaryFileSuffixes()
{
static const std::vector<wxString> strings {
"-wal",
#ifndef NO_SHM
"-shm",
#endif
};
return strings;
}
FilePath ProjectFileIO::SafetyFileName(const FilePath &src)
{
wxFileNameWrapper fn{ src };
// Extra characters inserted into filename before extension
wxString extra =
#ifdef __WXGTK__
wxT("~")
#else
wxT(".bak")
#endif
;
int nn = 1;
auto numberString = [](int num) -> wxString {
return num == 1 ? wxString{} : wxString::Format(".%d", num);
};
auto suffixes = AuxiliaryFileSuffixes();
suffixes.push_back({});
// Find backup paths not already occupied; check all auxiliary suffixes
const auto name = fn.GetName();
FilePath result;
do {
fn.SetName( name + numberString(nn++) + extra );
result = fn.GetFullPath();
}
while( std::any_of(suffixes.begin(), suffixes.end(), [&](auto &suffix){
return wxFileExists(result + suffix);
}) );
return result;
}
bool ProjectFileIO::RenameOrWarn(const FilePath &src, const FilePath &dst)
{
std::atomic_bool done = {false};
bool success = false;
auto thread = std::thread([&]
{
success = wxRenameFile(src, dst);
done = true;
});
auto &window = GetProjectFrame( mProject );
// Provides a progress dialog with indeterminate mode
wxGenericProgressDialog pd(XO("Copying Project").Translation(),
XO("This may take several seconds").Translation(),
300000, // range
&window, // parent
wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_SMOOTH);
// Wait for the checkpoints to end
while (!done)
{
wxMilliSleep(50);
pd.Pulse();
}
thread.join();
if (!success)
{
ShowError(
&window,
XO("Error Writing to File"),
XO("Audacity failed to write file %s.\n"
"Perhaps disk is full or not writable.\n"
"For tips on freeing up space, click the help button.")
.Format(dst),
"Error:_Disk_full_or_not_writable"
);
return false;
}
return true;
}
bool ProjectFileIO::MoveProject(const FilePath &src, const FilePath &dst)
{
// Assume the src database file is not busy.
if (!RenameOrWarn(src, dst))
return false;
// So far so good, but the separate -wal and -shm files might yet exist,
// as when checkpointing failed for limited space on the drive.
// If so move them too or else lose data.
std::vector< std::pair<FilePath, FilePath> > pairs{ { src, dst } };
bool success = false;
auto cleanup = finally([&]{
if (!success) {
// If any one of the renames failed, back out the previous ones.
// This should be a no-fail recovery! Not clear what to do if any
// of these renames fails.
for (auto &pair : pairs) {
if (!(pair.first.empty() && pair.second.empty()))
wxRenameFile(pair.second, pair.first);
}
}
});
for (const auto &suffix : AuxiliaryFileSuffixes()) {
auto srcName = src + suffix;
if (wxFileExists(srcName)) {
auto dstName = dst + suffix;
if (!RenameOrWarn(srcName, dstName))
return false;
pairs.push_back({ srcName, dstName });
}
}
return (success = true);
}
bool ProjectFileIO::RemoveProject(const FilePath &filename)
{
if (!wxFileExists(filename))
return false;
bool success = wxRemoveFile(filename);
auto &suffixes = AuxiliaryFileSuffixes();
for (const auto &suffix : suffixes) {
auto file = filename + suffix;
if (wxFileExists(file))
success = wxRemoveFile(file) && success;
}
return success;
}
ProjectFileIO::BackupProject::BackupProject(
ProjectFileIO &projectFileIO, const FilePath &path )
{
auto safety = SafetyFileName(path);
if (!projectFileIO.MoveProject(path, safety))
return;
mPath = path;
mSafety = safety;
}
void ProjectFileIO::BackupProject::Discard()
{
if (!mPath.empty()) {
// Succeeded; don't need the safety files
RemoveProject(mSafety);
mSafety.clear();
}
}
ProjectFileIO::BackupProject::~BackupProject()
{
if (!mPath.empty()) {
if (!mSafety.empty()) {
// Failed; restore from safety files
auto suffixes = AuxiliaryFileSuffixes();
suffixes.push_back({});
for (const auto &suffix : suffixes) {
auto path = mPath + suffix;
if (wxFileExists(path))
wxRemoveFile(path);
wxRenameFile(mSafety + suffix, mPath + suffix);
}
}
}
}
void ProjectFileIO::Compact(
const std::vector<const TrackList *> &tracks, bool force)
{
// Haven't compacted yet
mWasCompacted = false;
// Assume we have unused blocks until we find out otherwise. That way cleanup
// at project close time will still occur.
mHadUnused = true;
// If forcing compaction, bypass inspection.
if (!force)
{
// Don't compact if this is a temporary project or if it's determined there are not
// enough unused blocks to make it worthwhile.
if (IsTemporary() || !ShouldCompact(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.
// REVIEW: Could the autosave file be corrupt though at that point, and so
// prevent recovery?
// LLL: I believe Paul is correct since it's deleted with a single SQLite
// transaction. The next time the file opens will just invoke recovery.
(void) AutoSaveDelete();
}
return;
}
}
wxString origName = mFileName;
wxString backName = origName + "_compact_back";
wxString tempName = origName + "_compact_temp";
// Copy the original database to a new database. Only prune sample blocks if
// we have a tracklist.
// REVIEW: Compact can fail on the CopyTo with no error messages. That's OK?
// LLL: We could display an error message or just ignore the failure and allow
// the file to be compacted the next time it's saved.
if (CopyTo(tempName, XO("Compacting project"), IsTemporary(), !tracks.empty(), tracks))
{
// Must close the database to rename it
if (CloseConnection())
{
// Only use the new file if it is actually smaller than the original.
//
// If the original file doesn't have anything to compact (original and new
// are basically identical), the file could grow by a few pages because of
// differences in how SQLite constructs the b-tree.
//
// In this case, just toss the new file and continue to use the original.
//
// Also, do this after closing the connection so that the -wal file
// gets cleaned up.
if (wxFileName::GetSize(tempName) < wxFileName::GetSize(origName))
{
// Rename the original to backup
if (wxRenameFile(origName, backName))
{
// Rename the temporary to original
if (wxRenameFile(tempName, origName))
{
// Open the newly compacted original file
if (OpenConnection(origName))
{
// Remove the old original file
if (!wxRemoveFile(backName))
{
// Just log the error, nothing can be done to correct it
// and WX should have logged another message showing the
// system error code.
wxLogWarning(wxT("Compaction failed to delete backup %s"), backName);
}
// Remember that we compacted
mWasCompacted = true;
return;
}
else
{
wxLogWarning(wxT("Compaction failed to open new project %s"), origName);
}
if (!wxRenameFile(origName, tempName))
{
wxLogWarning(wxT("Compaction failed to rename orignal %s to temp %s"),
origName, tempName);
}
}
else
{
wxLogWarning(wxT("Compaction failed to rename temp %s to orig %s"),
origName, tempName);
}
if (!wxRenameFile(backName, origName))
{
wxLogWarning(wxT("Compaction failed to rename back %s to orig %s"),
backName, origName);
}
}
else
{
wxLogWarning(wxT("Compaction failed to rename orig %s to back %s"),
backName, origName);
}
}
if (!OpenConnection(origName))
{
wxLogWarning(wxT("Compaction failed to reopen %s"), origName);
}
}
// Did not achieve any real compaction
// RemoveProject not needed for what was an attached database
if (!wxRemoveFile(tempName))
{
// Just log the error, nothing can be done to correct it
// and WX should have logged another message showing the
// system error code.
wxLogWarning(wxT("Failed to delete temporary file...ignoring"));
}
}
return;
}
bool ProjectFileIO::WasCompacted()
{
return mWasCompacted;
}
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 &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 "<untitled>" if there
// is none.
if (number >= 0)
{
name =
/* i18n-hint: The %02i is the project number, the %s is the project name.*/
XO("[Project %02i] Audacity \"%s\"")
.Format( number + 1,
name.empty() ? XO("<untitled>") : Verbatim((const char *)name))
.Translation();
}
// If we are not showing numbers, then <untitled> 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 &project = mProject;
if (!mFileName.empty())
{
ActiveProjects::Remove(mFileName);
}
mFileName = fileName;
if (!mFileName.empty())
{
ActiveProjects::Add(mFileName);
}
if (IsTemporary())
{
project.SetProjectName({});
}
else
{
project.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);
int fileVer = ((fver *100)+frel)*100+frev;
int codeVer = ((cver *100)+crel)*100+crev;
if (codeVer<fileVer)
{
/* 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);
ShowError(
&window,
XO("Can't open project file"),
msg,
"FAQ:Errors_opening_an_Audacity_project"
);
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 &project = mProject;
auto fn = ProjectFileIORegistry::Lookup(tag);
if (fn)
{
return fn(project);
}
return nullptr;
}
void ProjectFileIO::OnCheckpointFailure()
{
wxCommandEvent evt{ EVT_CHECKPOINT_FAILURE };
mProject.ProcessEvent(evt);
}
void ProjectFileIO::WriteXMLHeader(XMLWriter &xmlFile) const
{
xmlFile.Write(wxT("<?xml "));
xmlFile.Write(wxT("version=\"1.0\" "));
xmlFile.Write(wxT("standalone=\"no\" "));
xmlFile.Write(wxT("?>\n"));
xmlFile.Write(wxT("<!DOCTYPE "));
xmlFile.Write(wxT("project "));
xmlFile.Write(wxT("PUBLIC "));
xmlFile.Write(wxT("\"-//audacityproject-1.3.0//DTD//EN\" "));
xmlFile.Write(wxT("\"http://audacity.sourceforge.net/xml/audacityproject-1.3.0.dtd\" "));
xmlFile.Write(wxT(">\n"));
}
void ProjectFileIO::WriteXML(XMLWriter &xmlFile,
bool recording /* = false */,
const TrackList *tracks /* = nullptr */)
// may throw
{
auto &proj = mProject;
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([&](const 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)
{
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::AutoSaveDelete");
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,
const char *schema /* = "main" */)
{
auto db = DB();
int rc;
// For now, we always use an ID of 1. This will replace the previously
// written row every time.
char sql[256];
sqlite3_snprintf(sizeof(sql),
sql,
"INSERT INTO %s.%s(id, dict, doc) VALUES(1, ?1, ?2)"
" ON CONFLICT(id) DO UPDATE SET dict = ?1, doc = ?2;",
schema,
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)
{
ADD_EXCEPTION_CONTEXT("sqlite3.query", sql);
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::WriteDoc::prepare");
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 statement parameters
// 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))
{
ADD_EXCEPTION_CONTEXT("sqlite3.query", sql);
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::WriteDoc::bind");
SetDBError(
XO("Unable to bind to blob")
);
return false;
}
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
{
ADD_EXCEPTION_CONTEXT("sqlite3.query", sql);
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::WriteDoc::step");
SetDBError(
XO("Failed to update the project file.\nThe following command failed:\n\n%s").Format(sql)
);
return false;
}
return true;
}
bool ProjectFileIO::LoadProject(const FilePath &fileName, bool ignoreAutosave)
{
bool success = false;
auto cleanup = finally([&]
{
if (!success)
{
RestoreConnection();
}
});
SaveConnection();
// Open the project file
if (!OpenConnection(fileName))
{
return false;
}
wxString project;
wxMemoryBuffer buffer;
bool usedAutosave = true;
// Get the autosave doc, if any
if (!ignoreAutosave &&
!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;
}
}
// Missing both the autosave and project docs. This can happen if the
// system were to crash before the first autosave into a temporary file.
// This should be a recoverable scenario.
if (buffer.GetDataLen() == 0)
{
mRecovered = true;
}
else
{
project = ProjectSerializer::Decode(buffer);
if (project.empty())
{
SetError(XO("Unable to decode project document"));
return false;
}
XMLFileReader xmlFile;
// Load 'er up
success = xmlFile.ParseString(this, project);
if (!success)
{
SetError(
XO("Unable to parse project information."),
xmlFile.GetErrorStr()
);
return false;
}
// Check for orphans blocks...sets mRecovered if any were deleted
auto blockids = WaveTrackFactory::Get( mProject )
.GetSampleBlockFactory()
->GetActiveBlockIDs();
if (blockids.size() > 0)
{
success = DeleteBlocks(blockids, true);
if (!success)
{
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;
success = GetValue("SELECT Count(*) FROM project;", result);
if (!success)
{
return false;
}
mTemporary = !result.IsSameAs(wxT("1"));
SetFileName(fileName);
DiscardConnection();
success = true;
return true;
}
bool ProjectFileIO::UpdateSaved(const TrackList *tracks)
{
ProjectSerializer doc;
WriteXMLHeader(doc);
WriteXML(doc, false, tracks);
if (!WriteDoc("project", doc))
{
return false;
}
// Autosave no longer needed
if (!AutoSaveDelete())
{
return false;
}
return true;
}
// REVIEW: This function is believed to report an error to the user in all cases
// of failure. Callers are believed not to need to do so if they receive 'false'.
// LLL: All failures checks should now be displaying an error.
bool ProjectFileIO::SaveProject(
const FilePath &fileName, const TrackList *lastSaved)
{
// In the case where we're saving a temporary project to a permanent project,
// we'll try to simply rename the project to save a bit of time. We then fall
// through to the normal Save (not SaveAs) processing.
if (IsTemporary() && mFileName != fileName)
{
FilePath savedName = mFileName;
if (CloseConnection())
{
bool reopened = false;
bool moved = false;
if (true == (moved = MoveProject(savedName, fileName)))
{
if (OpenConnection(fileName))
reopened = true;
else {
MoveProject(fileName, savedName);
moved = false; // No longer moved
reopened = OpenConnection(savedName);
}
}
else {
// Rename can fail -- if it's to a different device, requiring
// real copy of contents, which might exhaust space
reopened = OpenConnection(savedName);
}
// Warning issued in MoveProject()
if (reopened && !moved) {
return false;
}
if (!reopened) {
wxTheApp->CallAfter([this]{
ShowError(nullptr,
XO("Warning"),
XO(
"The project's database failed to reopen, "
"possibly because of limited space on the storage device."),
"Error:_Disk_full_or_not_writable"
);
wxCommandEvent evt{ EVT_RECONNECTION_FAILURE };
mProject.ProcessEvent(evt);
});
return false;
}
}
}
// 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.
if (!CopyTo(fileName, XO("Saving project"), false))
{
ShowError(
nullptr,
XO("Error Saving Project"),
FileException::WriteFailureMessage(fileName),
"Error:_Disk_full_or_not_writable"
);
return false;
}
// Open the newly created database
Connection newConn = std::make_unique<DBConnection>(
mProject.shared_from_this(), mpErrors,
[this]{ OnCheckpointFailure(); });
// NOTE: There is a noticeable delay here when dealing with large multi-hour
// projects that we just created. The delay occurs in Open() when it
// calls SafeMode() and is due to the switch from the NONE journal mode
// to the WAL journal mode.
//
// So, we do the Open() in a thread and display a progress dialog. Since
// this is currently the only known instance where this occurs, we do the
// threading here. If more instances are identified, then the threading
// should be moved to DBConnection::Open(), wrapping the SafeMode() call
// there.
{
std::atomic_bool done = {false};
bool success = true;
auto thread = std::thread([&]
{
auto rc = newConn->Open(fileName);
if (rc != SQLITE_OK)
{
// Capture the error string
SetError(Verbatim(sqlite3_errstr(rc)));
success = false;
}
done = true;
});
// Provides a progress dialog with indeterminate mode
wxGenericProgressDialog pd(XO("Syncing").Translation(),
XO("This may take several seconds").Translation(),
300000, // range
nullptr, // parent
wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_SMOOTH);
// Wait for the checkpoints to end
while (!done)
{
wxMilliSleep(50);
pd.Pulse();
}
thread.join();
if (!success)
{
// Additional help via a Help button links to the manual.
ShowError(nullptr,
XO("Error Saving Project"),
XO("The project failed to open, possibly due to limited space\n"
"on the storage device.\n\n%s").Format(GetLastError()),
"Error:_Disk_full_or_not_writable");
newConn = nullptr;
// Clean up the destination project
if (!wxRemoveFile(fileName))
{
wxLogMessage("Failed to remove destination project after open failure: %s", fileName);
}
return false;
}
}
// Autosave no longer needed in original project file.
if (!AutoSaveDelete())
{
// Additional help via a Help button links to the manual.
ShowError(nullptr,
XO("Error Saving Project"),
XO("Unable to remove autosave information, possibly due to limited space\n"
"on the storage device.\n\n%s").Format(GetLastError()),
"Error:_Disk_full_or_not_writable");
newConn = nullptr;
// Clean up the destination project
if (!wxRemoveFile(fileName))
{
wxLogMessage("Failed to remove destination project after AutoSaveDelete failure: %s", fileName);
}
return false;
}
if (lastSaved) {
// Bug2605: Be sure not to save orphan blocks
bool recovered = mRecovered;
SampleBlockIDSet blockids;
InspectBlocks( *lastSaved, {}, &blockids );
// TODO: Not sure what to do if the deletion fails
DeleteBlocks(blockids, true);
// Don't set mRecovered if any were deleted
mRecovered = recovered;
}
// Try to compact the original project file.
auto empty = TrackList::Create(&mProject);
Compact( { lastSaved ? lastSaved : empty.get() }, true );
// Safe to close the original project file now. Not much we can do if this fails,
// but we should still be in good shape since we'll be switching to the newly
// saved database below.
CloseProject();
// And make it the active project file
UseConnection(std::move(newConn), fileName);
}
else
{
if ( !UpdateSaved( nullptr ) ) {
ShowError(
nullptr,
XO("Error Saving Project"),
FileException::WriteFailureMessage(fileName),
"Error:_Disk_full_or_not_writable"
);
return false;
}
}
// 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();
return true;
}
bool ProjectFileIO::SaveCopy(const FilePath& fileName)
{
return CopyTo(fileName, XO("Backing up project"), false, true,
{&TrackList::Get(mProject)});
}
bool ProjectFileIO::OpenProject()
{
return OpenConnection();
}
bool ProjectFileIO::CloseProject()
{
auto &currConn = CurrConn();
if (!currConn)
{
wxLogDebug("Closing project with no database connection");
return true;
}
// Save the filename since CloseConnection() 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 (CloseConnection())
{
// If this is a temporary project, we no longer want to keep the
// project file.
if (IsTemporary())
{
// This is just a safety check.
wxFileName temp(TempDirectory::TempDir(), wxT(""));
wxFileName file(filename);
file.SetFullName(wxT(""));
if (file == temp)
RemoveProject(filename);
}
}
return true;
}
bool ProjectFileIO::ReopenProject()
{
FilePath fileName = mFileName;
if (!CloseConnection())
{
return false;
}
return OpenConnection(fileName);
}
bool ProjectFileIO::IsModified() const
{
return mModified;
}
bool ProjectFileIO::IsTemporary() const
{
return mTemporary;
}
bool ProjectFileIO::IsRecovered() const
{
return mRecovered;
}
wxLongLong ProjectFileIO::GetFreeDiskSpace() const
{
wxLongLong freeSpace;
if (wxGetDiskSpace(wxPathOnly(mFileName), NULL, &freeSpace))
{
if (FileNames::IsOnFATFileSystem(mFileName)) {
// 4 GiB per-file maximum
constexpr auto limit = 1ll << 32;
// Opening a file only to find its length looks wasteful but
// seems to be necessary at least on Windows with FAT filesystems.
// I don't know if that is only a wxWidgets bug.
auto length = wxFile{mFileName}.Length();
// auto length = wxFileName::GetSize(mFileName);
if (length == wxInvalidSize)
length = 0;
auto free = std::max<wxLongLong>(0, limit - length);
freeSpace = std::min(freeSpace, free);
}
return freeSpace;
}
return -1;
}
/// Displays an error dialog with a button that offers help
void ProjectFileIO::ShowError(wxWindow *parent,
const TranslatableString &dlogTitle,
const TranslatableString &message,
const wxString &helpPage)
{
ShowExceptionDialog(parent, dlogTitle, message, helpPage, true,
audacity::ToWString(GetLastLog()));
}
const TranslatableString &ProjectFileIO::GetLastError() const
{
return mpErrors->mLastError;
}
const TranslatableString &ProjectFileIO::GetLibraryError() const
{
return mpErrors->mLibraryError;
}
int ProjectFileIO::GetLastErrorCode() const
{
return mpErrors->mErrorCode;
}
const wxString &ProjectFileIO::GetLastLog() const
{
return mpErrors->mLog;
}
void ProjectFileIO::SetError(
const TranslatableString& msg, const TranslatableString& libraryError, int errorCode)
{
auto &currConn = CurrConn();
if (currConn)
currConn->SetError(msg, libraryError, errorCode);
}
void ProjectFileIO::SetDBError(
const TranslatableString &msg, const TranslatableString &libraryError, int errorCode)
{
auto &currConn = CurrConn();
if (currConn)
currConn->SetDBError(msg, libraryError, errorCode);
}
void ProjectFileIO::SetBypass()
{
auto &currConn = CurrConn();
if (!currConn)
return;
// Determine if we can bypass sample block deletes during shutdown.
//
// IMPORTANT:
// If the project was compacted, then we MUST bypass further
// deletions since the new file doesn't have the blocks that the
// Sequences expect to be there.
currConn->SetBypass( true );
// Only permanent project files need cleaning at shutdown
if (!IsTemporary() && !WasCompacted())
{
// 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())
{
currConn->SetBypass( false );
}
}
return;
}
int64_t ProjectFileIO::GetBlockUsage(SampleBlockID blockid)
{
auto pConn = CurrConn().get();
if (!pConn)
return 0;
return GetDiskUsage(*pConn, blockid);
}
int64_t ProjectFileIO::GetCurrentUsage(
const std::vector<const TrackList*> &trackLists) const
{
unsigned long long current = 0;
const auto fn = BlockSpaceUsageAccumulator(current);
// Must pass address of this set, even if not otherwise used, to avoid
// possible multiple count of shared blocks
SampleBlockIDSet seen;
for (auto pTracks: trackLists)
if (pTracks)
InspectBlocks(*pTracks, fn, &seen);
return current;
}
int64_t ProjectFileIO::GetTotalUsage()
{
auto pConn = CurrConn().get();
if (!pConn)
return 0;
return GetDiskUsage(*pConn, 0);
}
//
// Returns the amount of disk space used by the specified sample blockid or all
// of the sample blocks if the blockid is 0. It does this by using the raw SQLite
// pages available from the "sqlite_dbpage" virtual table to traverse the SQLite
// table b-tree described here: https://www.sqlite.org/fileformat.html
//
int64_t ProjectFileIO::GetDiskUsage(DBConnection &conn, SampleBlockID blockid /* = 0 */)
{
// Information we need to track our travels through the b-tree
typedef struct
{
int64_t pgno;
int currentCell;
int numCells;
unsigned char data[65536];
} page;
std::vector<page> stack;
int64_t total = 0;
int64_t found = 0;
int64_t right = 0;
int rc;
// Get the rootpage for the sampleblocks table.
sqlite3_stmt *stmt =
conn.Prepare(DBConnection::GetRootPage,
"SELECT rootpage FROM sqlite_master WHERE tbl_name = 'sampleblocks';");
if (stmt == nullptr || sqlite3_step(stmt) != SQLITE_ROW)
{
ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(sqlite3_errcode(conn.DB())));
ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::GetDiskUsage");
return 0;
}
// And store it in our first stack frame
stack.push_back({sqlite3_column_int64(stmt, 0)});
// All done with the statement
sqlite3_clear_bindings(stmt);
sqlite3_reset(stmt);
// Prepare/retrieve statement to read raw database page
stmt = conn.Prepare(DBConnection::GetDBPage,
"SELECT data FROM sqlite_dbpage WHERE pgno = ?1;");
if (stmt == nullptr)
{
return 0;
}
// Traverse the b-tree until we've visited all of the leaf pages or until
// we find the one corresponding to the passed in sample blockid. Because we
// use an integer primary key for the sampleblocks table, the traversal will
// be in ascending blockid sequence.
do
{
// Acces the top stack frame
page &pg = stack.back();
// Read the page from the sqlite_dbpage table if it hasn't yet been loaded
if (pg.numCells == 0)
{
// Bind the page number
sqlite3_bind_int64(stmt, 1, pg.pgno);
// And retrieve the page
if (sqlite3_step(stmt) != SQLITE_ROW)
{
// REVIEW: Likely harmless failure - says size is zero on
// this error.
// LLL: Yea, but not much else we can do.
return 0;
}
// Copy the page content to the stack frame
memcpy(&pg.data,
sqlite3_column_blob(stmt, 0),
sqlite3_column_bytes(stmt, 0));
// And retrieve the total number of cells within it
pg.numCells = get2(&pg.data[3]);
// Reset statement for next usage
sqlite3_clear_bindings(stmt);
sqlite3_reset(stmt);
}
//wxLogDebug("%*.*spgno %lld currentCell %d numCells %d", (stack.size() - 1) * 2, (stack.size() - 1) * 2, "", pg.pgno, pg.currentCell, pg.numCells);
// Process an interior table b-tree page
if (pg.data[0] == 0x05)
{
// Process the next cell if we haven't examined all of them yet
if (pg.currentCell < pg.numCells)
{
// Remember the right-most leaf page number.
right = get4(&pg.data[8]);
// Iterate over the cells.
//
// If we're not looking for a specific blockid, then we always push the
// target page onto the stack and leave the loop after a single iteration.
//
// Otherwise, we match the blockid against the highest integer key contained
// within the cell and if the blockid falls within the cell, we stack the
// page and stop the iteration.
//
// In theory, we could do a binary search for a specific blockid here, but
// because our sample blocks are always large, we will get very few cells
// per page...usually 6 or less.
//
// In both cases, the stacked page can be either an internal or leaf page.
bool stacked = false;
while (pg.currentCell < pg.numCells)
{
// Get the offset to this cell using the offset in the cell pointer
// array.
//
// The cell pointer array starts immediately after the page header
// at offset 12 and the retrieved offset is from the beginning of
// the page.
int celloff = get2(&pg.data[12 + (pg.currentCell * 2)]);
// Bump to the next cell for the next iteration.
pg.currentCell++;
// Get the page number this cell describes
int pagenum = get4(&pg.data[celloff]);
// And the highest integer key, which starts at offset 4 within the cell.
int64_t intkey = 0;
get_varint(&pg.data[celloff + 4], &intkey);
//wxLogDebug("%*.*sinternal - right %lld celloff %d pagenum %d intkey %lld", (stack.size() - 1) * 2, (stack.size() - 1) * 2, " ", right, celloff, pagenum, intkey);
// Stack the described page if we're not looking for a specific blockid
// or if this page contains the given blockid.
if (!blockid || blockid <= intkey)
{
stack.push_back({pagenum, 0, 0});
stacked = true;
break;
}
}
// If we pushed a new page onto the stack, we need to jump back up
// to read the page
if (stacked)
{
continue;
}
}
// We've exhausted all the cells with this page, so we stack the right-most
// leaf page. Ensure we only process it once.
if (right)
{
stack.push_back({right, 0, 0});
right = 0;
continue;
}
}
// Process a leaf table b-tree page
else if (pg.data[0] == 0x0d)
{
// Iterate over the cells
//
// If we're not looking for a specific blockid, then just accumulate the
// payload sizes. We will be reading every leaf page in the sampleblocks
// table.
//
// Otherwise we break out when we find the matching blockid. In this case,
// we only ever look at 1 leaf page.
bool stop = false;
for (int i = 0; i < pg.numCells; i++)
{
// Get the offset to this cell using the offset in the cell pointer
// array.
//
// The cell pointer array starts immediately after the page header
// at offset 8 and the retrieved offset is from the beginning of
// the page.
int celloff = get2(&pg.data[8 + (i * 2)]);
// Get the total payload size in bytes of the described row.
int64_t payload = 0;
int digits = get_varint(&pg.data[celloff], &payload);
// Get the integer key for this row.
int64_t intkey = 0;
get_varint(&pg.data[celloff + digits], &intkey);
//wxLogDebug("%*.*sleaf - celloff %4d intkey %lld payload %lld", (stack.size() - 1) * 2, (stack.size() - 1) * 2, " ", celloff, intkey, payload);
// Add this payload size to the total if we're not looking for a specific
// blockid
if (!blockid)
{
total += payload;
}
// Otherwise, return the payload size for a matching row
else if (blockid == intkey)
{
return payload;
}
}
}
// Done with the current branch, so pop back up to the previous one (if any)
stack.pop_back();
} while (!stack.empty());
// Return the total used for all sample blocks
return total;
}
// Retrieves a 2-byte big-endian integer from the page data
unsigned int ProjectFileIO::get2(const unsigned char *ptr)
{
return (ptr[0] << 8) | ptr[1];
}
// Retrieves a 4-byte big-endian integer from the page data
unsigned int ProjectFileIO::get4(const unsigned char *ptr)
{
return ((unsigned int) ptr[0] << 24) |
((unsigned int) ptr[1] << 16) |
((unsigned int) ptr[2] << 8) |
((unsigned int) ptr[3]);
}
// Retrieves a variable length integer from the page data. Returns the
// number of digits used to encode the integer and the stores the
// value at the given location.
int ProjectFileIO::get_varint(const unsigned char *ptr, int64_t *out)
{
int64_t val = 0;
int i;
for (i = 0; i < 8; ++i)
{
val = (val << 7) + (ptr[i] & 0x7f);
if ((ptr[i] & 0x80) == 0)
{
*out = val;
return i + 1;
}
}
val = (val << 8) + (ptr[i] & 0xff);
*out = val;
return 9;
}
InvisibleTemporaryProject::InvisibleTemporaryProject()
: mpProject{ std::make_shared< AudacityProject >() }
{
}
InvisibleTemporaryProject::~InvisibleTemporaryProject()
{
auto &projectFileIO = ProjectFileIO::Get( Project() );
projectFileIO.SetBypass();
auto &tracks = TrackList::Get( Project() );
tracks.Clear();
// Consume some delayed track list related events before destroying the
// temporary project
try { wxTheApp->Yield(); } catch(...) {}
// Destroy the project and yield again to let delayed window deletions happen
projectFileIO.CloseProject();
mpProject.reset();
try { wxTheApp->Yield(); } catch(...) {}
}