1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-09-17 16:50:26 +02:00

AUP3: Reworked CopyTo() yet again

This time it has the potential to produce much smaller
output files since it ONLY copies the active blocks and
not all of the blocks related to undo history.
This commit is contained in:
Leland Lucius 2020-07-12 10:39:37 -05:00
parent 5ffff72d35
commit e3c5563d35
4 changed files with 115 additions and 67 deletions

View File

@ -19,8 +19,10 @@ Paul Licameli split from AudacityProject.cpp
#include "ProjectFileIORegistry.h" #include "ProjectFileIORegistry.h"
#include "ProjectSerializer.h" #include "ProjectSerializer.h"
#include "ProjectSettings.h" #include "ProjectSettings.h"
#include "SampleBlock.h"
#include "Tags.h" #include "Tags.h"
#include "ViewInfo.h" #include "ViewInfo.h"
#include "WaveClip.h"
#include "WaveTrack.h" #include "WaveTrack.h"
#include "widgets/AudacityMessageBox.h" #include "widgets/AudacityMessageBox.h"
#include "widgets/NumericTextCtrl.h" #include "widgets/NumericTextCtrl.h"
@ -41,10 +43,10 @@ static const int ProjectFileVersion = 1;
// to sampleblocks. // to sampleblocks.
static const char *ProjectFileSchema = static const char *ProjectFileSchema =
"PRAGMA application_id = %d;" "PRAGMA <dbname>.application_id = %d;"
"PRAGMA user_version = %d;" "PRAGMA <dbname>.user_version = %d;"
"PRAGMA journal_mode = WAL;" "PRAGMA <dbname>.journal_mode = WAL;"
"PRAGMA locking_mode = EXCLUSIVE;" "PRAGMA <dbname>.locking_mode = EXCLUSIVE;"
"" ""
// project is a binary representation of an XML file. // project is a binary representation of an XML file.
// it's in binary for speed. // it's in binary for speed.
@ -58,7 +60,7 @@ static const char *ProjectFileSchema =
// There is no limit to document blob size. // There is no limit to document blob size.
// dict will be smallish, with an entry for each // dict will be smallish, with an entry for each
// kind of field. // kind of field.
"CREATE TABLE IF NOT EXISTS project" "CREATE TABLE IF NOT EXISTS <dbname>.project"
"(" "("
" id INTEGER PRIMARY KEY," " id INTEGER PRIMARY KEY,"
" dict BLOB," " dict BLOB,"
@ -78,7 +80,7 @@ static const char *ProjectFileSchema =
// There is no limit to document blob size. // There is no limit to document blob size.
// dict will be smallish, with an entry for each // dict will be smallish, with an entry for each
// kind of field. // kind of field.
"CREATE TABLE IF NOT EXISTS autosave" "CREATE TABLE IF NOT EXISTS <dbname>.autosave"
"(" "("
" id INTEGER PRIMARY KEY," " id INTEGER PRIMARY KEY,"
" dict BLOB," " dict BLOB,"
@ -87,7 +89,7 @@ static const char *ProjectFileSchema =
"" ""
// CREATE SQL tags // CREATE SQL tags
// tags is not used (yet) // tags is not used (yet)
"CREATE TABLE IF NOT EXISTS tags" "CREATE TABLE IF NOT EXISTS <dbname>.tags"
"(" "("
" name TEXT," " name TEXT,"
" value BLOB" " value BLOB"
@ -104,7 +106,7 @@ static const char *ProjectFileSchema =
// blockID is a 64 bit number. // blockID is a 64 bit number.
// //
// summin to summary64K are summaries at 3 distance scales. // summin to summary64K are summaries at 3 distance scales.
"CREATE TABLE IF NOT EXISTS sampleblocks" "CREATE TABLE IF NOT EXISTS <dbname>.sampleblocks"
"(" "("
" blockid INTEGER PRIMARY KEY AUTOINCREMENT," " blockid INTEGER PRIMARY KEY AUTOINCREMENT,"
" sampleformat INTEGER," " sampleformat INTEGER,"
@ -609,7 +611,7 @@ bool ProjectFileIO::CheckVersion()
// must be a new project file. // must be a new project file.
if (wxStrtol<char **>(result, nullptr, 10) == 0) if (wxStrtol<char **>(result, nullptr, 10) == 0)
{ {
return InstallSchema(); return InstallSchema(db);
} }
// Check for our application ID // Check for our application ID
@ -653,19 +655,15 @@ bool ProjectFileIO::CheckVersion()
return true; return true;
} }
bool ProjectFileIO::InstallSchema() bool ProjectFileIO::InstallSchema(sqlite3 *db, const char *dbname /* = "main" */)
{ {
auto db = DB();
int rc; int rc;
char sql[1024]; wxString sql;
sqlite3_snprintf(sizeof(sql), sql.Printf(ProjectFileSchema, ProjectFileID, ProjectFileVersion);
sql, sql.Replace("<dbname>", dbname);
ProjectFileSchema,
ProjectFileID,
ProjectFileVersion);
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); rc = sqlite3_exec(db, sql.mb_str().data(), nullptr, nullptr, nullptr);
if (rc != SQLITE_OK) if (rc != SQLITE_OK)
{ {
SetDBError( SetDBError(
@ -688,12 +686,12 @@ bool ProjectFileIO::UpgradeSchema()
// An SQLite function that takes a blockid and looks it up in a set of // 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 // blockids captured during project load. If the blockid isn't found
// in the set, it will be deleted. // in the set, it will be deleted.
void ProjectFileIO::isorphan(sqlite3_context *context, int argc, sqlite3_value **argv) void ProjectFileIO::InSet(sqlite3_context *context, int argc, sqlite3_value **argv)
{ {
BlockIDs *blockids = (BlockIDs *) sqlite3_user_data(context); BlockIDs *blockids = (BlockIDs *) sqlite3_user_data(context);
SampleBlockID blockid = sqlite3_value_int64(argv[0]); SampleBlockID blockid = sqlite3_value_int64(argv[0]);
sqlite3_result_int(context, blockids->find(blockid) == blockids->end()); sqlite3_result_int(context, blockids->find(blockid) != blockids->end());
} }
bool ProjectFileIO::CheckForOrphans(BlockIDs &blockids) bool ProjectFileIO::CheckForOrphans(BlockIDs &blockids)
@ -701,18 +699,25 @@ bool ProjectFileIO::CheckForOrphans(BlockIDs &blockids)
auto db = DB(); auto db = DB();
int rc; int rc;
// Add our function that will verify blockid against the set of valid blockids auto cleanup = finally([&]
rc = sqlite3_create_function(db, "isorphan", 1, SQLITE_UTF8, &blockids, isorphan, nullptr, nullptr); {
// Remove our function, whether it was successfully defined or not.
sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr, nullptr, nullptr, nullptr);
});
// Add the function used to verify each rows blockid against the set of active blockids
rc = sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, &blockids, InSet, nullptr, nullptr);
if (rc != SQLITE_OK) if (rc != SQLITE_OK)
{ {
//asdfasdf wxLogDebug(wxT("Unable to add 'inset' function"));
return false; return false;
} }
// Delete all rows that are orphaned // Delete all rows that are orphaned
rc = sqlite3_exec(db, "DELETE FROM sampleblocks WHERE isorphan(blockid);", nullptr, nullptr, nullptr); rc = sqlite3_exec(db, "DELETE FROM sampleblocks WHERE NOT inset(blockid);", nullptr, nullptr, nullptr);
if (rc != SQLITE_OK) if (rc != SQLITE_OK)
{ {
wxLogWarning(XO("Cleanup of orphan blocks failed").Translation());
return false; return false;
} }
@ -745,32 +750,81 @@ static int progress_callback(void *data)
sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath) sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath)
{ {
// Get access to the active tracklist
auto pProject = mpProject.lock();
if (!pProject)
{
return nullptr;
}
auto &tracklist = TrackList::Get(*pProject);
// Collect all active blockids
BlockIDs blockids;
for (auto wt : tracklist.Any<const WaveTrack>())
{
// Scan all clips within current track
for (const auto &clip : wt->GetAllClips())
{
// Scan all blockfiles within current clip
auto blocks = clip->GetSequenceBlockArray();
for (const auto &block : *blocks)
{
blockids.insert(block.sb->GetBlockID());
}
}
}
auto db = DB(); auto db = DB();
sqlite3 *destdb = nullptr;
bool success = false;
int rc; int rc;
ProgressResult res = ProgressResult::Success; ProgressResult res = ProgressResult::Success;
sqlite3_stmt *stmt = nullptr; // Cleanup in case things go awry
auto cleanup = finally([&] auto cleanup = finally([&]
{ {
if (stmt) // Detach the destination database, whether it was successfully attached or not
sqlite3_exec(db, "DETACH DATABASE dest;", nullptr, nullptr, nullptr);
// Remove our function, whether it was successfully defined or not.
sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr, nullptr, nullptr, nullptr);
if (!success)
{ {
sqlite3_finalize(stmt); sqlite3_close(destdb);
wxRemoveFile(destpath);
} }
}); });
rc = sqlite3_prepare_v2(db, "VACUUM INTO ?;", -1, &stmt, 0); // Add the function used to verify each rows blockid against the set of active blockids
rc = sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, &blockids, InSet, nullptr, nullptr);
if (rc != SQLITE_OK) if (rc != SQLITE_OK)
{ {
SetDBError( SetDBError(
XO("Unable to prepare project file command") XO("Unable to add 'inset' function")
); );
return nullptr; return nullptr;
} }
rc = sqlite3_bind_text(stmt, 1, destpath.mb_str().data(), destpath.mb_str().length(), SQLITE_STATIC); // Attach the destination database
wxString sql;
sql.Printf("ATTACH DATABASE '%s' AS dest;", destpath);
rc = sqlite3_exec(db, sql.mb_str().data(), nullptr, nullptr, nullptr);
if (rc != SQLITE_OK) if (rc != SQLITE_OK)
{ {
THROW_INCONSISTENCY_EXCEPTION; SetDBError(
XO("Unable to attach destination database")
);
return nullptr;
}
// Install our schema into the new database
if (!InstallSchema(db, "dest"))
{
// Message already set
return nullptr;
} }
{ {
@ -780,27 +834,37 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath)
sqlite3_progress_handler(db, 10, progress_callback, &progress); sqlite3_progress_handler(db, 10, progress_callback, &progress);
rc = sqlite3_step(stmt); rc = sqlite3_exec(db,
"INSERT INTO dest.tags"
" SELECT * FROM main.tags;",
nullptr,
nullptr,
nullptr);
if (rc == SQLITE_OK)
{
rc = sqlite3_exec(db,
"INSERT INTO dest.sampleblocks"
" SELECT * FROM main.sampleblocks"
" WHERE inset(blockid);",
nullptr,
nullptr,
nullptr);
}
sqlite3_progress_handler(db, 0, nullptr, nullptr); sqlite3_progress_handler(db, 0, nullptr, nullptr);
} }
sqlite3_finalize(stmt); // Copy failed
stmt = nullptr; if (rc != SQLITE_OK)
// VACUUMing failed
if (rc != SQLITE_DONE)
{ {
SetDBError( SetDBError(
XO("Project file copy failed") XO("Failed to copy project file")
); );
wxRemoveFile(destpath);
return nullptr; return nullptr;
} }
sqlite3 *destdb = nullptr; // Open the newly created database
rc = sqlite3_open(destpath, &destdb); rc = sqlite3_open(destpath, &destdb);
if (rc != SQLITE_OK) if (rc != SQLITE_OK)
{ {
@ -808,13 +872,12 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath)
XO("Failed to open copy of project file") XO("Failed to open copy of project file")
); );
sqlite3_close(destdb);
wxRemoveFile(destpath);
return nullptr; return nullptr;
} }
// Tell cleanup everything is good to go
success = true;
return destdb; return destdb;
} }
@ -1391,14 +1454,6 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
return false; return false;
} }
// We need to remove the autosave info from the file since it is now
// clean and unmodified. Otherwise, it would be considered "recovered"
// when next opened.
if (!AutoSaveDelete())
{
return false;
}
// Reaching this point defines success and all the rest are no-fail // Reaching this point defines success and all the rest are no-fail
// operations: // operations:
@ -1453,14 +1508,6 @@ bool ProjectFileIO::SaveCopy(const FilePath& fileName)
return false; return false;
} }
// We need to remove the autosave info from the new DB since it is now
// clean and unmodified. Otherwise, it would be considered "recovered"
// when next opened.
if (!AutoSaveDelete(db))
{
return false;
}
// Tell the finally block to behave // Tell the finally block to behave
success = true; success = true;

View File

@ -150,15 +150,17 @@ private:
bool GetBlob(const char *sql, wxMemoryBuffer &buffer); bool GetBlob(const char *sql, wxMemoryBuffer &buffer);
bool CheckVersion(); bool CheckVersion();
bool InstallSchema(); bool InstallSchema(sqlite3 *db, const char *dbname = "main");
bool UpgradeSchema(); bool UpgradeSchema();
// Write project or autosave XML (binary) documents // Write project or autosave XML (binary) documents
bool WriteDoc(const char *table, const ProjectSerializer &autosave, sqlite3 *db = nullptr); bool WriteDoc(const char *table, const ProjectSerializer &autosave, sqlite3 *db = nullptr);
// Checks for orphan blocks. This will go away at a future date // Application defined function to verify blockid exists is in set of blockids
using BlockIDs = std::set<SampleBlockID>; using BlockIDs = std::set<SampleBlockID>;
static void isorphan(sqlite3_context *context, int argc, sqlite3_value **argv); static void InSet(sqlite3_context *context, int argc, sqlite3_value **argv);
// Checks for orphan blocks. This will go away at a future date
bool CheckForOrphans(BlockIDs &blockids); bool CheckForOrphans(BlockIDs &blockids);
// Return a database connection if successful, which caller must close // Return a database connection if successful, which caller must close

View File

@ -599,10 +599,9 @@ bool ProjectFileManager::SaveCopy(const FilePath &fileName /* = wxT("") */)
if (!projectFileIO.SaveCopy(filename.GetFullPath())) if (!projectFileIO.SaveCopy(filename.GetFullPath()))
{ {
// Overwrite disallowed. The destination project is open in another window.
AudacityMessageDialog m( AudacityMessageDialog m(
nullptr, nullptr,
XO("The project will not saved because the selected project is open in another window.\nPlease try again and select an original name."), XO("The project was not saved. See Help->Diagnostics->Show Log for more info."),
XO("Error Saving Project"), XO("Error Saving Project"),
wxOK | wxICON_ERROR); wxOK | wxICON_ERROR);

View File

@ -705,7 +705,7 @@ bool AUPImportFileHandle::HandleProject(XMLTagHandler *&handler)
if (projName.empty()) if (projName.empty())
{ {
AudacityMessageBox( AudacityMessageBox(
XO("Couldn't find the project data folder: \"%s\"").Format(*value), XO("Couldn't find the project data folder: \"%s\"").Format(value),
XO("Error Opening Project"), XO("Error Opening Project"),
wxOK | wxCENTRE, wxOK | wxCENTRE,
&window); &window);