2019-05-29 15:45:19 +00:00
|
|
|
/**********************************************************************
|
|
|
|
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
|
|
|
|
ProjectFileIO.cpp
|
|
|
|
|
|
|
|
Paul Licameli split from AudacityProject.cpp
|
|
|
|
|
|
|
|
**********************************************************************/
|
|
|
|
|
|
|
|
#include "ProjectFileIO.h"
|
|
|
|
|
2020-07-13 05:04:54 +00:00
|
|
|
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
#include <sqlite3.h>
|
2020-07-01 05:45:17 +00:00
|
|
|
#include <wx/crt.h>
|
2019-06-09 12:18:53 +00:00
|
|
|
#include <wx/frame.h>
|
2020-07-20 16:10:31 +00:00
|
|
|
#include <wx/progdlg.h>
|
2020-07-15 06:32:48 +00:00
|
|
|
#include <wx/sstream.h>
|
|
|
|
#include <wx/xml/xml.h>
|
2019-05-29 15:45:19 +00:00
|
|
|
|
|
|
|
#include "FileNames.h"
|
|
|
|
#include "Project.h"
|
|
|
|
#include "ProjectFileIORegistry.h"
|
2020-07-10 05:50:52 +00:00
|
|
|
#include "ProjectSerializer.h"
|
2019-05-29 15:45:19 +00:00
|
|
|
#include "ProjectSettings.h"
|
2020-07-13 05:45:14 +00:00
|
|
|
#include "SampleBlock.h"
|
2020-07-13 14:48:37 +00:00
|
|
|
#include "Sequence.h"
|
2019-05-29 15:45:19 +00:00
|
|
|
#include "Tags.h"
|
2020-07-15 14:26:23 +00:00
|
|
|
#include "TimeTrack.h"
|
2019-05-29 15:45:19 +00:00
|
|
|
#include "ViewInfo.h"
|
2020-07-13 05:45:14 +00:00
|
|
|
#include "WaveClip.h"
|
2019-05-29 15:45:19 +00:00
|
|
|
#include "WaveTrack.h"
|
|
|
|
#include "widgets/AudacityMessageBox.h"
|
|
|
|
#include "widgets/NumericTextCtrl.h"
|
2020-07-01 05:45:17 +00:00
|
|
|
#include "widgets/ProgressDialog.h"
|
|
|
|
#include "xml/XMLFileReader.h"
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-04-21 17:07:01 +00:00
|
|
|
wxDEFINE_EVENT(EVT_PROJECT_TITLE_CHANGE, wxCommandEvent);
|
2020-04-21 16:26:37 +00:00
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
static const int ProjectFileID = ('A' << 24 | 'U' << 16 | 'D' << 8 | 'Y');
|
|
|
|
static const int ProjectFileVersion = 1;
|
2020-07-01 13:55:06 +00:00
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
static const char *ProjectFileSchema =
|
2020-07-15 06:32:48 +00:00
|
|
|
// These are persistent and not connection based
|
|
|
|
//
|
|
|
|
// See the CMakeList.txt for the SQLite lib for more
|
|
|
|
// settings.
|
2020-07-16 06:10:54 +00:00
|
|
|
"PRAGMA <schema>.application_id = %d;"
|
|
|
|
"PRAGMA <schema>.user_version = %d;"
|
2020-07-01 05:45:17 +00:00
|
|
|
""
|
2020-07-09 18:14:12 +00:00
|
|
|
// 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.
|
2020-07-16 06:10:54 +00:00
|
|
|
"CREATE TABLE IF NOT EXISTS <schema>.project"
|
2020-07-01 05:45:17 +00:00
|
|
|
"("
|
2020-07-01 18:26:20 +00:00
|
|
|
" id INTEGER PRIMARY KEY,"
|
2020-07-09 18:14:12 +00:00
|
|
|
" dict BLOB,"
|
|
|
|
" doc BLOB"
|
2020-07-01 05:45:17 +00:00
|
|
|
");"
|
|
|
|
""
|
2020-07-01 13:55:06 +00:00
|
|
|
// 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.
|
2020-07-16 06:10:54 +00:00
|
|
|
"CREATE TABLE IF NOT EXISTS <schema>.autosave"
|
2020-07-01 05:45:17 +00:00
|
|
|
"("
|
|
|
|
" id INTEGER PRIMARY KEY,"
|
|
|
|
" dict BLOB,"
|
|
|
|
" doc BLOB"
|
|
|
|
");"
|
|
|
|
""
|
2020-07-01 13:55:06 +00:00
|
|
|
// CREATE SQL tags
|
|
|
|
// tags is not used (yet)
|
2020-07-16 06:10:54 +00:00
|
|
|
"CREATE TABLE IF NOT EXISTS <schema>.tags"
|
2020-07-01 05:45:17 +00:00
|
|
|
"("
|
|
|
|
" name TEXT,"
|
|
|
|
" value BLOB"
|
|
|
|
");"
|
|
|
|
""
|
2020-07-01 13:55:06 +00:00
|
|
|
// CREATE SQL sampleblocks
|
2020-07-03 03:08:39 +00:00
|
|
|
// 'samples' are fixed size blocks of int16, int32 or float32 numbers.
|
2020-07-01 13:55:06 +00:00
|
|
|
// The blocks may be partially empty.
|
|
|
|
// The quantity of valid data in the blocks is
|
|
|
|
// provided in the project XML.
|
|
|
|
//
|
2020-07-03 03:08:39 +00:00
|
|
|
// sampleformat specifies the format of the samples stored.
|
2020-07-01 13:55:06 +00:00
|
|
|
//
|
|
|
|
// blockID is a 64 bit number.
|
|
|
|
//
|
|
|
|
// summin to summary64K are summaries at 3 distance scales.
|
2020-07-16 06:10:54 +00:00
|
|
|
"CREATE TABLE IF NOT EXISTS <schema>.sampleblocks"
|
2020-07-01 05:45:17 +00:00
|
|
|
"("
|
|
|
|
" blockid INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
" sampleformat INTEGER,"
|
|
|
|
" summin REAL,"
|
|
|
|
" summax REAL,"
|
|
|
|
" sumrms REAL,"
|
|
|
|
" summary256 BLOB,"
|
|
|
|
" summary64k BLOB,"
|
|
|
|
" samples BLOB"
|
|
|
|
");";
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Configuration to provide "safe" connections
|
|
|
|
static const char *SafeConfig =
|
2020-07-19 08:28:02 +00:00
|
|
|
"PRAGMA <schema>.locking_mode = SHARED;"
|
2020-07-17 21:17:42 +00:00
|
|
|
"PRAGMA <schema>.synchronous = NORMAL;"
|
|
|
|
"PRAGMA <schema>.journal_mode = WAL;"
|
2020-07-19 08:28:02 +00:00
|
|
|
"PRAGMA <schema>.wal_autocheckpoint = 0;";
|
2020-07-17 21:17:42 +00:00
|
|
|
|
|
|
|
// Configuration to provide "Fast" connections
|
|
|
|
static const char *FastConfig =
|
2020-07-20 16:10:31 +00:00
|
|
|
"PRAGMA <schema>.locking_mode = SHARED;"
|
2020-07-16 06:10:54 +00:00
|
|
|
"PRAGMA <schema>.synchronous = OFF;"
|
2020-07-17 21:17:42 +00:00
|
|
|
"PRAGMA <schema>.journal_mode = OFF;";
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
// 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()
|
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
// Enable URI filenames for all connections
|
|
|
|
mRc = sqlite3_config(SQLITE_CONFIG_URI, 1);
|
|
|
|
|
|
|
|
if (mRc == SQLITE_OK)
|
|
|
|
{
|
|
|
|
mRc = sqlite3_initialize();
|
|
|
|
}
|
2020-07-07 20:47:23 +00:00
|
|
|
|
|
|
|
#if !defined(__WXMSW__)
|
|
|
|
if (mRc == SQLITE_OK)
|
|
|
|
{
|
|
|
|
// Use the "unix-excl" VFS to make access to the DB exclusive. This gets
|
2020-07-16 06:10:54 +00:00
|
|
|
// rid of the "<database name>-shm" shared memory file.
|
2020-07-07 20:47:23 +00:00
|
|
|
//
|
|
|
|
// 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
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
~SQLiteIniter()
|
|
|
|
{
|
2020-07-07 20:41:33 +00:00
|
|
|
// This function must be called single-threaded only
|
|
|
|
// It returns a value, but there's nothing we can do with it
|
|
|
|
(void) sqlite3_shutdown();
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2020-07-07 20:41:33 +00:00
|
|
|
int mRc;
|
2020-07-01 05:45:17 +00:00
|
|
|
};
|
2020-07-07 20:41:33 +00:00
|
|
|
|
|
|
|
bool ProjectFileIO::InitializeSQL()
|
|
|
|
{
|
|
|
|
static SQLiteIniter sqliteIniter;
|
|
|
|
return sqliteIniter.mRc == SQLITE_OK;
|
|
|
|
}
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2019-06-05 11:23:08 +00:00
|
|
|
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(
|
2020-04-14 14:48:08 +00:00
|
|
|
_("[Project %02i] "), project.GetProjectNumber() + 1 );
|
2019-06-05 11:23:08 +00:00
|
|
|
RefreshAllTitles( true );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
UnnamedCount = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
TitleRestorer::~TitleRestorer() {
|
|
|
|
if( UnnamedCount > 1 )
|
|
|
|
RefreshAllTitles( false );
|
|
|
|
}
|
|
|
|
|
2019-05-29 15:45:19 +00:00
|
|
|
static const AudacityProject::AttachedObjects::RegisteredFactory sFileIOKey{
|
2019-06-07 17:24:38 +00:00
|
|
|
[]( AudacityProject &parent ){
|
|
|
|
auto result = std::make_shared< ProjectFileIO >( parent );
|
|
|
|
return result;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
ProjectFileIO &ProjectFileIO::Get( AudacityProject &project )
|
|
|
|
{
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
auto &result = project.AttachedObjects::Get< ProjectFileIO >( sFileIOKey );
|
|
|
|
return result;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const ProjectFileIO &ProjectFileIO::Get( const AudacityProject &project )
|
|
|
|
{
|
|
|
|
return Get( const_cast< AudacityProject & >( project ) );
|
|
|
|
}
|
|
|
|
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
ProjectFileIO::ProjectFileIO(AudacityProject &)
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
2020-07-07 20:41:33 +00:00
|
|
|
mPrevDB = nullptr;
|
2020-07-01 05:45:17 +00:00
|
|
|
mDB = nullptr;
|
|
|
|
|
|
|
|
mRecovered = false;
|
|
|
|
mModified = false;
|
|
|
|
mTemporary = true;
|
2020-07-03 03:08:39 +00:00
|
|
|
mBypass = false;
|
|
|
|
|
2019-05-29 15:45:19 +00:00
|
|
|
UpdatePrefs();
|
|
|
|
}
|
|
|
|
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
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();
|
2020-07-19 08:28:02 +00:00
|
|
|
|
|
|
|
// Kick off the checkpoint thread
|
|
|
|
mCheckpointThread = std::thread([this]{ CheckpointThread(); });
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
ProjectFileIO::~ProjectFileIO()
|
|
|
|
{
|
2020-07-20 16:10:31 +00:00
|
|
|
wxASSERT_MSG(mDB == nullptr, wxT("Project file was not closed at shutdown"));
|
2020-07-19 08:28:02 +00:00
|
|
|
|
|
|
|
// Tell the checkpoint thread to shutdown
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> guard(mCheckpointMutex);
|
|
|
|
mCheckpointStop = true;
|
|
|
|
mCheckpointCondition.notify_one();
|
|
|
|
}
|
|
|
|
|
|
|
|
// And wait for it to do so
|
|
|
|
mCheckpointThread.join();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ProjectFileIO::CheckpointThread()
|
|
|
|
{
|
2020-07-20 16:10:31 +00:00
|
|
|
mCheckpointStop = false;
|
|
|
|
|
2020-07-19 08:28:02 +00:00
|
|
|
while (true)
|
|
|
|
{
|
|
|
|
{
|
2020-07-20 16:10:31 +00:00
|
|
|
// Wait for work or the stop signal
|
2020-07-19 08:28:02 +00:00
|
|
|
std::unique_lock<std::mutex> lock(mCheckpointMutex);
|
|
|
|
mCheckpointCondition.wait(lock,
|
|
|
|
[&]
|
|
|
|
{
|
2020-07-20 16:10:31 +00:00
|
|
|
return mCheckpointWaitingPages || mCheckpointStop;
|
2020-07-19 08:28:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Requested to stop, so bail
|
|
|
|
if (mCheckpointStop)
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// Capture the number of pages that need checkpointing and reset
|
|
|
|
mCheckpointCurrentPages = mCheckpointWaitingPages;
|
|
|
|
mCheckpointWaitingPages = 0;
|
2020-07-19 08:28:02 +00:00
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// Lock out others while the checkpoint is running
|
2020-07-19 08:28:02 +00:00
|
|
|
mCheckpointActive.lock();
|
|
|
|
}
|
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// Open another connection to the DB to prevent blocking the main thread.
|
|
|
|
sqlite3 *db = nullptr;
|
|
|
|
if (sqlite3_open(mFileName, &db) == SQLITE_OK)
|
2020-07-19 08:28:02 +00:00
|
|
|
{
|
2020-07-20 16:10:31 +00:00
|
|
|
// Configure it to be safe
|
|
|
|
Config(db, SafeConfig);
|
2020-07-19 08:28:02 +00:00
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// 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);
|
2020-07-19 08:28:02 +00:00
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// All done.
|
2020-07-19 08:28:02 +00:00
|
|
|
sqlite3_close(db);
|
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// Reset
|
|
|
|
mCheckpointCurrentPages = 0;
|
|
|
|
|
2020-07-19 08:28:02 +00:00
|
|
|
// 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<ProjectFileIO *>(data);
|
|
|
|
|
|
|
|
// Qeuue the database pointer for our checkpoint thread to process
|
|
|
|
std::lock_guard<std::mutex> guard(that->mCheckpointMutex);
|
2020-07-20 16:10:31 +00:00
|
|
|
that->mCheckpointWaitingPages = pages;
|
2020-07-19 08:28:02 +00:00
|
|
|
that->mCheckpointCondition.notify_one();
|
|
|
|
|
|
|
|
return SQLITE_OK;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sqlite3 *ProjectFileIO::DB()
|
|
|
|
{
|
2020-07-06 20:42:18 +00:00
|
|
|
if (!mDB)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
if (!OpenDB())
|
|
|
|
{
|
|
|
|
throw SimpleMessageBoxException
|
|
|
|
{
|
|
|
|
XO("Failed to open the project's database")
|
|
|
|
};
|
|
|
|
}
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
2020-07-06 20:42:18 +00:00
|
|
|
return mDB;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
2020-07-07 20:41:33 +00:00
|
|
|
// 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;
|
2020-07-09 23:15:35 +00:00
|
|
|
mPrevFileName = mFileName;
|
|
|
|
|
2020-07-07 20:41:33 +00:00
|
|
|
mDB = nullptr;
|
2020-07-09 23:15:35 +00:00
|
|
|
SetFileName({});
|
2020-07-07 20:41:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
2020-07-09 23:15:35 +00:00
|
|
|
mPrevFileName.clear();
|
2020-07-07 20:41:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
2020-07-09 23:15:35 +00:00
|
|
|
SetFileName(mPrevFileName);
|
|
|
|
|
|
|
|
mPrevDB = nullptr;
|
|
|
|
mPrevFileName.clear();
|
2020-07-07 20:41:33 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 23:15:35 +00:00
|
|
|
void ProjectFileIO::UseConnection( sqlite3 *db, const FilePath &filePath )
|
2020-07-07 20:41:33 +00:00
|
|
|
{
|
|
|
|
wxASSERT(mDB == nullptr);
|
|
|
|
mDB = db;
|
2020-07-09 23:15:35 +00:00
|
|
|
SetFileName( filePath );
|
2020-07-07 20:41:33 +00:00
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
void ProjectFileIO::Config(sqlite3 *db, const char *config, const wxString &schema)
|
2020-07-16 06:10:54 +00:00
|
|
|
{
|
|
|
|
int rc;
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
wxString sql = config;
|
2020-07-16 06:10:54 +00:00
|
|
|
|
|
|
|
if (schema.empty())
|
|
|
|
{
|
|
|
|
sql.Replace(wxT("<schema>."), wxT(""));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
sql.Replace(wxT("<schema>"), 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;
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
sqlite3 *ProjectFileIO::OpenDB(FilePath fileName)
|
|
|
|
{
|
|
|
|
wxASSERT(mDB == nullptr);
|
2020-07-01 18:26:20 +00:00
|
|
|
bool temp = false;
|
2020-07-01 05:45:17 +00:00
|
|
|
|
|
|
|
if (fileName.empty())
|
|
|
|
{
|
|
|
|
fileName = GetFileName();
|
|
|
|
if (fileName.empty())
|
|
|
|
{
|
|
|
|
fileName = FileNames::UnsavedProjectFileName();
|
2020-07-01 18:26:20 +00:00
|
|
|
temp = true;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
temp = false;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int rc = sqlite3_open(fileName, &mDB);
|
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetDBError(XO("Failed to open project file"));
|
2020-07-06 20:42:18 +00:00
|
|
|
// sqlite3 docs say you should close anyway to avoid leaks
|
|
|
|
sqlite3_close( mDB );
|
|
|
|
mDB = nullptr;
|
2020-07-01 05:45:17 +00:00
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2020-07-16 06:10:54 +00:00
|
|
|
// Ensure attached DB connection gets configured
|
2020-07-17 21:17:42 +00:00
|
|
|
Config(mDB, SafeConfig);
|
2020-07-16 06:10:54 +00:00
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
if (!CheckVersion())
|
|
|
|
{
|
|
|
|
CloseDB();
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2020-07-01 18:26:20 +00:00
|
|
|
mTemporary = temp;
|
2020-07-17 21:17:42 +00:00
|
|
|
|
2020-07-01 18:26:20 +00:00
|
|
|
SetFileName(fileName);
|
|
|
|
|
2020-07-19 08:28:02 +00:00
|
|
|
// Install our checkpoint hook
|
|
|
|
sqlite3_wal_hook(mDB, CheckpointHook, this);
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
return mDB;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ProjectFileIO::CloseDB()
|
|
|
|
{
|
|
|
|
int rc;
|
|
|
|
|
|
|
|
if (mDB)
|
|
|
|
{
|
2020-07-20 16:10:31 +00:00
|
|
|
// Uninstall our checkpoint hook so that no additional checkpoints
|
|
|
|
// are sent our way. (Though this shouldn't really happen.)
|
2020-07-19 08:28:02 +00:00
|
|
|
sqlite3_wal_hook(mDB, nullptr, nullptr);
|
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// Display a progress dialog if there's active or pending checkpoints
|
|
|
|
if (mCheckpointWaitingPages || mCheckpointCurrentPages)
|
2020-07-19 08:28:02 +00:00
|
|
|
{
|
2020-07-20 16:10:31 +00:00
|
|
|
TranslatableString title = XO("Checkpointing project");
|
2020-07-19 08:28:02 +00:00
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// Get access to the active tracklist
|
|
|
|
auto pProject = mpProject.lock();
|
|
|
|
if (pProject)
|
|
|
|
{
|
|
|
|
title = XO("Checkpointing %s").Format(pProject->GetProjectName());
|
|
|
|
}
|
2020-07-19 08:28:02 +00:00
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
2020-07-19 08:28:02 +00:00
|
|
|
|
|
|
|
// Close the DB
|
2020-07-01 05:45:17 +00:00
|
|
|
rc = sqlite3_close(mDB);
|
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetDBError(XO("Failed to close the project file"));
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
mDB = nullptr;
|
2020-07-01 18:26:20 +00:00
|
|
|
SetFileName({});
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ProjectFileIO::DeleteDB()
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
wxASSERT(mDB == nullptr);
|
|
|
|
|
|
|
|
if (mTemporary && !mFileName.empty())
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
|
|
|
wxFileName temp(FileNames::TempDir());
|
2020-07-01 18:26:20 +00:00
|
|
|
if (temp == wxPathOnly(mFileName))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
if (!wxRemoveFile(mFileName))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetError(XO("Failed to close the project file"));
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-03 16:34:53 +00:00
|
|
|
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(),
|
2020-07-09 18:14:12 +00:00
|
|
|
wxT("RELEASE ") + name + wxT(";"),
|
2020-07-03 16:34:53 +00:00
|
|
|
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(),
|
2020-07-09 18:14:12 +00:00
|
|
|
wxT("ROLLBACK TO ") + name + wxT(";"),
|
2020-07-03 16:34:53 +00:00
|
|
|
nullptr,
|
|
|
|
nullptr,
|
|
|
|
&errmsg);
|
|
|
|
|
|
|
|
if (errmsg)
|
|
|
|
{
|
|
|
|
SetDBError(
|
|
|
|
XO("Failed to release savepoint:\n\n%s").Format(name)
|
|
|
|
);
|
|
|
|
sqlite3_free(errmsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
return rc == SQLITE_OK;
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
/* static */
|
|
|
|
int ProjectFileIO::ExecCallback(void *data, int cols, char **vals, char **names)
|
|
|
|
{
|
|
|
|
ExecParm *parms = static_cast<ExecParm *>(data);
|
|
|
|
return parms->func(parms->result, cols, vals, names);
|
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
int ProjectFileIO::Exec(const char *query, ExecCB callback, ExecResult &result)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
|
|
|
char *errmsg = nullptr;
|
|
|
|
ExecParm ep = {callback, result};
|
|
|
|
|
|
|
|
int rc = sqlite3_exec(DB(), query, ExecCallback, &ep, &errmsg);
|
|
|
|
|
|
|
|
if (errmsg)
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetDBError(
|
|
|
|
XO("Failed to execute a project file command:\n\n%s").Format(query)
|
|
|
|
);
|
|
|
|
mLibraryError = Verbatim(errmsg);
|
2020-07-01 05:45:17 +00:00
|
|
|
sqlite3_free(errmsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
return rc;
|
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
bool ProjectFileIO::Query(const char *sql, ExecResult &result)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-09 18:14:12 +00:00
|
|
|
result.clear();
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
auto getresult = [](ExecResult &result, int cols, char **vals, char **names)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
std::vector<wxString> row;
|
|
|
|
|
|
|
|
for (int i = 0; i < cols; ++i)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
row.push_back(vals[i]);
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
result.push_back(row);
|
|
|
|
|
|
|
|
return SQLITE_OK;
|
2020-07-01 05:45:17 +00:00
|
|
|
};
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
int rc = Exec(sql, getresult, result);
|
2020-07-01 05:45:17 +00:00
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
2020-07-08 05:18:05 +00:00
|
|
|
return false;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
2020-07-08 05:18:05 +00:00
|
|
|
return true;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
bool ProjectFileIO::GetValue(const char *sql, wxString &result)
|
|
|
|
{
|
|
|
|
result.clear();
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
ExecResult holder;
|
|
|
|
if (!Query(sql, holder))
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Return the first column in the first row, if any
|
|
|
|
if (holder.size() && holder[0].size())
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
result.assign(holder[0][0]);
|
2020-07-15 06:32:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
bool ProjectFileIO::GetBlob(const char *sql, wxMemoryBuffer &buffer)
|
|
|
|
{
|
|
|
|
auto db = DB();
|
|
|
|
int rc;
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
buffer.Clear();
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
sqlite3_stmt *stmt = nullptr;
|
|
|
|
auto cleanup = finally([&]
|
|
|
|
{
|
|
|
|
if (stmt)
|
|
|
|
{
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
|
2020-07-01 05:45:17 +00:00
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetDBError(
|
|
|
|
XO("Unable to prepare project file command:\n\n%s").Format(sql)
|
|
|
|
);
|
2020-07-01 05:45:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
rc = sqlite3_step(stmt);
|
2020-07-08 05:18:05 +00:00
|
|
|
|
|
|
|
// A row wasn't found...not an error
|
|
|
|
if (rc == SQLITE_DONE)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
if (rc != SQLITE_ROW)
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetDBError(
|
|
|
|
XO("Failed to retrieve data from the project file.\nThe following command failed:\n\n%s").Format(sql)
|
|
|
|
);
|
2020-07-01 11:59:38 +00:00
|
|
|
// AUD TODO handle error
|
2020-07-01 05:45:17 +00:00
|
|
|
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
|
2020-07-08 05:18:05 +00:00
|
|
|
wxString result;
|
|
|
|
if (!GetValue("SELECT Count(*) FROM sqlite_master WHERE type='table';", result))
|
2020-07-01 18:26:20 +00:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the return count is zero, then there are no tables defined, so this
|
|
|
|
// must be a new project file.
|
2020-07-08 05:18:05 +00:00
|
|
|
if (wxStrtol<char **>(result, nullptr, 10) == 0)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-13 05:45:14 +00:00
|
|
|
return InstallSchema(db);
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check for our application ID
|
2020-07-08 05:18:05 +00:00
|
|
|
if (!GetValue("PRAGMA application_ID;", result))
|
2020-07-01 18:26:20 +00:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// It's a database that SQLite recognizes, but it's not one of ours
|
2020-07-08 05:18:05 +00:00
|
|
|
if (wxStrtoul<char **>(result, nullptr, 10) != ProjectFileID)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetError(XO("This is not an Audacity project file"));
|
2020-07-01 05:45:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the project file version
|
2020-07-08 05:18:05 +00:00
|
|
|
if (!GetValue("PRAGMA user_version;", result))
|
2020-07-01 18:26:20 +00:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2020-07-08 05:18:05 +00:00
|
|
|
long version = wxStrtol<char **>(result, nullptr, 10);
|
|
|
|
|
2020-07-01 18:26:20 +00:00
|
|
|
// Project file version is higher than ours. We will refuse to
|
|
|
|
// process it since we can't trust anything about it.
|
2020-07-01 05:45:17 +00:00
|
|
|
if (version > ProjectFileVersion)
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetError(
|
|
|
|
XO("This project was created with a newer version of Audacity:\n\nYou will need to upgrade to process it")
|
|
|
|
);
|
2020-07-01 05:45:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Project file is older than ours, ask the user if it's okay to
|
|
|
|
// upgrade.
|
|
|
|
if (version < ProjectFileVersion)
|
|
|
|
{
|
|
|
|
return UpgradeSchema();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-16 06:10:54 +00:00
|
|
|
bool ProjectFileIO::InstallSchema(sqlite3 *db, const char *schema /* = "main" */)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
|
|
|
int rc;
|
|
|
|
|
2020-07-13 05:45:14 +00:00
|
|
|
wxString sql;
|
|
|
|
sql.Printf(ProjectFileSchema, ProjectFileID, ProjectFileVersion);
|
2020-07-16 06:10:54 +00:00
|
|
|
sql.Replace("<schema>", schema);
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
|
2020-07-01 05:45:17 +00:00
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetDBError(
|
|
|
|
XO("Unable to initialize the project file")
|
|
|
|
);
|
2020-07-01 05:45:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ProjectFileIO::UpgradeSchema()
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
// 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.
|
2020-07-13 05:45:14 +00:00
|
|
|
void ProjectFileIO::InSet(sqlite3_context *context, int argc, sqlite3_value **argv)
|
2020-07-08 05:18:05 +00:00
|
|
|
{
|
2020-07-09 18:14:12 +00:00
|
|
|
BlockIDs *blockids = (BlockIDs *) sqlite3_user_data(context);
|
|
|
|
SampleBlockID blockid = sqlite3_value_int64(argv[0]);
|
|
|
|
|
2020-07-13 05:45:14 +00:00
|
|
|
sqlite3_result_int(context, blockids->find(blockid) != blockids->end());
|
2020-07-08 05:18:05 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
bool ProjectFileIO::CheckForOrphans(BlockIDs &blockids)
|
2020-07-08 05:18:05 +00:00
|
|
|
{
|
|
|
|
auto db = DB();
|
2020-07-09 18:14:12 +00:00
|
|
|
int rc;
|
2020-07-08 05:18:05 +00:00
|
|
|
|
2020-07-13 05:45:14 +00:00
|
|
|
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);
|
2020-07-09 18:14:12 +00:00
|
|
|
if (rc != SQLITE_OK)
|
2020-07-08 05:18:05 +00:00
|
|
|
{
|
2020-07-13 05:45:14 +00:00
|
|
|
wxLogDebug(wxT("Unable to add 'inset' function"));
|
2020-07-09 18:14:12 +00:00
|
|
|
return false;
|
2020-07-08 05:18:05 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
// Delete all rows that are orphaned
|
2020-07-13 05:45:14 +00:00
|
|
|
rc = sqlite3_exec(db, "DELETE FROM sampleblocks WHERE NOT inset(blockid);", nullptr, nullptr, nullptr);
|
2020-07-08 05:18:05 +00:00
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
2020-07-13 05:45:14 +00:00
|
|
|
wxLogWarning(XO("Cleanup of orphan blocks failed").Translation());
|
2020-07-08 05:18:05 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
// Mark the project recovered if we deleted any rows
|
2020-07-08 05:18:05 +00:00
|
|
|
int changes = sqlite3_changes(db);
|
|
|
|
if (changes > 0)
|
|
|
|
{
|
|
|
|
wxLogInfo(XO("Total orphan blocks deleted %d").Translation(), changes);
|
|
|
|
mRecovered = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
|
|
|
|
const TranslatableString &msg,
|
2020-07-17 21:17:42 +00:00
|
|
|
bool prune /* = false */,
|
|
|
|
const std::shared_ptr<TrackList> &tracks/* = nullptr */)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-13 05:45:14 +00:00
|
|
|
// Get access to the active tracklist
|
|
|
|
auto pProject = mpProject.lock();
|
|
|
|
if (!pProject)
|
|
|
|
{
|
|
|
|
return nullptr;
|
|
|
|
}
|
2020-07-17 21:17:42 +00:00
|
|
|
auto &tracklist = tracks ? *tracks : TrackList::Get(*pProject);
|
2020-07-13 05:45:14 +00:00
|
|
|
|
|
|
|
BlockIDs blockids;
|
2020-07-15 06:32:48 +00:00
|
|
|
|
|
|
|
// Collect all active blockids
|
|
|
|
if (prune)
|
2020-07-13 05:45:14 +00:00
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
for (auto wt : tracklist.Any<const WaveTrack>())
|
2020-07-13 05:45:14 +00:00
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
// Scan all clips within current track
|
|
|
|
for (const auto &clip : wt->GetAllClips())
|
2020-07-13 05:45:14 +00:00
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
// Scan all blockfiles within current clip
|
|
|
|
auto blocks = clip->GetSequenceBlockArray();
|
|
|
|
for (const auto &block : *blocks)
|
|
|
|
{
|
|
|
|
blockids.insert(block.sb->GetBlockID());
|
|
|
|
}
|
2020-07-13 05:45:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-07-15 06:32:48 +00:00
|
|
|
// Collect ALL blockids
|
|
|
|
else
|
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
ExecResult holder;
|
|
|
|
if (!Query("SELECT blockid FROM sampleblocks;", holder))
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto block : holder)
|
|
|
|
{
|
|
|
|
SampleBlockID blockid;
|
2020-07-17 21:17:42 +00:00
|
|
|
block[0].ToLongLong(&blockid);
|
2020-07-15 06:32:48 +00:00
|
|
|
|
|
|
|
blockids.insert(blockid);
|
|
|
|
}
|
|
|
|
}
|
2020-07-13 05:45:14 +00:00
|
|
|
|
2020-07-20 17:51:01 +00:00
|
|
|
// Create the project doc
|
|
|
|
ProjectSerializer doc;
|
|
|
|
WriteXMLHeader(doc);
|
|
|
|
WriteXML(doc, false, tracks);
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
auto db = DB();
|
2020-07-13 05:45:14 +00:00
|
|
|
sqlite3 *destdb = nullptr;
|
|
|
|
bool success = false;
|
2020-07-01 05:45:17 +00:00
|
|
|
int rc;
|
|
|
|
ProgressResult res = ProgressResult::Success;
|
|
|
|
|
2020-07-13 05:45:14 +00:00
|
|
|
// Cleanup in case things go awry
|
2020-07-10 05:50:52 +00:00
|
|
|
auto cleanup = finally([&]
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-13 05:45:14 +00:00
|
|
|
if (!success)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-13 05:45:14 +00:00
|
|
|
sqlite3_close(destdb);
|
|
|
|
|
2020-07-16 06:10:54 +00:00
|
|
|
sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr);
|
|
|
|
|
2020-07-13 05:45:14 +00:00
|
|
|
wxRemoveFile(destpath);
|
2020-07-10 05:50:52 +00:00
|
|
|
}
|
|
|
|
});
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2020-07-13 05:45:14 +00:00
|
|
|
// Attach the destination database
|
|
|
|
wxString sql;
|
2020-07-16 06:10:54 +00:00
|
|
|
sql.Printf("ATTACH DATABASE '%s' AS outbound;", destpath);
|
2020-07-13 05:45:14 +00:00
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
|
2020-07-10 05:50:52 +00:00
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
2020-07-13 05:45:14 +00:00
|
|
|
SetDBError(
|
|
|
|
XO("Unable to attach destination database")
|
|
|
|
);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2020-07-16 06:10:54 +00:00
|
|
|
// Ensure attached DB connection gets configured
|
2020-07-17 21:17:42 +00:00
|
|
|
Config(db, FastConfig, "outbound");
|
2020-07-16 06:10:54 +00:00
|
|
|
|
2020-07-13 05:45:14 +00:00
|
|
|
// Install our schema into the new database
|
2020-07-16 06:10:54 +00:00
|
|
|
if (!InstallSchema(db, "outbound"))
|
2020-07-13 05:45:14 +00:00
|
|
|
{
|
|
|
|
// Message already set
|
|
|
|
return nullptr;
|
2020-07-10 05:50:52 +00:00
|
|
|
}
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Copy over tags (not really used yet)
|
2020-07-15 06:32:48 +00:00
|
|
|
rc = sqlite3_exec(db,
|
2020-07-16 06:10:54 +00:00
|
|
|
"INSERT INTO outbound.tags SELECT * FROM main.tags;",
|
2020-07-15 06:32:48 +00:00
|
|
|
nullptr,
|
|
|
|
nullptr,
|
|
|
|
nullptr);
|
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
|
|
|
SetDBError(
|
|
|
|
XO("Failed to copy tags")
|
|
|
|
);
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
2020-07-13 05:04:54 +00:00
|
|
|
|
2020-07-10 05:50:52 +00:00
|
|
|
{
|
2020-07-16 06:10:54 +00:00
|
|
|
// 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 = ?;",
|
2020-07-17 21:17:42 +00:00
|
|
|
-1,
|
|
|
|
&stmt,
|
|
|
|
nullptr);
|
2020-07-16 06:10:54 +00:00
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
|
|
|
SetDBError(
|
|
|
|
XO("Unable to prepare project file command:\n\n%s").Format(sql)
|
|
|
|
);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2020-07-10 05:50:52 +00:00
|
|
|
/* i18n-hint: This title appears on a dialog that indicates the progress
|
|
|
|
in doing something.*/
|
2020-07-15 06:32:48 +00:00
|
|
|
ProgressDialog progress(XO("Progress"), msg, pdlgHideStopButton);
|
|
|
|
ProgressResult result = ProgressResult::Success;
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2020-07-13 14:48:37 +00:00
|
|
|
wxLongLong_t count = 0;
|
|
|
|
wxLongLong_t total = blockids.size();
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// 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
|
2020-07-15 06:32:48 +00:00
|
|
|
for (auto blockid : blockids)
|
2020-07-13 05:04:54 +00:00
|
|
|
{
|
2020-07-16 06:10:54 +00:00
|
|
|
// BIND blockid parameter
|
|
|
|
if (sqlite3_bind_int64(stmt, 1, blockid) != SQLITE_OK)
|
2020-07-13 05:04:54 +00:00
|
|
|
{
|
2020-07-16 06:10:54 +00:00
|
|
|
THROW_INCONSISTENCY_EXCEPTION;
|
2020-07-13 05:04:54 +00:00
|
|
|
}
|
2020-07-16 06:10:54 +00:00
|
|
|
|
|
|
|
// Process it
|
|
|
|
rc = sqlite3_step(stmt);
|
|
|
|
if (rc != SQLITE_DONE)
|
2020-07-13 05:04:54 +00:00
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
SetDBError(
|
2020-07-16 06:10:54 +00:00
|
|
|
XO("Failed to update the project file.\nThe following command failed:\n\n%s").Format(sql)
|
2020-07-15 06:32:48 +00:00
|
|
|
);
|
|
|
|
return nullptr;
|
2020-07-13 05:04:54 +00:00
|
|
|
}
|
2020-07-16 06:10:54 +00:00
|
|
|
|
|
|
|
// BIND blockid parameter
|
|
|
|
if (sqlite3_reset(stmt) != SQLITE_OK)
|
|
|
|
{
|
|
|
|
THROW_INCONSISTENCY_EXCEPTION;
|
|
|
|
}
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
result = progress.Update(++count, total);
|
|
|
|
if (result != ProgressResult::Success)
|
2020-07-13 05:04:54 +00:00
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
// Note that we're not setting success, so the finally
|
|
|
|
// block above will take care of cleaning up
|
|
|
|
return nullptr;
|
2020-07-13 05:04:54 +00:00
|
|
|
}
|
|
|
|
}
|
2020-07-17 21:17:42 +00:00
|
|
|
|
|
|
|
// See BEGIN above...
|
|
|
|
sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2020-07-10 05:50:52 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Detach the destination database
|
|
|
|
rc = sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr);
|
|
|
|
if (rc != SQLITE_OK)
|
2020-07-10 05:50:52 +00:00
|
|
|
{
|
|
|
|
SetDBError(
|
2020-07-17 21:17:42 +00:00
|
|
|
XO("Destination project could not be detached")
|
2020-07-10 05:50:52 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2020-07-13 05:45:14 +00:00
|
|
|
// Open the newly created database
|
2020-07-10 05:50:52 +00:00
|
|
|
rc = sqlite3_open(destpath, &destdb);
|
|
|
|
if (rc != SQLITE_OK)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetDBError(
|
2020-07-10 05:50:52 +00:00
|
|
|
XO("Failed to open copy of project file")
|
2020-07-01 18:26:20 +00:00
|
|
|
);
|
2020-07-10 05:50:52 +00:00
|
|
|
|
2020-07-07 20:41:33 +00:00
|
|
|
return nullptr;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Ensure attached DB connection gets configured
|
2020-07-20 16:10:31 +00:00
|
|
|
Config(destdb, SafeConfig);
|
2020-07-16 06:10:54 +00:00
|
|
|
|
2020-07-20 17:51:01 +00:00
|
|
|
// Write the project doc
|
|
|
|
if (!WriteDoc("project", doc, destdb))
|
|
|
|
{
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2020-07-13 05:45:14 +00:00
|
|
|
// Tell cleanup everything is good to go
|
|
|
|
success = true;
|
|
|
|
|
2020-07-07 20:41:33 +00:00
|
|
|
return destdb;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
bool ProjectFileIO::ShouldVacuum(const std::shared_ptr<TrackList> &tracks)
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
std::set<long long> active;
|
|
|
|
unsigned long long current = 0;
|
2020-07-13 18:01:36 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Scan all wave tracks
|
|
|
|
for (auto wt : tracks->Any<const WaveTrack>())
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
|
|
|
// Scan all clips within current track
|
|
|
|
for (const auto &clip : wt->GetAllClips())
|
2020-07-13 18:01:36 +00:00
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
// Scan all blockfiles within current clip
|
|
|
|
auto blocks = clip->GetSequenceBlockArray();
|
|
|
|
for (const auto &block : *blocks)
|
2020-07-13 18:01:36 +00:00
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
const auto &sb = block.sb;
|
2020-07-17 21:17:42 +00:00
|
|
|
auto blockid = sb->GetBlockID();
|
2020-07-13 18:01:36 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Accumulate space used by the block if the blocckid has not
|
|
|
|
// yet been seen
|
|
|
|
if (active.count(blockid) == 0)
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
current += sb->GetSpaceUsage();
|
2020-07-15 06:32:48 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
active.insert(blockid);
|
|
|
|
}
|
2020-07-13 18:01:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-07-15 06:32:48 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// 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;
|
|
|
|
}
|
2020-07-13 18:01:36 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Verify we got the results we asked for
|
|
|
|
if (holder.size() != 1 || holder[0].size() != 2)
|
2020-07-13 18:01:36 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
// Shouldn't vacuum since we don't have the full picture
|
2020-07-13 18:01:36 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// 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;
|
|
|
|
|
2020-07-20 18:37:36 +00:00
|
|
|
wxLogDebug(wxT("used = %lld total = %lld %lld"), current, total, total ? current / total : 0);
|
2020-07-20 17:51:01 +00:00
|
|
|
if (!total || current / total > 80)
|
2020-07-13 18:01:36 +00:00
|
|
|
{
|
|
|
|
wxLogDebug(wxT("not vacuuming"));
|
2020-07-17 21:17:42 +00:00
|
|
|
return false;
|
2020-07-13 18:01:36 +00:00
|
|
|
}
|
|
|
|
wxLogDebug(wxT("vacuuming"));
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ProjectFileIO::Vacuum(const std::shared_ptr<TrackList> &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;
|
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// Don't vacuum if this is a temporary project or if it's deteremined there not
|
|
|
|
// enough unused blocks to make it worthwhile
|
2020-07-17 21:17:42 +00:00
|
|
|
if (IsTemporary() || !ShouldVacuum(tracks))
|
|
|
|
{
|
2020-07-20 16:10:31 +00:00
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-07-13 05:04:54 +00:00
|
|
|
|
|
|
|
// Create the project doc
|
|
|
|
ProjectSerializer doc;
|
|
|
|
WriteXMLHeader(doc);
|
2020-07-17 21:17:42 +00:00
|
|
|
WriteXML(doc, false, tracks);
|
|
|
|
|
|
|
|
wxString origName = mFileName;
|
|
|
|
wxString tempName = origName + "_vacuum";
|
2020-07-13 05:04:54 +00:00
|
|
|
|
|
|
|
// Must close the database to rename it
|
|
|
|
if (!CloseDB())
|
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
return;
|
2020-07-13 05:04:54 +00:00
|
|
|
}
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// Shouldn't need to do this, but doesn't hurt.
|
|
|
|
wxRemoveFile(tempName);
|
|
|
|
|
2020-07-13 05:04:54 +00:00
|
|
|
// If we can't rename the original to temporary, backout
|
|
|
|
if (!wxRenameFile(origName, tempName))
|
|
|
|
{
|
|
|
|
OpenDB(origName);
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
return;
|
2020-07-13 05:04:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If we can't reopen the original database using the temporary name, backout
|
2020-07-17 21:17:42 +00:00
|
|
|
sqlite3 *tempDB = nullptr;
|
|
|
|
if (sqlite3_open(tempName, &tempDB) != SQLITE_OK)
|
2020-07-13 05:04:54 +00:00
|
|
|
{
|
|
|
|
SetDBError(XO("Failed to open project file"));
|
|
|
|
// sqlite3 docs say you should close anyway to avoid leaks
|
2020-07-17 21:17:42 +00:00
|
|
|
sqlite3_close( tempDB );
|
2020-07-13 05:04:54 +00:00
|
|
|
|
|
|
|
wxRenameFile(tempName, origName);
|
|
|
|
|
|
|
|
OpenDB(origName);
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
return;
|
2020-07-13 05:04:54 +00:00
|
|
|
}
|
2020-07-17 21:17:42 +00:00
|
|
|
UseConnection(tempDB, tempName);
|
2020-07-13 05:04:54 +00:00
|
|
|
|
2020-07-16 06:10:54 +00:00
|
|
|
// Ensure connection gets configured
|
2020-07-17 21:17:42 +00:00
|
|
|
Config(mDB, SafeConfig);
|
2020-07-16 06:10:54 +00:00
|
|
|
|
2020-07-13 05:04:54 +00:00
|
|
|
// Copy the original database to a new database while pruning unused sample blocks
|
2020-07-17 21:17:42 +00:00
|
|
|
auto newDB = CopyTo(origName, XO("Compacting project"), true, tracks);
|
|
|
|
|
2020-07-20 17:51:01 +00:00
|
|
|
// Close handle to the original database, even if the copy failed
|
2020-07-17 21:17:42 +00:00
|
|
|
CloseDB();
|
|
|
|
|
2020-07-20 17:51:01 +00:00
|
|
|
// Reestablish the original name.
|
2020-07-17 21:17:42 +00:00
|
|
|
UseConnection(newDB, origName);
|
2020-07-13 05:04:54 +00:00
|
|
|
|
|
|
|
// If the copy failed or we aren't able to write the project doc, backout
|
2020-07-20 17:51:01 +00:00
|
|
|
if (!newDB)
|
2020-07-13 05:04:54 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
// Close the new database
|
2020-07-13 05:04:54 +00:00
|
|
|
sqlite3_close(newDB);
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// AUD3 warn user somehow
|
2020-07-13 05:04:54 +00:00
|
|
|
wxRemoveFile(origName);
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// AUD3 warn user somehow
|
2020-07-13 05:04:54 +00:00
|
|
|
wxRenameFile(tempName, origName);
|
|
|
|
|
2020-07-20 17:51:01 +00:00
|
|
|
// Reopen original file
|
|
|
|
OpenDB(origName);
|
2020-07-13 05:04:54 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
return;
|
2020-07-13 05:04:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
wxRemoveFile(tempName);
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Remember that we vacuumed
|
|
|
|
mWasVacuumed = true;
|
2020-07-13 05:04:54 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ProjectFileIO::WasVacuumed()
|
|
|
|
{
|
|
|
|
return mWasVacuumed;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ProjectFileIO::HadUnused()
|
|
|
|
{
|
|
|
|
return mHadUnused;
|
2020-07-13 05:04:54 +00:00
|
|
|
}
|
|
|
|
|
2019-05-29 15:45:19 +00:00
|
|
|
void ProjectFileIO::UpdatePrefs()
|
|
|
|
{
|
|
|
|
SetProjectTitle();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pass a number in to show project number, or -1 not to.
|
2020-07-01 05:45:17 +00:00
|
|
|
void ProjectFileIO::SetProjectTitle(int number)
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
auto pProject = mpProject.lock();
|
|
|
|
if (! pProject )
|
|
|
|
return;
|
|
|
|
|
|
|
|
auto &project = *pProject;
|
2019-06-10 15:40:30 +00:00
|
|
|
auto pWindow = project.GetFrame();
|
2020-07-01 05:45:17 +00:00
|
|
|
if (!pWindow)
|
|
|
|
{
|
2019-06-10 15:40:30 +00:00
|
|
|
return;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2019-06-10 15:40:30 +00:00
|
|
|
auto &window = *pWindow;
|
2019-05-29 15:45:19 +00:00
|
|
|
wxString name = project.GetProjectName();
|
|
|
|
|
|
|
|
// If we are showing project numbers, then we also explicitly show "<untitled>" if there
|
|
|
|
// is none.
|
2020-07-01 05:45:17 +00:00
|
|
|
if (number >= 0)
|
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
/* i18n-hint: The %02i is the project number, the %s is the project name.*/
|
2020-07-01 05:45:17 +00:00
|
|
|
name = wxString::Format(_("[Project %02i] Audacity \"%s\""), number + 1,
|
|
|
|
name.empty() ? "<untitled>" : (const char *)name);
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
// If we are not showing numbers, then <untitled> shows as 'Audacity'.
|
2020-07-01 05:45:17 +00:00
|
|
|
else if (name.empty())
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
|
|
|
name = _TS("Audacity");
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
if (mRecovered)
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
|
|
|
name += wxT(" ");
|
|
|
|
/* i18n-hint: E.g this is recovered audio that had been lost.*/
|
|
|
|
name += _("(Recovered)");
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
if (name != window.GetTitle())
|
|
|
|
{
|
2020-04-21 17:07:01 +00:00
|
|
|
window.SetTitle( name );
|
|
|
|
window.SetName(name); // to make the nvda screen reader read the correct title
|
2020-04-21 16:26:37 +00:00
|
|
|
|
2020-04-21 17:07:01 +00:00
|
|
|
project.QueueEvent(
|
|
|
|
safenew wxCommandEvent{ EVT_PROJECT_TITLE_CHANGE } );
|
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
const FilePath &ProjectFileIO::GetFileName() const
|
|
|
|
{
|
|
|
|
return mFileName;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ProjectFileIO::SetFileName(const FilePath &fileName)
|
|
|
|
{
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
auto pProject = mpProject.lock();
|
|
|
|
if (! pProject )
|
|
|
|
return;
|
|
|
|
auto &project = *pProject;
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
mFileName = fileName;
|
2020-07-01 18:26:20 +00:00
|
|
|
|
|
|
|
if (mTemporary)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
project.SetProjectName({});
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
project.SetProjectName(wxFileName(mFileName).GetName());
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
SetProjectTitle();
|
|
|
|
}
|
|
|
|
|
2019-05-29 15:45:19 +00:00
|
|
|
bool ProjectFileIO::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
|
|
|
|
{
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
auto pProject = mpProject.lock();
|
|
|
|
if (! pProject )
|
|
|
|
return false;
|
|
|
|
auto &project = *pProject;
|
2020-07-01 05:45:17 +00:00
|
|
|
auto &window = GetProjectFrame(project);
|
|
|
|
auto &viewInfo = ViewInfo::Get(project);
|
|
|
|
auto &settings = ProjectSettings::Get(project);
|
|
|
|
|
2020-05-23 10:41:26 +00:00
|
|
|
wxString fileVersion;
|
2020-07-01 05:45:17 +00:00
|
|
|
wxString audacityVersion;
|
2019-05-29 15:45:19 +00:00
|
|
|
int requiredTags = 0;
|
|
|
|
long longVpos = 0;
|
|
|
|
|
|
|
|
// loop through attrs, which is a null-terminated list of
|
|
|
|
// attribute-value pairs
|
2020-07-01 05:45:17 +00:00
|
|
|
while (*attrs)
|
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
const wxChar *attr = *attrs++;
|
|
|
|
const wxChar *value = *attrs++;
|
|
|
|
|
|
|
|
if (!value || !XMLValueChecker::IsGoodString(value))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
break;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
if (viewInfo.ReadXMLAttribute(attr, value))
|
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
// 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++;
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
else if (!wxStrcmp(attr, wxT("audacityversion")))
|
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
audacityVersion = value;
|
|
|
|
requiredTags++;
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
else if (!wxStrcmp(attr, wxT("rate")))
|
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
double rate;
|
|
|
|
Internat::CompatibleToDouble(value, &rate);
|
|
|
|
settings.SetRate( rate );
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
else if (!wxStrcmp(attr, wxT("snapto")))
|
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
settings.SetSnapTo(wxString(value) == wxT("on") ? true : false);
|
|
|
|
}
|
|
|
|
|
|
|
|
else if (!wxStrcmp(attr, wxT("selectionformat")))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
settings.SetSelectionFormat(
|
|
|
|
NumericConverter::LookupFormat( NumericConverter::TIME, value) );
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-02-14 12:02:21 +00:00
|
|
|
else if (!wxStrcmp(attr, wxT("audiotimeformat")))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-02-14 12:02:21 +00:00
|
|
|
settings.SetAudioTimeFormat(
|
|
|
|
NumericConverter::LookupFormat( NumericConverter::TIME, value) );
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2020-02-14 12:02:21 +00:00
|
|
|
|
2019-05-29 15:45:19 +00:00
|
|
|
else if (!wxStrcmp(attr, wxT("frequencyformat")))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
settings.SetFrequencySelectionFormatName(
|
|
|
|
NumericConverter::LookupFormat( NumericConverter::FREQUENCY, value ) );
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
|
|
|
else if (!wxStrcmp(attr, wxT("bandwidthformat")))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
settings.SetBandwidthSelectionFormatName(
|
|
|
|
NumericConverter::LookupFormat( NumericConverter::BANDWIDTH, value ) );
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
} // while
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
if (longVpos != 0)
|
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
// PRL: It seems this must happen after SetSnapTo
|
2020-07-01 05:45:17 +00:00
|
|
|
viewInfo.vpos = longVpos;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
if (requiredTags < 2)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
// 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)
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
|
|
|
/* i18n-hint: %s will be replaced by the version number.*/
|
2019-12-07 19:30:07 +00:00
|
|
|
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.")
|
2020-07-01 05:45:17 +00:00
|
|
|
.Format(audacityVersion, AUDACITY_VERSION_STRING);
|
|
|
|
|
2019-12-07 19:30:07 +00:00
|
|
|
AudacityMessageBox(
|
|
|
|
msg,
|
|
|
|
XO("Can't open project file"),
|
|
|
|
wxOK | wxICON_EXCLAMATION | wxCENTRE,
|
|
|
|
&window);
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
return false;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-01 18:26:20 +00:00
|
|
|
if (wxStrcmp(tag, wxT("project")))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2019-05-29 15:45:19 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// All other tests passed, so we succeed
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
XMLTagHandler *ProjectFileIO::HandleXMLChild(const wxChar *tag)
|
|
|
|
{
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
auto pProject = mpProject.lock();
|
|
|
|
if (! pProject )
|
|
|
|
return nullptr;
|
|
|
|
auto &project = *pProject;
|
2020-07-01 05:45:17 +00:00
|
|
|
auto fn = ProjectFileIORegistry::Lookup(tag);
|
2019-05-29 15:45:19 +00:00
|
|
|
if (fn)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
return fn(project);
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
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"));
|
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
void ProjectFileIO::WriteXML(XMLWriter &xmlFile,
|
|
|
|
bool recording /* = false */,
|
|
|
|
const std::shared_ptr<TrackList> &tracks /* = nullptr */)
|
2019-05-29 15:45:19 +00:00
|
|
|
// may throw
|
|
|
|
{
|
Unitary changes (#599)
* Define SampleBlockFactory replacing static members of SampleBlock...
... This will become an abstract base class
* Sequence and WaveTrack only store SampleBlockFactory not Project...
... This adds a dependency from Track to SampleBlock which temporarily enlarges
a cycle in the dependency graph
* Register a global factory of SampleBlockFactory...
... so that later we can make an abstract SampleBlockFactory, separate from the
concrete implementation in terms of sqlite, and inject the dependency at startup
avoiding static dependency
* New concrete classes SqliteSampleBlock, SqliteSampleBlockFactory...
... separated from abstract base classes and put into a new source file,
breaking dependency cycles, and perhaps allowing easy reimplementation for other
databases in the future.
Note that the new file is a header-less plug-in! Nothing depends on it. It
uses static initialization to influence the program's behavior.
* Compile dependency on sqlite3.h limited to just two .cpp files...
... these are ProjectFileIO.cpp and SqliteSampleBlock.cpp.
But there is still close cooperation of ProjectFileIO and SqliteSampleBlock.cpp.
This suggests that these files ought to be merged, and perhaps ProjectFileIO
also needs to be split into abstract and concrete classes, and there should be
another injection of a factory function at startup. That will make the choice
of database implementation even more modular.
Also removed one unnecessary inclusion of ProjectFileIO.h
* Fix crashes cutting and pasting cross-project...
... in case the source project is closed before the paste happens.
This caused destruction of the ProjectFileIO object and a closing of the sqlite
database with the sample data in it, leaving dangling references in the
SqliteSampleBlock objects.
The fix is that the SqliteSampleBlockFactory object holds a shared_ptr to the
ProjectFileIO object. So the clipboard may own WaveTracks, which own WaveClips,
which own Sequences, which own SqliteSampleBlockFactories, which keep the
ProjectFileIO and the database connection alive until the clipboard is cleared.
The consequence of the fix is delayed closing of the entire database associated
with the source project.
If the source project is reopened before the clipboard is cleared, will there
be correct concurrent access to the same persistent store? My preliminary
trials suggest this is so (reopening a saved project, deleting from it, closing
it again -- the clipboard contents are still unchanged and available).
2020-07-02 23:11:38 +00:00
|
|
|
auto pProject = mpProject.lock();
|
|
|
|
if (! pProject )
|
|
|
|
THROW_INCONSISTENCY_EXCEPTION;
|
|
|
|
auto &proj = *pProject;
|
2020-07-17 21:17:42 +00:00
|
|
|
auto &tracklist = tracks ? *tracks : TrackList::Get(proj);
|
2020-07-01 05:45:17 +00:00
|
|
|
auto &viewInfo = ViewInfo::Get(proj);
|
|
|
|
auto &tags = Tags::Get(proj);
|
|
|
|
const auto &settings = ProjectSettings::Get(proj);
|
2019-06-07 15:54:20 +00:00
|
|
|
|
2019-05-29 15:45:19 +00:00
|
|
|
//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;
|
2020-07-11 05:49:37 +00:00
|
|
|
tracklist.Any().Visit([&](Track *t)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-11 05:49:37 +00:00
|
|
|
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();
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
2020-07-11 05:49:37 +00:00
|
|
|
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);
|
|
|
|
});
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
xmlFile.EndTag(wxT("project"));
|
|
|
|
|
|
|
|
//TIMER_STOP( xml_writer_timer );
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-11 05:49:37 +00:00
|
|
|
bool ProjectFileIO::AutoSave(bool recording)
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
2020-07-10 05:50:52 +00:00
|
|
|
ProjectSerializer autosave;
|
2020-07-01 05:45:17 +00:00
|
|
|
WriteXMLHeader(autosave);
|
2020-07-11 05:49:37 +00:00
|
|
|
WriteXML(autosave, recording);
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
if (WriteDoc("autosave", autosave))
|
|
|
|
{
|
|
|
|
mModified = true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-10 05:50:52 +00:00
|
|
|
bool ProjectFileIO::AutoSaveDelete(sqlite3 *db /* = nullptr */)
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
2020-07-01 05:45:17 +00:00
|
|
|
int rc;
|
|
|
|
|
2020-07-10 05:50:52 +00:00
|
|
|
if (!db)
|
|
|
|
{
|
|
|
|
db = DB();
|
|
|
|
}
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
mModified = false;
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-10 05:50:52 +00:00
|
|
|
bool ProjectFileIO::WriteDoc(const char *table,
|
|
|
|
const ProjectSerializer &autosave,
|
2020-07-13 05:04:54 +00:00
|
|
|
sqlite3 *db /* = nullptr */)
|
2020-07-09 18:14:12 +00:00
|
|
|
{
|
|
|
|
int rc;
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2020-07-10 05:50:52 +00:00
|
|
|
if (!db)
|
|
|
|
{
|
|
|
|
db = DB();
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
// 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,
|
2020-07-09 18:14:12 +00:00
|
|
|
"INSERT INTO %s(id, dict, doc) VALUES(1, ?1, ?2)"
|
2020-07-10 05:50:52 +00:00
|
|
|
" ON CONFLICT(id) DO UPDATE SET dict = ?1, doc = ?2;",
|
|
|
|
table);
|
2020-07-01 05:45:17 +00:00
|
|
|
|
|
|
|
sqlite3_stmt *stmt = nullptr;
|
|
|
|
auto cleanup = finally([&]
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
2020-07-01 05:45:17 +00:00
|
|
|
if (stmt)
|
|
|
|
{
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
|
2020-07-01 05:45:17 +00:00
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetDBError(
|
|
|
|
XO("Unable to prepare project file command:\n\n%s").Format(sql)
|
|
|
|
);
|
2020-07-01 05:45:17 +00:00
|
|
|
return false;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
const wxMemoryBuffer &dict = autosave.GetDict();
|
|
|
|
const wxMemoryBuffer &data = autosave.GetData();
|
|
|
|
|
2020-07-01 13:55:06 +00:00
|
|
|
// BIND SQL autosave
|
2020-07-07 20:41:33 +00:00
|
|
|
// 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)
|
|
|
|
)
|
2020-07-09 18:14:12 +00:00
|
|
|
{
|
2020-07-07 20:41:33 +00:00
|
|
|
THROW_INCONSISTENCY_EXCEPTION;
|
2020-07-09 18:14:12 +00:00
|
|
|
}
|
2020-07-01 05:45:17 +00:00
|
|
|
|
|
|
|
rc = sqlite3_step(stmt);
|
|
|
|
if (rc != SQLITE_DONE)
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetDBError(
|
|
|
|
XO("Failed to update the project file.\nThe following command failed:\n\n%s").Format(sql)
|
|
|
|
);
|
2020-07-01 05:45:17 +00:00
|
|
|
return false;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// 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)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
// Get access to the current project file
|
|
|
|
auto db = DB();
|
2020-07-15 12:57:20 +00:00
|
|
|
|
2020-07-01 18:26:20 +00:00
|
|
|
bool success = false;
|
2020-07-15 06:32:48 +00:00
|
|
|
bool restore = true;
|
|
|
|
int rc;
|
2020-07-01 18:26:20 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Ensure the inbound database gets detached
|
|
|
|
auto detach = finally([&]
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
sqlite3_exec(db, "DETACH DATABASE inbound;", nullptr, nullptr, nullptr);
|
|
|
|
});
|
2020-07-15 06:32:48 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Attach the inbound project file
|
|
|
|
wxString sql;
|
|
|
|
sql.Printf("ATTACH DATABASE 'file:%s?immutable=1&mode=ro' AS inbound;", fileName);
|
2020-07-15 06:32:48 +00:00
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
|
2020-07-17 21:17:42 +00:00
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
|
|
|
SetDBError(
|
|
|
|
XO("Unable to attach %s project file").Format(fileName)
|
|
|
|
);
|
2020-07-15 06:32:48 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
return false;
|
|
|
|
}
|
2020-07-16 06:10:54 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// We need either the autosave or project docs from the inbound AUP3
|
|
|
|
wxMemoryBuffer buffer;
|
2020-07-15 06:32:48 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Get the autosave doc, if any
|
|
|
|
if (!GetBlob("SELECT dict || doc FROM inbound.project WHERE id = 1;", buffer))
|
|
|
|
{
|
|
|
|
// Error already set
|
|
|
|
return false;
|
|
|
|
}
|
2020-07-15 06:32:48 +00:00
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// 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))
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
|
|
|
// Error already set
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Missing both the autosave and project docs...this shouldn't happen!!!
|
|
|
|
if (buffer.GetDataLen() > 0)
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
SetError(XO("Unable to load project or autosave documents"));
|
2020-07-15 06:32:48 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2020-07-17 21:17:42 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// 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<wxXmlNode *> blocknodes;
|
|
|
|
std::function<void(wxXmlNode *)> findblocks = [&](wxXmlNode *node)
|
|
|
|
{
|
|
|
|
while (node)
|
|
|
|
{
|
|
|
|
if (node->GetName().IsSameAs(wxT("waveblock")))
|
|
|
|
{
|
|
|
|
blocknodes.push_back(node);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
findblocks(node->GetChildren());
|
|
|
|
}
|
|
|
|
|
|
|
|
node = node->GetNext();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Get access to the active tracklist
|
|
|
|
auto pProject = mpProject.lock();
|
|
|
|
if (!pProject)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2020-07-15 12:57:20 +00:00
|
|
|
auto &tracklist = TrackList::Get(*pProject);
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Search for a timetrack and remove it if the project already has one
|
2020-07-15 12:57:20 +00:00
|
|
|
if (*tracklist.Any<TimeTrack>().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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// 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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
2020-07-16 06:10:54 +00:00
|
|
|
// Cleanup...
|
|
|
|
sqlite3_stmt *stmt = nullptr;
|
2020-07-15 06:32:48 +00:00
|
|
|
auto cleanup = finally([&]
|
|
|
|
{
|
2020-07-16 06:10:54 +00:00
|
|
|
// Ensure the prepared statement gets cleaned up
|
|
|
|
if (stmt)
|
|
|
|
{
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
}
|
2020-07-15 06:32:48 +00:00
|
|
|
});
|
|
|
|
|
2020-07-16 06:10:54 +00:00
|
|
|
// 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);
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
|
2020-07-16 06:10:54 +00:00
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
{
|
|
|
|
SetDBError(
|
|
|
|
XO("Unable to prepare project file command:\n\n%s").Format(sql)
|
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
/* i18n-hint: This title appears on a dialog that indicates the progress
|
|
|
|
in doing something.*/
|
2020-07-16 06:10:54 +00:00
|
|
|
ProgressDialog progress(XO("Progress"), XO("Importing project"), pdlgHideStopButton);
|
2020-07-15 06:32:48 +00:00
|
|
|
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<SampleBlockID> 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);
|
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
|
2020-07-15 06:32:48 +00:00
|
|
|
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);
|
|
|
|
|
2020-07-16 06:10:54 +00:00
|
|
|
// 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)
|
2020-07-15 06:32:48 +00:00
|
|
|
{
|
|
|
|
SetDBError(
|
2020-07-16 06:10:54 +00:00
|
|
|
XO("Failed to import sample block.\nThe following command failed:\n\n%s").Format(sql)
|
2020-07-15 06:32:48 +00:00
|
|
|
);
|
2020-07-16 06:10:54 +00:00
|
|
|
return false;
|
2020-07-15 06:32:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Replace the original blockid with the new one
|
|
|
|
attr->SetValue(wxString::Format(wxT("%lld"), sqlite3_last_insert_rowid(db)));
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Reset the statement for the next iteration
|
2020-07-16 06:10:54 +00:00
|
|
|
if (sqlite3_reset(stmt) != SQLITE_OK)
|
|
|
|
{
|
|
|
|
THROW_INCONSISTENCY_EXCEPTION;
|
|
|
|
}
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// 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;
|
2020-07-01 18:26:20 +00:00
|
|
|
|
|
|
|
auto cleanup = finally([&]
|
|
|
|
{
|
|
|
|
if (!success)
|
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
RestoreConnection();
|
2020-07-01 18:26:20 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
SaveConnection();
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
// Open the project file
|
2020-07-01 18:26:20 +00:00
|
|
|
if (!OpenDB(fileName))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
BlockIDs blockids;
|
|
|
|
wxString project;
|
|
|
|
wxMemoryBuffer buffer;
|
2020-07-15 06:32:48 +00:00
|
|
|
bool usedAutosave = true;
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
// Get the autosave doc, if any
|
2020-07-09 18:14:12 +00:00
|
|
|
if (!GetBlob("SELECT dict || doc FROM autosave WHERE id = 1;", buffer))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-08 05:18:05 +00:00
|
|
|
// Error already set
|
|
|
|
return false;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2020-07-15 06:32:48 +00:00
|
|
|
|
|
|
|
// If we didn't have an autosave doc, load the project doc instead
|
|
|
|
if (buffer.GetDataLen() == 0)
|
2020-07-09 18:14:12 +00:00
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
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;
|
|
|
|
}
|
2020-07-09 18:14:12 +00:00
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// Decode it while capturing the associated sample blockids
|
|
|
|
project = ProjectSerializer::Decode(buffer, blockids);
|
|
|
|
if (project.empty())
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
SetError(XO("Unable to decode project document"));
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// Check for orphans blocks...sets mRecovered if any were deleted
|
2020-07-09 18:14:12 +00:00
|
|
|
if (blockids.size() > 0)
|
|
|
|
{
|
|
|
|
if (!CheckForOrphans(blockids))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
XMLFileReader xmlFile;
|
2020-07-08 05:18:05 +00:00
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
// Load 'er up
|
|
|
|
success = xmlFile.ParseString(this, project);
|
2019-05-29 15:45:19 +00:00
|
|
|
if (!success)
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
SetError(
|
|
|
|
XO("Unable to parse project information.")
|
|
|
|
);
|
|
|
|
mLibraryError = xmlFile.GetErrorStr();
|
|
|
|
return false;
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
2019-05-29 15:45:19 +00:00
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
// Remember if we used autosave or not
|
2020-07-15 06:32:48 +00:00
|
|
|
if (usedAutosave)
|
2020-07-08 05:18:05 +00:00
|
|
|
{
|
2020-07-09 18:14:12 +00:00
|
|
|
mRecovered = true;
|
2020-07-08 05:18:05 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
// Mark the project modified if we recovered it
|
2020-07-01 05:45:17 +00:00
|
|
|
if (mRecovered)
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
2020-07-01 05:45:17 +00:00
|
|
|
mModified = true;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-01 18:26:20 +00:00
|
|
|
// 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
|
2020-07-15 06:32:48 +00:00
|
|
|
// file or a permanent project file
|
2020-07-08 05:18:05 +00:00
|
|
|
wxString result;
|
|
|
|
if (!GetValue("SELECT Count(*) FROM project;", result))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
mTemporary = (wxStrtol<char **>(result, nullptr, 10) != 1);
|
2020-07-01 18:26:20 +00:00
|
|
|
|
|
|
|
SetFileName(fileName);
|
|
|
|
|
2020-07-15 06:32:48 +00:00
|
|
|
DiscardConnection();
|
|
|
|
|
2020-07-01 18:26:20 +00:00
|
|
|
return true;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
bool ProjectFileIO::SaveProject(const FilePath &fileName)
|
2019-05-29 15:45:19 +00:00
|
|
|
{
|
2020-07-01 18:26:20 +00:00
|
|
|
wxString origName;
|
|
|
|
bool wasTemp = false;
|
|
|
|
bool success = false;
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
// Should probably simplify all of the following by using renames.
|
2020-07-01 18:26:20 +00:00
|
|
|
|
|
|
|
auto restore = finally([&]
|
|
|
|
{
|
|
|
|
if (!origName.empty())
|
|
|
|
{
|
|
|
|
if (success)
|
|
|
|
{
|
2020-07-07 20:41:33 +00:00
|
|
|
// 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
|
2020-07-01 18:26:20 +00:00
|
|
|
if (wasTemp)
|
|
|
|
{
|
|
|
|
wxRemoveFile(origName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-07-07 20:41:33 +00:00
|
|
|
// Close the new database and go back to using the original
|
|
|
|
// connection
|
|
|
|
RestoreConnection();
|
2020-07-01 18:26:20 +00:00
|
|
|
|
2020-07-07 20:41:33 +00:00
|
|
|
// And delete the new database
|
|
|
|
wxRemoveFile(fileName);
|
2020-07-01 18:26:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
// 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"));
|
2020-07-07 20:41:33 +00:00
|
|
|
if (!newDB)
|
2020-07-01 18:26:20 +00:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
|
2020-07-07 20:41:33 +00:00
|
|
|
// 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();
|
2020-07-17 21:17:42 +00:00
|
|
|
|
|
|
|
// Make the new connection "safe"
|
|
|
|
Config(newDB, SafeConfig);
|
|
|
|
|
|
|
|
// And make it the active project file
|
|
|
|
UseConnection(newDB, fileName);
|
2020-07-20 16:10:31 +00:00
|
|
|
|
|
|
|
// Install our checkpoint hook
|
|
|
|
sqlite3_wal_hook(mDB, CheckpointHook, this);
|
2020-07-01 18:26:20 +00:00
|
|
|
}
|
2020-07-20 17:51:01 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
ProjectSerializer doc;
|
|
|
|
WriteXMLHeader(doc);
|
|
|
|
WriteXML(doc);
|
2020-07-01 18:26:20 +00:00
|
|
|
|
2020-07-20 17:51:01 +00:00
|
|
|
if (!WriteDoc("project", doc))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2020-07-01 05:45:17 +00:00
|
|
|
|
2020-07-20 17:51:01 +00:00
|
|
|
// Autosave no longer needed
|
|
|
|
AutoSaveDelete();
|
2020-07-01 05:45:17 +00:00
|
|
|
}
|
|
|
|
|
2020-07-07 20:41:33 +00:00
|
|
|
// Reaching this point defines success and all the rest are no-fail
|
|
|
|
// operations:
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
// No longer modified
|
|
|
|
mModified = false;
|
|
|
|
|
|
|
|
// No longer recovered
|
|
|
|
mRecovered = false;
|
|
|
|
|
|
|
|
// No longer a temporary project
|
|
|
|
mTemporary = false;
|
|
|
|
|
|
|
|
// Adjust the title
|
|
|
|
SetProjectTitle();
|
|
|
|
|
2020-07-17 21:17:42 +00:00
|
|
|
// Tell the finally block to behave
|
|
|
|
success = true;
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
return true;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2020-07-03 19:38:57 +00:00
|
|
|
bool ProjectFileIO::SaveCopy(const FilePath& fileName)
|
|
|
|
{
|
2020-07-15 06:32:48 +00:00
|
|
|
auto db = CopyTo(fileName, XO("Backing up project"), true);
|
2020-07-07 20:41:33 +00:00
|
|
|
if (!db)
|
2020-07-03 19:38:57 +00:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-07-03 19:38:57 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-07-01 05:45:17 +00:00
|
|
|
bool ProjectFileIO::IsModified() const
|
|
|
|
{
|
|
|
|
return mModified;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ProjectFileIO::IsTemporary() const
|
|
|
|
{
|
|
|
|
return mTemporary;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ProjectFileIO::IsRecovered() const
|
|
|
|
{
|
|
|
|
return mRecovered;
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
|
|
|
|
2019-06-07 17:24:38 +00:00
|
|
|
void ProjectFileIO::Reset()
|
|
|
|
{
|
2020-07-01 05:45:17 +00:00
|
|
|
wxASSERT_MSG(mDB == nullptr, wxT("Resetting project with open project file"));
|
|
|
|
|
|
|
|
mModified = false;
|
|
|
|
mRecovered = false;
|
|
|
|
|
|
|
|
SetFileName({});
|
2019-05-29 15:45:19 +00:00
|
|
|
}
|
2020-07-01 05:45:17 +00:00
|
|
|
|
|
|
|
wxLongLong ProjectFileIO::GetFreeDiskSpace()
|
|
|
|
{
|
|
|
|
// make sure it's open and the path is defined
|
|
|
|
auto db = DB();
|
|
|
|
|
|
|
|
wxLongLong freeSpace;
|
2020-07-01 18:26:20 +00:00
|
|
|
if (wxGetDiskSpace(wxPathOnly(mFileName), NULL, &freeSpace))
|
2020-07-01 05:45:17 +00:00
|
|
|
{
|
|
|
|
return freeSpace;
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
const TranslatableString & ProjectFileIO::GetLastError() const
|
|
|
|
{
|
|
|
|
return mLastError;
|
|
|
|
}
|
|
|
|
|
2020-07-01 18:26:20 +00:00
|
|
|
const TranslatableString & ProjectFileIO::GetLibraryError() const
|
|
|
|
{
|
2020-07-02 16:42:25 +00:00
|
|
|
return mLibraryError;
|
2020-07-01 18:26:20 +00:00
|
|
|
}
|
|
|
|
|
2020-07-03 03:08:39 +00:00
|
|
|
void ProjectFileIO::SetError(const TranslatableString &msg)
|
2020-07-01 18:26:20 +00:00
|
|
|
{
|
|
|
|
mLastError = msg;
|
|
|
|
mLibraryError = {};
|
|
|
|
}
|
|
|
|
|
2020-07-03 03:08:39 +00:00
|
|
|
void ProjectFileIO::SetDBError(const TranslatableString &msg)
|
2020-07-01 18:26:20 +00:00
|
|
|
{
|
|
|
|
mLastError = msg;
|
2020-07-08 06:11:38 +00:00
|
|
|
wxLogDebug(wxT("SQLite error: %s"), mLastError.Debug());
|
2020-07-17 21:17:42 +00:00
|
|
|
printf(" Lib error: %s", mLastError.Debug().mb_str().data());
|
|
|
|
|
2020-07-01 18:26:20 +00:00
|
|
|
if (mDB)
|
|
|
|
{
|
|
|
|
mLibraryError = Verbatim(sqlite3_errmsg(mDB));
|
2020-07-08 06:11:38 +00:00
|
|
|
wxLogDebug(wxT(" Lib error: %s"), mLibraryError.Debug());
|
2020-07-17 21:17:42 +00:00
|
|
|
printf(" Lib error: %s", mLibraryError.Debug().mb_str().data());
|
2020-07-01 18:26:20 +00:00
|
|
|
}
|
2020-07-20 16:10:31 +00:00
|
|
|
abort();
|
2020-07-17 21:17:42 +00:00
|
|
|
wxASSERT(false);
|
2020-07-01 18:26:20 +00:00
|
|
|
}
|
2020-07-03 03:08:39 +00:00
|
|
|
|
2020-07-20 16:10:31 +00:00
|
|
|
void ProjectFileIO::SetBypass()
|
2020-07-03 03:08:39 +00:00
|
|
|
{
|
2020-07-20 16:10:31 +00:00
|
|
|
// 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;
|
2020-07-03 03:08:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ProjectFileIO::ShouldBypass()
|
|
|
|
{
|
2020-07-17 21:17:42 +00:00
|
|
|
return mBypass;
|
2020-07-03 03:08:39 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 18:14:12 +00:00
|
|
|
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;
|
|
|
|
}
|