Audacity 3.2.0
ProjectFileIO.cpp
Go to the documentation of this file.
1/**********************************************************************
2
3Audacity: A Digital Audio Editor
4
5ProjectFileIO.cpp
6
7Paul Licameli split from AudacityProject.cpp
8
9**********************************************************************/
10
11#include "ProjectFileIO.h"
12
13#include <atomic>
14#include <sqlite3.h>
15#include <optional>
16#include <cstring>
17
18#include <wx/app.h>
19#include <wx/crt.h>
20#include <wx/frame.h>
21#include <wx/log.h>
22#include <wx/sstream.h>
23
24#include "ActiveProjects.h"
25#include "CodeConversions.h"
26#include "DBConnection.h"
27#include "Project.h"
28#include "ProjectHistory.h"
29#include "ProjectSerializer.h"
30#include "ProjectWindows.h"
31#include "SampleBlock.h"
32#include "TempDirectory.h"
33#include "TransactionScope.h"
34#include "WaveTrack.h"
36#include "BasicUI.h"
38#include "wxFileNameWrapper.h"
39#include "XMLFileReader.h"
40#include "SentryHelper.h"
41#include "MemoryX.h"
42
44
46#include "FromChars.h"
47
48// Don't change this unless the file format changes
49// in an irrevocable way
50#define AUDACITY_FILE_FORMAT_VERSION "1.3.0"
51
52#undef NO_SHM
53#if !defined(__WXMSW__)
54 #define NO_SHM
55#endif
56
57// Used to convert 4 byte-sized values into an integer for use in SQLite
58// PRAGMA statements. These values will be store in the database header.
59//
60// Note that endianness is not an issue here since SQLite integers are
61// architecture independent.
62#define PACK(b1, b2, b3, b4) ((b1 << 24) | (b2 << 16) | (b3 << 8) | b4)
63
64// The ProjectFileID is stored in the SQLite database header to identify the file
65// as an Audacity project file. It can be used by applications that identify file
66// types, such as the Linux "file" command.
67static const int ProjectFileID = PACK('A', 'U', 'D', 'Y');
68
69// The "ProjectFileVersion" represents the version of Audacity at which a specific
70// database schema was used. It is assumed that any changes to the database schema
71// will require a new Audacity version so if schema changes are required set this
72// to the new release being produced.
73//
74// This version is checked before accessing any tables in the database since there's
75// no guarantee what tables exist. If it's found that the database is newer than the
76// currently running Audacity, an error dialog will be displayed informing the user
77// that they need a newer version of Audacity.
78//
79// Note that this is NOT the "schema_version" that SQLite maintains. The value
80// specified here is stored in the "user_version" field of the SQLite database
81// header.
82// DV: ProjectFileVersion is now evaluated at runtime
83// static const int ProjectFileVersion = PACK(3, 0, 0, 0);
84
85// Navigation:
86//
87// Bindings are marked out in the code by, e.g.
88// BIND SQL sampleblocks
89// A search for "BIND SQL" will find all bindings.
90// A search for "SQL sampleblocks" will find all SQL related
91// to sampleblocks.
92
93static const char *ProjectFileSchema =
94 // These are persistent and not connection based
95 //
96 // See the CMakeList.txt for the SQLite lib for more
97 // settings.
98 "PRAGMA <schema>.application_id = %d;"
99 "PRAGMA <schema>.user_version = %u;"
100 ""
101 // project is a binary representation of an XML file.
102 // it's in binary for speed.
103 // One instance only. id is always 1.
104 // dict is a dictionary of fieldnames.
105 // doc is the binary representation of the XML
106 // in the doc, fieldnames are replaced by 2 byte dictionary
107 // index numbers.
108 // This is all opaque to SQLite. It just sees two
109 // big binary blobs.
110 // There is no limit to document blob size.
111 // dict will be smallish, with an entry for each
112 // kind of field.
113 "CREATE TABLE IF NOT EXISTS <schema>.project"
114 "("
115 " id INTEGER PRIMARY KEY,"
116 " dict BLOB,"
117 " doc BLOB"
118 ");"
119 ""
120 // CREATE SQL autosave
121 // autosave is a binary representation of an XML file.
122 // it's in binary for speed.
123 // One instance only. id is always 1.
124 // dict is a dictionary of fieldnames.
125 // doc is the binary representation of the XML
126 // in the doc, fieldnames are replaced by 2 byte dictionary
127 // index numbers.
128 // This is all opaque to SQLite. It just sees two
129 // big binary blobs.
130 // There is no limit to document blob size.
131 // dict will be smallish, with an entry for each
132 // kind of field.
133 "CREATE TABLE IF NOT EXISTS <schema>.autosave"
134 "("
135 " id INTEGER PRIMARY KEY,"
136 " dict BLOB,"
137 " doc BLOB"
138 ");"
139 ""
140 // CREATE SQL sampleblocks
141 // 'samples' are fixed size blocks of int16, int32 or float32 numbers.
142 // The blocks may be partially empty.
143 // The quantity of valid data in the blocks is
144 // provided in the project blob.
145 //
146 // sampleformat specifies the format of the samples stored.
147 //
148 // blockID is a 64 bit number.
149 //
150 // Rows are immutable -- never updated after addition, but may be
151 // deleted.
152 //
153 // summin to summary64K are summaries at 3 distance scales.
154 "CREATE TABLE IF NOT EXISTS <schema>.sampleblocks"
155 "("
156 " blockid INTEGER PRIMARY KEY AUTOINCREMENT,"
157 " sampleformat INTEGER,"
158 " summin REAL,"
159 " summax REAL,"
160 " sumrms REAL,"
161 " summary256 BLOB,"
162 " summary64k BLOB,"
163 " samples BLOB"
164 ");";
165
166// This singleton handles initialization/shutdown of the SQLite library.
167// It is needed because our local SQLite is built with SQLITE_OMIT_AUTOINIT
168// defined.
169//
170// It's safe to use even if a system version of SQLite is used that didn't
171// have SQLITE_OMIT_AUTOINIT defined.
173{
174public:
176 {
177 // Enable URI filenames for all connections
178 mRc = sqlite3_config(SQLITE_CONFIG_URI, 1);
179 if (mRc == SQLITE_OK)
180 {
181 mRc = sqlite3_config(SQLITE_CONFIG_LOG, LogCallback, nullptr);
182 if (mRc == SQLITE_OK)
183 {
184 mRc = sqlite3_initialize();
185 }
186 }
187
188#ifdef NO_SHM
189 if (mRc == SQLITE_OK)
190 {
191 // Use the "unix-excl" VFS to make access to the DB exclusive. This gets
192 // rid of the "<database name>-shm" shared memory file.
193 //
194 // Though it shouldn't, it doesn't matter if this fails.
195 auto vfs = sqlite3_vfs_find("unix-excl");
196 if (vfs)
197 {
198 sqlite3_vfs_register(vfs, 1);
199 }
200 }
201#endif
202 }
204 {
205 // This function must be called single-threaded only
206 // It returns a value, but there's nothing we can do with it
207 (void) sqlite3_shutdown();
208 }
209
210 static void LogCallback(void *WXUNUSED(arg), int code, const char *msg)
211 {
212 wxLogMessage("sqlite3 message: (%d) %s", code, msg);
213 }
214
215 int mRc;
216};
217
219{
220public:
221 static std::optional<SQLiteBlobStream> Open(
222 sqlite3* db, const char* schema, const char* table, const char* column,
223 int64_t rowID, bool readOnly) noexcept
224 {
225 if (db == nullptr)
226 return {};
227
228 sqlite3_blob* blob = nullptr;
229
230 const int rc = sqlite3_blob_open(
231 db, schema, table, column, rowID, readOnly ? 0 : 1, &blob);
232
233 if (rc != SQLITE_OK)
234 return {};
235
236 return std::make_optional<SQLiteBlobStream>(blob, readOnly);
237 }
238
239 SQLiteBlobStream(sqlite3_blob* blob, bool readOnly) noexcept
240 : mBlob(blob)
241 , mIsReadOnly(readOnly)
242 {
243 mBlobSize = sqlite3_blob_bytes(blob);
244 }
245
247 {
248 *this = std::move(rhs);
249 }
250
252 {
253 std::swap(mBlob, rhs.mBlob);
254 std::swap(mBlobSize, rhs.mBlobSize);
255 std::swap(mOffset, rhs.mOffset);
256 std::swap(mIsReadOnly, rhs.mIsReadOnly);
257
258 return *this;
259 }
260
262 {
263 // Destructor should not throw and there is no
264 // way to handle the error otherwise
265 (void) Close();
266 }
267
268 bool IsOpen() const noexcept
269 {
270 return mBlob != nullptr;
271 }
272
273 int Close() noexcept
274 {
275 if (mBlob == nullptr)
276 return SQLITE_OK;
277
278 const int rc = sqlite3_blob_close(mBlob);
279
280 mBlob = nullptr;
281
282 return rc;
283 }
284
285 int Write(const void* ptr, int size) noexcept
286 {
287 // Stream APIs usually return the number of bytes written.
288 // sqlite3_blob_write is all-or-nothing function,
289 // so Write will return the result of the call
290 if (!IsOpen() || mIsReadOnly || ptr == nullptr)
291 return SQLITE_MISUSE;
292
293 const int rc = sqlite3_blob_write(mBlob, ptr, size, mOffset);
294
295 if (rc == SQLITE_OK)
296 mOffset += size;
297
298 return rc;
299 }
300
301 int Read(void* ptr, int& size) noexcept
302 {
303 if (!IsOpen() || ptr == nullptr)
304 return SQLITE_MISUSE;
305
306 const int availableBytes = mBlobSize - mOffset;
307
308 if (availableBytes == 0)
309 {
310 size = 0;
311 return SQLITE_OK;
312 }
313 else if (availableBytes < size)
314 {
315 size = availableBytes;
316 }
317
318 const int rc = sqlite3_blob_read(mBlob, ptr, size, mOffset);
319
320 if (rc == SQLITE_OK)
321 mOffset += size;
322
323 return rc;
324 }
325
326 bool IsEof() const noexcept
327 {
328 return mOffset == mBlobSize;
329 }
330
331private:
332 sqlite3_blob* mBlob { nullptr };
333 size_t mBlobSize { 0 };
334
335 int mOffset { 0 };
336
337 bool mIsReadOnly { false };
338};
339
341{
342public:
343 static constexpr std::array<const char*, 2> Columns = { "dict", "doc" };
344
346 sqlite3* db, const char* schema, const char* table,
347 int64_t rowID)
348 // Despite we use 64k pages in SQLite - it is impossible to guarantee
349 // that read is satisfied from a single page.
350 // Reading 64k proved to be slower, (64k - 8) gives no measurable difference
351 // to reading 32k.
352 // Reading 4k is slower than reading 32k.
353 : BufferedStreamReader(32 * 1024)
354 , mDB(db)
355 , mSchema(schema)
356 , mTable(table)
357 , mRowID(rowID)
358 {
359 }
360
361private:
362 bool OpenBlob(size_t index)
363 {
364 if (index >= Columns.size())
365 {
366 mBlobStream.reset();
367 return false;
368 }
369
371 mDB, mSchema, mTable, Columns[index], mRowID, true);
372
373 return mBlobStream.has_value();
374 }
375
376 std::optional<SQLiteBlobStream> mBlobStream;
377 size_t mNextBlobIndex { 0 };
378
379 sqlite3* mDB;
380 const char* mSchema;
381 const char* mTable;
382 const int64_t mRowID;
383
384protected:
385 bool HasMoreData() const override
386 {
387 return mBlobStream.has_value() || mNextBlobIndex < Columns.size();
388 }
389
390 size_t ReadData(void* buffer, size_t maxBytes) override
391 {
392 if (!mBlobStream || mBlobStream->IsEof())
393 {
394 if (!OpenBlob(mNextBlobIndex++))
395 return {};
396 }
397
398 // Do not allow reading more then 2GB at a time (O_o)
399 maxBytes = std::min<size_t>(maxBytes, std::numeric_limits<int>::max());
400 auto bytesRead = static_cast<int>(maxBytes);
401
402 if (SQLITE_OK != mBlobStream->Read(buffer, bytesRead))
403 {
404 // Reading has failed, close the stream and do not allow opening
405 // the next one
406 mBlobStream = {};
407 mNextBlobIndex = Columns.size();
408
409 return 0;
410 }
411 else if (bytesRead == 0)
412 {
413 mBlobStream = {};
414 }
415
416 return static_cast<size_t>(bytesRead);
417 }
418};
419
420constexpr std::array<const char*, 2> BufferedProjectBlobStream::Columns;
421
423{
424 static SQLiteIniter sqliteIniter;
425 return sqliteIniter.mRc == SQLITE_OK;
426}
427
428static void RefreshAllTitles(bool bShowProjectNumbers )
429{
430 for ( auto pProject : AllProjects{} ) {
431 if ( !GetProjectFrame( *pProject ).IsIconized() ) {
433 bShowProjectNumbers ? pProject->GetProjectNumber() : -1 );
434 }
435 }
436}
437
439 wxTopLevelWindow &window, AudacityProject &project )
440{
441 if( window.IsIconized() )
442 window.Restore();
443 window.Raise(); // May help identifying the window on Mac
444
445 // Construct this project's name and number.
446 sProjName = project.GetProjectName();
447 if ( sProjName.empty() ) {
448 sProjName = _("<untitled>");
449 UnnamedCount = std::count_if(
451 []( const AllProjects::value_type &ptr ){
452 return ptr->GetProjectName().empty();
453 }
454 );
455 if ( UnnamedCount > 1 ) {
456 sProjNumber.Printf(
457 _("[Project %02i] "), project.GetProjectNumber() + 1 );
458 RefreshAllTitles( true );
459 }
460 }
461 else
462 UnnamedCount = 0;
463}
464
466 if( UnnamedCount > 1 )
467 RefreshAllTitles( false );
468}
469
471 []( AudacityProject &parent ){
472 auto result = std::make_shared< ProjectFileIO >( parent );
473 return result;
474 }
475};
476
478{
479 auto &result = project.AttachedObjects::Get< ProjectFileIO >( sFileIOKey );
480 return result;
481}
482
484{
485 return Get( const_cast< AudacityProject & >( project ) );
486}
487
489 : mProject{ project }
490 , mpErrors{ std::make_shared<DBConnectionErrors>() }
491{
492 mPrevConn = nullptr;
493
494 mRecovered = false;
495 mModified = false;
496 mTemporary = true;
497
498 UpdatePrefs();
499
500 // Make sure there is plenty of space for Sqlite files
501 wxLongLong freeSpace = 0;
502
503 auto path = TempDirectory::TempDir();
504 if (wxGetDiskSpace(path, NULL, &freeSpace)) {
505 if (freeSpace < wxLongLong(wxLL(100 * 1048576))) {
506 auto volume = FileNames::AbbreviatePath( path );
507 /* i18n-hint: %s will be replaced by the drive letter (on Windows) */
509 XO("Warning"),
510 XO("There is very little free disk space left on %s\n"
511 "Please select a bigger temporary directory location in\n"
512 "Directories Preferences.").Format( volume ),
513 "Error:_Disk_full_or_not_writable"
514 );
515 }
516 }
517}
518
520{
521}
522
524{
525 auto &connectionPtr = ConnectionPtr::Get( mProject );
526 return connectionPtr.mpConnection != nullptr;
527}
528
530{
531 auto &curConn = CurrConn();
532 if (!curConn)
533 {
534 if (!OpenConnection())
535 {
537 {
539 XO("Failed to open the project's database"),
540 XO("Warning"),
541 "Error:_Disk_full_or_not_writable"
542 };
543 }
544 }
545
546 return *curConn;
547}
548
550{
551 auto &trackList = TrackList::Get( mProject );
552
553 XMLStringWriter doc;
554 WriteXMLHeader(doc);
555 WriteXML(doc, false, trackList.empty() ? nullptr : &trackList);
556 return doc;
557}
558
560{
561 return GetConnection().DB();
562}
563
569{
570 auto &curConn = CurrConn();
571 wxASSERT(!curConn);
572 bool isTemp = false;
573
574 if (fileName.empty())
575 {
576 fileName = GetFileName();
577 if (fileName.empty())
578 {
580 isTemp = true;
581 }
582 }
583 else
584 {
585 // If this project resides in the temporary directory, then we'll mark it
586 // as temporary.
587 wxFileName temp(TempDirectory::TempDir(), wxT(""));
588 wxFileName file(fileName);
589 file.SetFullName(wxT(""));
590 if (file == temp)
591 {
592 isTemp = true;
593 }
594 }
595
596 // Pass weak_ptr to project into DBConnection constructor
597 curConn = std::make_unique<DBConnection>(
598 mProject.shared_from_this(), mpErrors, [this]{ OnCheckpointFailure(); } );
599 auto rc = curConn->Open(fileName);
600 if (rc != SQLITE_OK)
601 {
602 // Must use SetError() here since we do not have an active DB
603 SetError(
604 XO("Failed to open database file:\n\n%s").Format(fileName),
605 {},
606 rc
607 );
608 curConn.reset();
609 return false;
610 }
611
612 if (!CheckVersion())
613 {
615 curConn.reset();
616 return false;
617 }
618
619 mTemporary = isTemp;
620
621 SetFileName(fileName);
622
623 return true;
624}
625
627{
628 auto &curConn = CurrConn();
629 if (!curConn)
630 return false;
631
632 if (!curConn->Close())
633 {
634 return false;
635 }
636 curConn.reset();
637
638 SetFileName({});
639
640 return true;
641}
642
643// Put the current database connection aside, keeping it open, so that
644// another may be opened with OpenConnection()
646{
647 // Should do nothing in proper usage, but be sure not to leak a connection:
649
650 mPrevConn = std::move(CurrConn());
653
654 SetFileName({});
655}
656
657// Close any set-aside connection
659{
660 if (mPrevConn)
661 {
662 if (!mPrevConn->Close())
663 {
664 // Store an error message
666 XO("Failed to discard connection")
667 );
668 }
669
670 // If this is a temporary project, we no longer want to keep the
671 // project file.
672 if (mPrevTemporary)
673 {
674 // This is just a safety check.
675 wxFileName temp(TempDirectory::TempDir(), wxT(""));
676 wxFileName file(mPrevFileName);
677 file.SetFullName(wxT(""));
678 if (file == temp)
679 {
681 {
682 wxLogMessage("Failed to remove temporary project %s", mPrevFileName);
683 }
684 }
685 }
686 mPrevConn = nullptr;
687 mPrevFileName.clear();
688 }
689}
690
691// Close any current connection and switch back to using the saved
693{
694 auto &curConn = CurrConn();
695 if (curConn)
696 {
697 if (!curConn->Close())
698 {
699 // Store an error message
701 XO("Failed to restore connection")
702 );
703 }
704 }
705
706 curConn = std::move(mPrevConn);
709
710 mPrevFileName.clear();
711}
712
714{
715 auto &curConn = CurrConn();
716 wxASSERT(!curConn);
717
718 curConn = std::move(conn);
719 SetFileName(filePath);
720}
721
722static int ExecCallback(void *data, int cols, char **vals, char **names)
723{
724 auto &cb = *static_cast<const ProjectFileIO::ExecCB *>(data);
725 // Be careful not to throw anything across sqlite3's stack frames.
726 return GuardedCall<int>(
727 [&]{ return cb(cols, vals, names); },
728 MakeSimpleGuard( 1 )
729 );
730}
731
732int ProjectFileIO::Exec(const char *query, const ExecCB &callback, bool silent)
733{
734 char *errmsg = nullptr;
735
736 const void *ptr = &callback;
737 int rc = sqlite3_exec(DB(), query, ExecCallback,
738 const_cast<void*>(ptr), &errmsg);
739
740 if (rc != SQLITE_ABORT && errmsg && !silent)
741 {
742 ADD_EXCEPTION_CONTEXT("sqlite3.query", query);
743 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
744
746 XO("Failed to execute a project file command:\n\n%s").Format(query),
747 Verbatim(errmsg),
748 rc
749 );
750 }
751 if (errmsg)
752 {
753 sqlite3_free(errmsg);
754 }
755
756 return rc;
757}
758
759bool ProjectFileIO::Query(const char *sql, const ExecCB &callback, bool silent)
760{
761 int rc = Exec(sql, callback, silent);
762 // SQLITE_ABORT is a non-error return only meaning the callback
763 // stopped the iteration of rows early
764 if ( !(rc == SQLITE_OK || rc == SQLITE_ABORT) )
765 {
766 return false;
767 }
768
769 return true;
770}
771
772bool ProjectFileIO::GetValue(const char *sql, wxString &result, bool silent)
773{
774 // Retrieve the first column in the first row, if any
775 result.clear();
776 auto cb = [&result](int cols, char **vals, char **){
777 if (cols > 0)
778 result = vals[0];
779 // Stop after one row
780 return 1;
781 };
782
783 return Query(sql, cb, silent);
784}
785
786bool ProjectFileIO::GetValue(const char *sql, int64_t &value, bool silent)
787{
788 bool success = false;
789 auto cb = [&value, &success](int cols, char** vals, char**)
790 {
791 if (cols > 0)
792 {
793 const std::string_view valueString = vals[0];
794
795 success = std::errc() ==
796 FromChars(
797 valueString.data(), valueString.data() + valueString.length(),
798 value)
799 .ec;
800 }
801 // Stop after one row
802 return 1;
803 };
804
805 return Query(sql, cb, silent) && success;
806}
807
809{
810 auto db = DB();
811 int rc;
812
813 // Install our schema if this is an empty DB
814 wxString result;
815 if (!GetValue("SELECT Count(*) FROM sqlite_master WHERE type='table';", result))
816 {
817 // Bug 2718 workaround for a better error message:
818 // If at this point we get SQLITE_CANTOPEN, then the directory is read-only
819 if (GetLastErrorCode() == SQLITE_CANTOPEN)
820 {
821 SetError(
822 /* i18n-hint: An error message. */
823 XO("Project is in a read only directory\n(Unable to create the required temporary files)"),
825 );
826 }
827
828 return false;
829 }
830
831 // If the return count is zero, then there are no tables defined, so this
832 // must be a new project file.
833 if (wxStrtol<char **>(result, nullptr, 10) == 0)
834 {
835 return InstallSchema(db);
836 }
837
838 // Check for our application ID
839 if (!GetValue("PRAGMA application_ID;", result))
840 {
841 return false;
842 }
843
844 // It's a database that SQLite recognizes, but it's not one of ours
845 if (wxStrtoul<char **>(result, nullptr, 10) != ProjectFileID)
846 {
847 SetError(XO("This is not an Audacity project file"));
848 return false;
849 }
850
851 // Get the project file version
852 if (!GetValue("PRAGMA user_version;", result))
853 {
854 return false;
855 }
856
857 const ProjectFormatVersion version =
858 ProjectFormatVersion::FromPacked(wxStrtoul<char**>(result, nullptr, 10));
859
860 // Project file version is higher than ours. We will refuse to
861 // process it since we can't trust anything about it.
862 if (SupportedProjectFormatVersion < version)
863 {
864 SetError(
865 XO("This project was created with a newer version of Audacity.\n\nYou will need to upgrade to open it.")
866 );
867 return false;
868 }
869
870 return true;
871}
872
873bool ProjectFileIO::InstallSchema(sqlite3 *db, const char *schema /* = "main" */)
874{
875 int rc;
876
877 wxString sql;
879 sql.Replace("<schema>", schema);
880
881 rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
882 if (rc != SQLITE_OK)
883 {
885 XO("Unable to initialize the project file")
886 );
887 return false;
888 }
889
890 return true;
891}
892
893// The orphan block handling should be removed once autosave and related
894// blocks become part of the same transaction.
895
896// An SQLite function that takes a blockid and looks it up in a set of
897// blockids captured during project load. If the blockid isn't found
898// in the set, it will be deleted.
899void ProjectFileIO::InSet(sqlite3_context *context, int argc, sqlite3_value **argv)
900{
901 BlockIDs *blockids = (BlockIDs *) sqlite3_user_data(context);
902 SampleBlockID blockid = sqlite3_value_int64(argv[0]);
903
904 sqlite3_result_int(context, blockids->find(blockid) != blockids->end());
905}
906
907bool ProjectFileIO::DeleteBlocks(const BlockIDs &blockids, bool complement)
908{
909 auto db = DB();
910 int rc;
911
912 auto cleanup = finally([&]
913 {
914 // Remove our function, whether it was successfully defined or not.
915 sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr, nullptr, nullptr, nullptr);
916 });
917
918 // Add the function used to verify each row's blockid against the set of active blockids
919 const void *p = &blockids;
920 rc = sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, const_cast<void*>(p), InSet, nullptr, nullptr);
921 if (rc != SQLITE_OK)
922 {
923 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
924 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::DeleteBlocks::create_function");
925
926 /* i18n-hint: An error message. Don't translate inset or blockids.*/
927 SetDBError(XO("Unable to add 'inset' function (can't verify blockids)"));
928 return false;
929 }
930
931 // Delete all rows in the set, or not in it
932 // This is the first command that writes to the database, and so we
933 // do more informative error reporting than usual, if it fails.
934 auto sql = wxString::Format(
935 "DELETE FROM sampleblocks WHERE %sinset(blockid);",
936 complement ? "NOT " : "" );
937 rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
938 if (rc != SQLITE_OK)
939 {
940 ADD_EXCEPTION_CONTEXT("sqlite3.query", sql.ToStdString());
941 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
942 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::GetBlob");
943
944 if( rc==SQLITE_READONLY)
945 /* i18n-hint: An error message. Don't translate blockfiles.*/
946 SetDBError(XO("Project is read only\n(Unable to work with the blockfiles)"));
947 else if( rc==SQLITE_LOCKED)
948 /* i18n-hint: An error message. Don't translate blockfiles.*/
949 SetDBError(XO("Project is locked\n(Unable to work with the blockfiles)"));
950 else if( rc==SQLITE_BUSY)
951 /* i18n-hint: An error message. Don't translate blockfiles.*/
952 SetDBError(XO("Project is busy\n(Unable to work with the blockfiles)"));
953 else if( rc==SQLITE_CORRUPT)
954 /* i18n-hint: An error message. Don't translate blockfiles.*/
955 SetDBError(XO("Project is corrupt\n(Unable to work with the blockfiles)"));
956 else if( rc==SQLITE_PERM)
957 /* i18n-hint: An error message. Don't translate blockfiles.*/
958 SetDBError(XO("Some permissions issue\n(Unable to work with the blockfiles)"));
959 else if( rc==SQLITE_IOERR)
960 /* i18n-hint: An error message. Don't translate blockfiles.*/
961 SetDBError(XO("A disk I/O error\n(Unable to work with the blockfiles)"));
962 else if( rc==SQLITE_AUTH)
963 /* i18n-hint: An error message. Don't translate blockfiles.*/
964 SetDBError(XO("Not authorized\n(Unable to work with the blockfiles)"));
965 else
966 /* i18n-hint: An error message. Don't translate blockfiles.*/
967 SetDBError(XO("Unable to work with the blockfiles"));
968
969 return false;
970 }
971
972 // Mark the project recovered if we deleted any rows
973 int changes = sqlite3_changes(db);
974 if (changes > 0)
975 {
976 wxLogInfo(XO("Total orphan blocks deleted %d").Translation(), changes);
977 mRecovered = true;
978 }
979
980 return true;
981}
982
983bool ProjectFileIO::CopyTo(const FilePath &destpath,
984 const TranslatableString &msg,
985 bool isTemporary,
986 bool prune /* = false */,
987 const std::vector<const TrackList *> &tracks /* = {} */)
988{
989 auto pConn = CurrConn().get();
990 if (!pConn)
991 return false;
992
993 // Get access to the active tracklist
994 auto pProject = &mProject;
995
996 SampleBlockIDSet blockids;
997
998 // Collect all active blockids
999 if (prune)
1000 {
1001 for (auto trackList : tracks)
1002 if (trackList)
1003 InspectBlocks( *trackList, {}, &blockids );
1004 }
1005 // Collect ALL blockids
1006 else
1007 {
1008 auto cb = [&blockids](int cols, char **vals, char **){
1009 SampleBlockID blockid;
1010 wxString{ vals[0] }.ToLongLong(&blockid);
1011 blockids.insert(blockid);
1012 return 0;
1013 };
1014
1015 if (!Query("SELECT blockid FROM sampleblocks;", cb))
1016 {
1017 // Error message already captured.
1018 return false;
1019 }
1020 }
1021
1022 // Create the project doc
1024 WriteXMLHeader(doc);
1025 WriteXML(doc, false, tracks.empty() ? nullptr : tracks[0]);
1026
1027 auto db = DB();
1028 Connection destConn = nullptr;
1029 bool success = false;
1030 int rc = SQLITE_OK;
1032
1033 // Cleanup in case things go awry
1034 auto cleanup = finally([&]
1035 {
1036 if (!success)
1037 {
1038 if (destConn)
1039 {
1040 destConn->Close();
1041 destConn = nullptr;
1042 }
1043
1044 // Rollback transaction in case one was active.
1045 // If this fails (probably due to memory or disk space), the transaction will
1046 // (presumably) still be active, so further updates to the project file will
1047 // fail as well. Not really much we can do about it except tell the user.
1048 auto result = sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
1049
1050 // Only capture the error if there wasn't a previous error
1051 if (result != SQLITE_OK && (rc == SQLITE_DONE || rc == SQLITE_OK))
1052 {
1053 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1055 "sqlite3.context", "ProjectGileIO::CopyTo.cleanup");
1056
1057 SetDBError(
1058 XO("Failed to rollback transaction during import")
1059 );
1060 }
1061
1062 // And detach the outbound DB in case (if it's attached). Don't check for
1063 // errors since it may not be attached. But, if it is and the DETACH fails,
1064 // subsequent CopyTo() actions will fail until Audacity is relaunched.
1065 sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr);
1066
1067 // RemoveProject not necessary to clean up attached database
1068 wxRemoveFile(destpath);
1069 }
1070 });
1071
1072 // Attach the destination database
1073 wxString sql;
1074 wxString dbName = destpath;
1075 // Bug 2793: Quotes in name need escaping for sqlite3.
1076 dbName.Replace( "'", "''");
1077 sql.Printf("ATTACH DATABASE '%s' AS outbound;", dbName.ToUTF8());
1078
1079 rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
1080 if (rc != SQLITE_OK)
1081 {
1082 SetDBError(
1083 XO("Unable to attach destination database")
1084 );
1085 return false;
1086 }
1087
1088 // Ensure attached DB connection gets configured
1089 //
1090 // NOTE: Between the above attach and setting the mode here, a normal DELETE
1091 // mode journal will be used and will briefly appear in the filesystem.
1092 if ( pConn->FastMode("outbound") != SQLITE_OK)
1093 {
1094 SetDBError(
1095 XO("Unable to switch to fast journaling mode")
1096 );
1097
1098 return false;
1099 }
1100
1101 // Install our schema into the new database
1102 if (!InstallSchema(db, "outbound"))
1103 {
1104 // Message already set
1105 return false;
1106 }
1107
1108 {
1109 // Ensure statement gets cleaned up
1110 sqlite3_stmt *stmt = nullptr;
1111 auto cleanup = finally([&]
1112 {
1113 if (stmt)
1114 {
1115 // No need to check return code
1116 sqlite3_finalize(stmt);
1117 }
1118 });
1119
1120 // Prepare the statement only once
1121 rc = sqlite3_prepare_v2(db,
1122 "INSERT INTO outbound.sampleblocks"
1123 " SELECT * FROM main.sampleblocks"
1124 " WHERE blockid = ?;",
1125 -1,
1126 &stmt,
1127 nullptr);
1128 if (rc != SQLITE_OK)
1129 {
1130 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1132 "sqlite3.context", "ProjectGileIO::CopyTo.prepare");
1133
1134 SetDBError(
1135 XO("Unable to prepare project file command:\n\n%s").Format(sql)
1136 );
1137 return false;
1138 }
1139
1140 /* i18n-hint: This title appears on a dialog that indicates the progress
1141 in doing something.*/
1142 ProgressDialog progress(XO("Progress"), msg, pdlgHideStopButton);
1144
1145 wxLongLong_t count = 0;
1146 wxLongLong_t total = blockids.size();
1147
1148 // Start a transaction. Since we're running without a journal,
1149 // this really doesn't provide rollback. It just prevents SQLite
1150 // from auto committing after each step through the loop.
1151 //
1152 // Also note that we will have an open transaction if we fail
1153 // while copying the blocks. This is fine since we're just going
1154 // to delete the database anyway.
1155 sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr);
1156
1157 // Copy sample blocks from the main DB to the outbound DB
1158 for (auto blockid : blockids)
1159 {
1160 // Bind statement parameters
1161 rc = sqlite3_bind_int64(stmt, 1, blockid);
1162 if (rc != SQLITE_OK)
1163 {
1164 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1166 "sqlite3.context", "ProjectGileIO::CopyTo.bind");
1167
1168 SetDBError(
1169 XO("Failed to bind SQL parameter")
1170 );
1171
1172 return false;
1173 }
1174
1175 // Process it
1176 rc = sqlite3_step(stmt);
1177 if (rc != SQLITE_DONE)
1178 {
1179 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1181 "sqlite3.context", "ProjectGileIO::CopyTo.step");
1182
1183 SetDBError(
1184 XO("Failed to update the project file.\nThe following command failed:\n\n%s").Format(sql)
1185 );
1186 return false;
1187 }
1188
1189 // Reset statement to beginning
1190 if (sqlite3_reset(stmt) != SQLITE_OK)
1191 {
1192 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1194 "sqlite3.context", "ProjectGileIO::CopyTo.reset");
1195
1197 }
1198
1199 result = progress.Update(++count, total);
1200 if (result != ProgressResult::Success)
1201 {
1202 // Note that we're not setting success, so the finally
1203 // block above will take care of cleaning up
1204 return false;
1205 }
1206 }
1207
1208 // Write the doc.
1209 //
1210 // If we're compacting a temporary project (user initiated from the File
1211 // menu), then write the doc to the "autosave" table since temporary
1212 // projects do not have a "project" doc.
1213 if (!WriteDoc(isTemporary ? "autosave" : "project", doc, "outbound"))
1214 {
1215 return false;
1216 }
1217
1218 // See BEGIN above...
1219 sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
1220 }
1221
1222 // Detach the destination database
1223 rc = sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr);
1224 if (rc != SQLITE_OK)
1225 {
1226 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1227 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::CopyTo::detach");
1228
1229 SetDBError(
1230 XO("Destination project could not be detached")
1231 );
1232
1233 return false;
1234 }
1235
1236 // Tell cleanup everything is good to go
1237 success = true;
1238
1239 return true;
1240}
1241
1242bool ProjectFileIO::ShouldCompact(const std::vector<const TrackList *> &tracks)
1243{
1244 SampleBlockIDSet active;
1245 unsigned long long current = 0;
1246
1247 {
1248 auto fn = BlockSpaceUsageAccumulator( current );
1249 for (auto pTracks : tracks)
1250 if (pTracks)
1251 InspectBlocks( *pTracks, fn,
1252 &active // Visit unique blocks only
1253 );
1254 }
1255
1256 // Get the number of blocks and total length from the project file.
1257 unsigned long long total = GetTotalUsage();
1258 unsigned long long blockcount = 0;
1259
1260 auto cb = [&blockcount](int cols, char **vals, char **)
1261 {
1262 // Convert
1263 wxString(vals[0]).ToULongLong(&blockcount);
1264 return 0;
1265 };
1266
1267 if (!Query("SELECT Count(*) FROM sampleblocks;", cb) || blockcount == 0)
1268 {
1269 // Shouldn't compact since we don't have the full picture
1270 return false;
1271 }
1272
1273 // Remember if we had unused blocks in the project file
1274 mHadUnused = (blockcount > active.size());
1275
1276 // Let's make a percentage...should be plenty of head room
1277 current *= 100;
1278
1279 wxLogDebug(wxT("used = %lld total = %lld %lld"), current, total, total ? current / total : 0);
1280 if (!total || current / total > 80)
1281 {
1282 wxLogDebug(wxT("not compacting"));
1283 return false;
1284 }
1285 wxLogDebug(wxT("compacting"));
1286
1287 return true;
1288}
1289
1291{
1292 auto &connectionPtr = ConnectionPtr::Get( mProject );
1293 return connectionPtr.mpConnection;
1294}
1295
1296const std::vector<wxString> &ProjectFileIO::AuxiliaryFileSuffixes()
1297{
1298 static const std::vector<wxString> strings {
1299 "-wal",
1300#ifndef NO_SHM
1301 "-shm",
1302#endif
1303 };
1304 return strings;
1305}
1306
1308{
1309 wxFileNameWrapper fn{ src };
1310
1311 // Extra characters inserted into filename before extension
1312 wxString extra =
1313#ifdef __WXGTK__
1314 wxT("~")
1315#else
1316 wxT(".bak")
1317#endif
1318 ;
1319
1320 int nn = 1;
1321 auto numberString = [](int num) -> wxString {
1322 return num == 1 ? wxString{} : wxString::Format(".%d", num);
1323 };
1324
1325 auto suffixes = AuxiliaryFileSuffixes();
1326 suffixes.push_back({});
1327
1328 // Find backup paths not already occupied; check all auxiliary suffixes
1329 const auto name = fn.GetName();
1330 FilePath result;
1331 do {
1332 fn.SetName( name + numberString(nn++) + extra );
1333 result = fn.GetFullPath();
1334 }
1335 while( std::any_of(suffixes.begin(), suffixes.end(), [&](auto &suffix){
1336 return wxFileExists(result + suffix);
1337 }) );
1338
1339 return result;
1340}
1341
1343{
1344 std::atomic_bool done = {false};
1345 bool success = false;
1346 auto thread = std::thread([&]
1347 {
1348 success = wxRenameFile(src, dst);
1349 done = true;
1350 });
1351
1352 // Provides a progress dialog with indeterminate mode
1353 using namespace BasicUI;
1355 XO("Copying Project"), XO("This may take several seconds"));
1356 wxASSERT(pd);
1357
1358 // Wait for the checkpoints to end
1359 while (!done)
1360 {
1361 using namespace std::chrono;
1362 std::this_thread::sleep_for(50ms);
1363 pd->Pulse();
1364 }
1365 thread.join();
1366
1367 if (!success)
1368 {
1370 XO("Error Writing to File"),
1371 XO("Audacity failed to write file %s.\n"
1372 "Perhaps disk is full or not writable.\n"
1373 "For tips on freeing up space, click the help button.")
1374 .Format(dst),
1375 "Error:_Disk_full_or_not_writable"
1376 );
1377 return false;
1378 }
1379
1380 return true;
1381}
1382
1384{
1385 // Assume the src database file is not busy.
1386 if (!RenameOrWarn(src, dst))
1387 return false;
1388
1389 // So far so good, but the separate -wal and -shm files might yet exist,
1390 // as when checkpointing failed for limited space on the drive.
1391 // If so move them too or else lose data.
1392
1393 std::vector< std::pair<FilePath, FilePath> > pairs{ { src, dst } };
1394 bool success = false;
1395 auto cleanup = finally([&]{
1396 if (!success) {
1397 // If any one of the renames failed, back out the previous ones.
1398 // This should be a no-fail recovery! Not clear what to do if any
1399 // of these renames fails.
1400 for (auto &pair : pairs) {
1401 if (!(pair.first.empty() && pair.second.empty()))
1402 wxRenameFile(pair.second, pair.first);
1403 }
1404 }
1405 });
1406
1407 for (const auto &suffix : AuxiliaryFileSuffixes()) {
1408 auto srcName = src + suffix;
1409 if (wxFileExists(srcName)) {
1410 auto dstName = dst + suffix;
1411 if (!RenameOrWarn(srcName, dstName))
1412 return false;
1413 pairs.push_back({ srcName, dstName });
1414 }
1415 }
1416
1417 return (success = true);
1418}
1419
1421{
1422 if (!wxFileExists(filename))
1423 return false;
1424
1425 bool success = wxRemoveFile(filename);
1426 auto &suffixes = AuxiliaryFileSuffixes();
1427 for (const auto &suffix : suffixes) {
1428 auto file = filename + suffix;
1429 if (wxFileExists(file))
1430 success = wxRemoveFile(file) && success;
1431 }
1432 return success;
1433}
1434
1436 ProjectFileIO &projectFileIO, const FilePath &path )
1437{
1438 auto safety = SafetyFileName(path);
1439 if (!projectFileIO.MoveProject(path, safety))
1440 return;
1441
1442 mPath = path;
1443 mSafety = safety;
1444}
1445
1447{
1448 if (!mPath.empty()) {
1449 // Succeeded; don't need the safety files
1450 RemoveProject(mSafety);
1451 mSafety.clear();
1452 }
1453}
1454
1456{
1457 if (!mPath.empty()) {
1458 if (!mSafety.empty()) {
1459 // Failed; restore from safety files
1460 auto suffixes = AuxiliaryFileSuffixes();
1461 suffixes.push_back({});
1462 for (const auto &suffix : suffixes) {
1463 auto path = mPath + suffix;
1464 if (wxFileExists(path))
1465 wxRemoveFile(path);
1466 wxRenameFile(mSafety + suffix, mPath + suffix);
1467 }
1468 }
1469 }
1470}
1471
1473 const std::vector<const TrackList *> &tracks, bool force)
1474{
1475 // Haven't compacted yet
1476 mWasCompacted = false;
1477
1478 // Assume we have unused blocks until we find out otherwise. That way cleanup
1479 // at project close time will still occur.
1480 mHadUnused = true;
1481
1482 // If forcing compaction, bypass inspection.
1483 if (!force)
1484 {
1485 // Don't compact if this is a temporary project or if it's determined there are not
1486 // enough unused blocks to make it worthwhile.
1487 if (IsTemporary() || !ShouldCompact(tracks))
1488 {
1489 // Delete the AutoSave doc it if exists
1490 if (IsModified())
1491 {
1492 // PRL: not clear what to do if the following fails, but the worst should
1493 // be, the project may reopen in its present state as a recovery file, not
1494 // at the last saved state.
1495 // REVIEW: Could the autosave file be corrupt though at that point, and so
1496 // prevent recovery?
1497 // LLL: I believe Paul is correct since it's deleted with a single SQLite
1498 // transaction. The next time the file opens will just invoke recovery.
1499 (void) AutoSaveDelete();
1500 }
1501
1502 return;
1503 }
1504 }
1505
1506 wxString origName = mFileName;
1507 wxString backName = origName + "_compact_back";
1508 wxString tempName = origName + "_compact_temp";
1509
1510 // Copy the original database to a new database. Only prune sample blocks if
1511 // we have a tracklist.
1512 // REVIEW: Compact can fail on the CopyTo with no error messages. That's OK?
1513 // LLL: We could display an error message or just ignore the failure and allow
1514 // the file to be compacted the next time it's saved.
1515 if (CopyTo(tempName, XO("Compacting project"), IsTemporary(), !tracks.empty(), tracks))
1516 {
1517 // Must close the database to rename it
1518 if (CloseConnection())
1519 {
1520 // Only use the new file if it is actually smaller than the original.
1521 //
1522 // If the original file doesn't have anything to compact (original and new
1523 // are basically identical), the file could grow by a few pages because of
1524 // differences in how SQLite constructs the b-tree.
1525 //
1526 // In this case, just toss the new file and continue to use the original.
1527 //
1528 // Also, do this after closing the connection so that the -wal file
1529 // gets cleaned up.
1530 if (wxFileName::GetSize(tempName) < wxFileName::GetSize(origName))
1531 {
1532 // Rename the original to backup
1533 if (wxRenameFile(origName, backName))
1534 {
1535 // Rename the temporary to original
1536 if (wxRenameFile(tempName, origName))
1537 {
1538 // Open the newly compacted original file
1539 if (OpenConnection(origName))
1540 {
1541 // Remove the old original file
1542 if (!wxRemoveFile(backName))
1543 {
1544 // Just log the error, nothing can be done to correct it
1545 // and WX should have logged another message showing the
1546 // system error code.
1547 wxLogWarning(wxT("Compaction failed to delete backup %s"), backName);
1548 }
1549
1550 // Remember that we compacted
1551 mWasCompacted = true;
1552
1553 return;
1554 }
1555 else
1556 {
1557 wxLogWarning(wxT("Compaction failed to open new project %s"), origName);
1558 }
1559
1560 if (!wxRenameFile(origName, tempName))
1561 {
1562 wxLogWarning(wxT("Compaction failed to rename original %s to temp %s"),
1563 origName, tempName);
1564 }
1565 }
1566 else
1567 {
1568 wxLogWarning(wxT("Compaction failed to rename temp %s to orig %s"),
1569 origName, tempName);
1570 }
1571
1572 if (!wxRenameFile(backName, origName))
1573 {
1574 wxLogWarning(wxT("Compaction failed to rename back %s to orig %s"),
1575 backName, origName);
1576 }
1577 }
1578 else
1579 {
1580 wxLogWarning(wxT("Compaction failed to rename orig %s to back %s"),
1581 backName, origName);
1582 }
1583 }
1584
1585 if (!OpenConnection(origName))
1586 {
1587 wxLogWarning(wxT("Compaction failed to reopen %s"), origName);
1588 }
1589 }
1590
1591 // Did not achieve any real compaction
1592 // RemoveProject not needed for what was an attached database
1593 if (!wxRemoveFile(tempName))
1594 {
1595 // Just log the error, nothing can be done to correct it
1596 // and WX should have logged another message showing the
1597 // system error code.
1598 wxLogWarning(wxT("Failed to delete temporary file...ignoring"));
1599 }
1600 }
1601
1602 return;
1603}
1604
1606{
1607 return mWasCompacted;
1608}
1609
1611{
1612 return mHadUnused;
1613}
1614
1616{
1618}
1619
1620// Pass a number in to show project number, or -1 not to.
1622{
1623 auto &project = mProject;
1624 auto pWindow = FindProjectFrame(&project);
1625 if (!pWindow)
1626 {
1627 return;
1628 }
1629 auto &window = *pWindow;
1630 wxString name = project.GetProjectName();
1631
1632 // If we are showing project numbers, then we also explicitly show "<untitled>" if there
1633 // is none.
1634 if (number >= 0)
1635 {
1636 name =
1637 /* i18n-hint: The %02i is the project number, the %s is the project name.*/
1638 XO("[Project %02i] Audacity \"%s\"")
1639 .Format( number + 1,
1640 name.empty() ? XO("<untitled>") : Verbatim((const char *)name))
1641 .Translation();
1642 }
1643 // If we are not showing numbers, then <untitled> shows as 'Audacity'.
1644 else if (name.empty())
1645 {
1646 name = _TS("Audacity");
1647 }
1648
1649 if (mRecovered)
1650 {
1651 name += wxT(" ");
1652 /* i18n-hint: E.g this is recovered audio that had been lost.*/
1653 name += _("(Recovered)");
1654 }
1655
1656 if (name != window.GetTitle())
1657 {
1658 window.SetTitle( name );
1659 window.SetName(name); // to make the nvda screen reader read the correct title
1660
1661 BasicUI::CallAfter( [wThis = weak_from_this()]{
1662 if (auto pThis = wThis.lock())
1664 } );
1665 }
1666}
1667
1669{
1670 return mFileName;
1671}
1672
1674{
1675 auto &project = mProject;
1676
1677 if (!mFileName.empty())
1678 {
1680 }
1681
1682 mFileName = fileName;
1683
1684 if (!mFileName.empty())
1685 {
1687 }
1688
1689 if (IsTemporary())
1690 {
1691 project.SetProjectName({});
1692 }
1693 else
1694 {
1695 project.SetProjectName(wxFileName(mFileName).GetName());
1696 }
1697
1699}
1700
1701bool ProjectFileIO::HandleXMLTag(const std::string_view& tag, const AttributesList &attrs)
1702{
1703 auto &project = mProject;
1704
1705 wxString fileVersion;
1706 wxString audacityVersion;
1707 int requiredTags = 0;
1708
1709 // loop through attrs, which is a null-terminated list of
1710 // attribute-value pairs
1711 for (auto pair : attrs)
1712 {
1713 auto attr = pair.first;
1714 auto value = pair.second;
1715
1717 .CallAttributeHandler( attr, project, value ) )
1718 continue;
1719
1720 else if (attr == "version")
1721 {
1722 fileVersion = value.ToWString();
1723 requiredTags++;
1724 }
1725
1726 else if (attr == "audacityversion")
1727 {
1728 audacityVersion = value.ToWString();
1729 requiredTags++;
1730 }
1731 } // while
1732
1733 if (requiredTags < 2)
1734 {
1735 return false;
1736 }
1737
1738 // Parse the file version from the project
1739 int fver;
1740 int frel;
1741 int frev;
1742 if (!wxSscanf(fileVersion, wxT("%i.%i.%i"), &fver, &frel, &frev))
1743 {
1744 return false;
1745 }
1746
1747 // Parse the file version Audacity was build with
1748 int cver;
1749 int crel;
1750 int crev;
1751 wxSscanf(wxT(AUDACITY_FILE_FORMAT_VERSION), wxT("%i.%i.%i"), &cver, &crel, &crev);
1752
1753 int fileVer = ((fver *100)+frel)*100+frev;
1754 int codeVer = ((cver *100)+crel)*100+crev;
1755
1756 if (codeVer<fileVer)
1757 {
1758 /* i18n-hint: %s will be replaced by the version number.*/
1759 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.")
1760 .Format(audacityVersion, AUDACITY_VERSION_STRING);
1761
1762 ShowError( *ProjectFramePlacement(&project),
1763 XO("Can't open project file"),
1764 msg,
1765 "FAQ:Errors_opening_an_Audacity_project"
1766 );
1767
1768 return false;
1769 }
1770
1771 if (tag != "project")
1772 {
1773 return false;
1774 }
1775
1776 // All other tests passed, so we succeed
1777 return true;
1778}
1779
1781{
1782 auto &project = mProject;
1783 return ProjectFileIORegistry::Get().CallObjectAccessor(tag, project);
1784}
1785
1787{
1788 // DBConnection promises to invoke this in main thread idle time
1789 // So we don't need a redundant CallAfter to satisfy our own promise
1791}
1792
1794{
1795 xmlFile.Write(wxT("<?xml "));
1796 xmlFile.Write(wxT("version=\"1.0\" "));
1797 xmlFile.Write(wxT("standalone=\"no\" "));
1798 xmlFile.Write(wxT("?>\n"));
1799
1800 xmlFile.Write(wxT("<!DOCTYPE "));
1801 xmlFile.Write(wxT("project "));
1802 xmlFile.Write(wxT("PUBLIC "));
1803 xmlFile.Write(wxT("\"-//audacityproject-1.3.0//DTD//EN\" "));
1804 xmlFile.Write(wxT("\"http://audacity.sourceforge.net/xml/audacityproject-1.3.0.dtd\" "));
1805 xmlFile.Write(wxT(">\n"));
1806}
1807
1809 bool recording /* = false */,
1810 const TrackList *tracks /* = nullptr */)
1811// may throw
1812{
1813 auto &proj = mProject;
1814 auto &tracklist = tracks ? *tracks : TrackList::Get(proj);
1815
1816 //TIMER_START( "AudacityProject::WriteXML", xml_writer_timer );
1817
1818 xmlFile.StartTag(wxT("project"));
1819 xmlFile.WriteAttr(wxT("xmlns"), wxT("http://audacity.sourceforge.net/xml/"));
1820
1821 xmlFile.WriteAttr(wxT("version"), wxT(AUDACITY_FILE_FORMAT_VERSION));
1822 xmlFile.WriteAttr(wxT("audacityversion"), AUDACITY_VERSION_STRING);
1823
1824 ProjectFileIORegistry::Get().CallWriters(proj, xmlFile);
1825
1826 tracklist.Any().Visit([&](const Track *t)
1827 {
1828 auto useTrack = t;
1829 if ( recording ) {
1830 // When append-recording, there is a temporary "shadow" track accumulating
1831 // changes and displayed on the screen but it is not yet part of the
1832 // regular track list. That is the one that we want to back up.
1833 // SubstitutePendingChangedTrack() fetches the shadow, if the track has
1834 // one, else it gives the same track back.
1835 useTrack = t->SubstitutePendingChangedTrack().get();
1836 }
1837 else if ( useTrack->GetId() == TrackId{} ) {
1838 // This is a track added during a non-appending recording that is
1839 // not yet in the undo history. The UndoManager skips backing it up
1840 // when pushing. Don't auto-save it.
1841 return;
1842 }
1843 useTrack->WriteXML(xmlFile);
1844 });
1845
1846 xmlFile.EndTag(wxT("project"));
1847
1848 //TIMER_STOP( xml_writer_timer );
1849}
1850
1851bool ProjectFileIO::AutoSave(bool recording)
1852{
1853 ProjectSerializer autosave;
1854 WriteXMLHeader(autosave);
1855 WriteXML(autosave, recording);
1856
1857 if (WriteDoc("autosave", autosave))
1858 {
1859 mModified = true;
1860 return true;
1861 }
1862
1863 return false;
1864}
1865
1866bool ProjectFileIO::AutoSaveDelete(sqlite3 *db /* = nullptr */)
1867{
1868 int rc;
1869
1870 if (!db)
1871 {
1872 db = DB();
1873 }
1874
1875 rc = sqlite3_exec(db, "DELETE FROM autosave;", nullptr, nullptr, nullptr);
1876 if (rc != SQLITE_OK)
1877 {
1878 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1879 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::AutoSaveDelete");
1880
1881 SetDBError(
1882 XO("Failed to remove the autosave information from the project file.")
1883 );
1884 return false;
1885 }
1886
1887 mModified = false;
1888
1889 return true;
1890}
1891
1892bool ProjectFileIO::WriteDoc(const char *table,
1893 const ProjectSerializer &autosave,
1894 const char *schema /* = "main" */)
1895{
1896 auto db = DB();
1897
1898 TransactionScope transaction(mProject, "UpdateProject");
1899
1900 int rc;
1901
1902 // For now, we always use an ID of 1. This will replace the previously
1903 // written row every time.
1904 char sql[256];
1905 sqlite3_snprintf(
1906 sizeof(sql), sql,
1907 "INSERT INTO %s.%s(id, dict, doc) VALUES(1, ?1, ?2)"
1908 " ON CONFLICT(id) DO UPDATE SET dict = ?1, doc = ?2;",
1909 schema, table);
1910
1911 sqlite3_stmt *stmt = nullptr;
1912 auto cleanup = finally([&]
1913 {
1914 if (stmt)
1915 {
1916 sqlite3_finalize(stmt);
1917 }
1918 });
1919
1920 rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
1921 if (rc != SQLITE_OK)
1922 {
1923 ADD_EXCEPTION_CONTEXT("sqlite3.query", sql);
1924 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1925 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::WriteDoc::prepare");
1926
1927 SetDBError(
1928 XO("Unable to prepare project file command:\n\n%s").Format(sql)
1929 );
1930 return false;
1931 }
1932
1933 const MemoryStream& dict = autosave.GetDict();
1934 const MemoryStream& data = autosave.GetData();
1935
1936 // Bind statement parameters
1937 // Might return SQL_MISUSE which means it's our mistake that we violated
1938 // preconditions; should return SQL_OK which is 0
1939 if (
1940 sqlite3_bind_zeroblob(stmt, 1, dict.GetSize()) ||
1941 sqlite3_bind_zeroblob(stmt, 2, data.GetSize()))
1942 {
1943 ADD_EXCEPTION_CONTEXT("sqlite3.query", sql);
1944 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1945 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::WriteDoc::bind");
1946
1947 SetDBError(XO("Unable to bind to blob"));
1948 return false;
1949 }
1950
1951 const auto reportError = [this](auto sql) {
1952 SetDBError(
1953 XO("Failed to update the project file.\nThe following command failed:\n\n%s")
1954 .Format(sql));
1955 };
1956
1957 rc = sqlite3_step(stmt);
1958
1959 if (rc != SQLITE_DONE)
1960 {
1961 ADD_EXCEPTION_CONTEXT("sqlite3.query", sql);
1962 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
1963 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::WriteDoc::step");
1964
1965 reportError(sql);
1966 return false;
1967 }
1968
1969 // Finalize the statement before committing the transaction
1970 sqlite3_finalize(stmt);
1971 stmt = nullptr;
1972
1973 // Get rowid
1974
1975 int64_t rowID = 0;
1976
1977 const wxString rowIDSql =
1978 wxString::Format("SELECT ROWID FROM %s.%s WHERE id = 1;", schema, table);
1979
1980 if (!GetValue(rowIDSql, rowID, true))
1981 {
1982 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(sqlite3_errcode(db)));
1983 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::WriteDoc::rowid");
1984
1985 reportError(rowIDSql);
1986 return false;
1987 }
1988
1989 const auto writeStream = [db, schema, table, rowID, this](const char* column, const MemoryStream& stream) {
1990
1991 auto blobStream =
1992 SQLiteBlobStream::Open(db, schema, table, column, rowID, false);
1993
1994 if (!blobStream)
1995 {
1996 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(sqlite3_errcode(db)));
1997 ADD_EXCEPTION_CONTEXT("sqlite3.col", column);
1998 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::WriteDoc::openBlobStream");
1999
2000 SetDBError(XO("Unable to bind to blob"));
2001 return false;
2002 }
2003
2004 for (auto chunk : stream)
2005 {
2006 if (SQLITE_OK != blobStream->Write(chunk.first, chunk.second))
2007 {
2008 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(sqlite3_errcode(db)));
2009 ADD_EXCEPTION_CONTEXT("sqlite3.col", column);
2010 ADD_EXCEPTION_CONTEXT("sqlite3.context", "ProjectGileIO::WriteDoc::writeBlobStream");
2011 // The user visible message is not changed, so there is no need for new strings
2012 SetDBError(XO("Unable to bind to blob"));
2013 return false;
2014 }
2015 }
2016
2017 if (blobStream->Close() != SQLITE_OK)
2018 {
2020 "sqlite3.rc", std::to_string(sqlite3_errcode(db)));
2021 ADD_EXCEPTION_CONTEXT("sqlite3.col", column);
2023 "sqlite3.context", "ProjectGileIO::WriteDoc::writeBlobStream");
2024 // The user visible message is not changed, so there is no need for new
2025 // strings
2026 SetDBError(XO("Unable to bind to blob"));
2027 return false;
2028 }
2029
2030 return true;
2031 };
2032
2033 if (!writeStream("dict", dict))
2034 return false;
2035
2036 if (!writeStream("doc", data))
2037 return false;
2038
2039 const auto requiredVersion =
2041
2042 const wxString setVersionSql =
2043 wxString::Format("PRAGMA user_version = %u", requiredVersion.GetPacked());
2044
2045 if (!Query(setVersionSql.c_str(), [](auto...) { return 0; }))
2046 {
2047 // DV: Very unlikely case.
2048 // Since we need to improve the error messages in the future, let's use
2049 // the generic message for now, so no new strings are needed
2050 reportError(setVersionSql);
2051 return false;
2052 }
2053
2054 return transaction.Commit();
2055}
2056
2057bool ProjectFileIO::LoadProject(const FilePath &fileName, bool ignoreAutosave)
2058{
2059 auto now = std::chrono::high_resolution_clock::now();
2060
2061 bool success = false;
2062
2063 auto cleanup = finally([&]
2064 {
2065 if (!success)
2066 {
2068 }
2069 });
2070
2072
2073 // Open the project file
2074 if (!OpenConnection(fileName))
2075 {
2076 return false;
2077 }
2078
2079 int64_t rowId = -1;
2080
2081 bool useAutosave =
2082 !ignoreAutosave &&
2083 GetValue("SELECT ROWID FROM main.autosave WHERE id = 1;", rowId, true);
2084
2085 int64_t rowsCount = 0;
2086 // If we didn't have an autosave doc, load the project doc instead
2087 if (
2088 !useAutosave &&
2089 (!GetValue("SELECT COUNT(1) FROM main.project;", rowsCount, true) || rowsCount == 0))
2090 {
2091 // Missing both the autosave and project docs. This can happen if the
2092 // system were to crash before the first autosave into a temporary file.
2093 // This should be a recoverable scenario.
2094 mRecovered = true;
2095 mModified = true;
2096
2097 return true;
2098 }
2099
2100 if (!useAutosave && !GetValue("SELECT ROWID FROM main.project WHERE id = 1;", rowId, false))
2101 {
2102 return false;
2103 }
2104 else
2105 {
2106 // Load 'er up
2108 DB(), "main", useAutosave ? "autosave" : "project", rowId);
2109
2110 success = ProjectSerializer::Decode(stream, this);
2111
2112 if (!success)
2113 {
2114 SetError(
2115 XO("Unable to parse project information.")
2116 );
2117 return false;
2118 }
2119
2120 // Check for orphans blocks...sets mRecovered if any were deleted
2121
2122 auto blockids = WaveTrackFactory::Get( mProject )
2124 ->GetActiveBlockIDs();
2125 if (blockids.size() > 0)
2126 {
2127 success = DeleteBlocks(blockids, true);
2128 if (!success)
2129 {
2130 return false;
2131 }
2132 }
2133
2134 // Remember if we used autosave or not
2135 if (useAutosave)
2136 {
2137 mRecovered = true;
2138 }
2139 }
2140
2141 // Mark the project modified if we recovered it
2142 if (mRecovered)
2143 {
2144 mModified = true;
2145 }
2146
2147 // A previously saved project will have a document in the project table, so
2148 // we use that knowledge to determine if this file is an unsaved/temporary
2149 // file or a permanent project file
2150 wxString result;
2151 success = GetValue("SELECT Count(*) FROM project;", result);
2152 if (!success)
2153 {
2154 return false;
2155 }
2156
2157 mTemporary = !result.IsSameAs(wxT("1"));
2158
2159 SetFileName(fileName);
2160
2162
2163 success = true;
2164
2165 auto duration = std::chrono::high_resolution_clock::now() - now;
2166
2167 wxLogInfo(
2168 "Project loaded in %lld ms",
2169 std::chrono::duration_cast<std::chrono::milliseconds>(duration).count());
2170
2171 return true;
2172}
2173
2175{
2177 WriteXMLHeader(doc);
2178 WriteXML(doc, false, tracks);
2179
2180 if (!WriteDoc("project", doc))
2181 {
2182 return false;
2183 }
2184
2185 // Autosave no longer needed
2186 if (!AutoSaveDelete())
2187 {
2188 return false;
2189 }
2190
2191 return true;
2192}
2193
2194// REVIEW: This function is believed to report an error to the user in all cases
2195// of failure. Callers are believed not to need to do so if they receive 'false'.
2196// LLL: All failures checks should now be displaying an error.
2198 const FilePath &fileName, const TrackList *lastSaved)
2199{
2200 // In the case where we're saving a temporary project to a permanent project,
2201 // we'll try to simply rename the project to save a bit of time. We then fall
2202 // through to the normal Save (not SaveAs) processing.
2203 if (IsTemporary() && mFileName != fileName)
2204 {
2205 FilePath savedName = mFileName;
2206 if (CloseConnection())
2207 {
2208 bool reopened = false;
2209 bool moved = false;
2210 if (true == (moved = MoveProject(savedName, fileName)))
2211 {
2212 if (OpenConnection(fileName))
2213 reopened = true;
2214 else {
2215 MoveProject(fileName, savedName);
2216 moved = false; // No longer moved
2217
2218 reopened = OpenConnection(savedName);
2219 }
2220 }
2221 else {
2222 // Rename can fail -- if it's to a different device, requiring
2223 // real copy of contents, which might exhaust space
2224 reopened = OpenConnection(savedName);
2225 }
2226
2227 // Warning issued in MoveProject()
2228 if (reopened && !moved) {
2229 return false;
2230 }
2231
2232 if (!reopened) {
2233 wxTheApp->CallAfter([this]{
2234 ShowError( {},
2235 XO("Warning"),
2236 XO(
2237"The project's database failed to reopen, "
2238"possibly because of limited space on the storage device."),
2239 "Error:_Disk_full_or_not_writable"
2240 );
2242 });
2243
2244 return false;
2245 }
2246 }
2247 }
2248
2249 // If we're saving to a different file than the current one, then copy the
2250 // current to the new file and make it the active file.
2251 if (mFileName != fileName)
2252 {
2253 // Do NOT prune here since we need to retain the Undo history
2254 // after we switch to the new file.
2255 if (!CopyTo(fileName, XO("Saving project"), false))
2256 {
2257 ShowError( {},
2258 XO("Error Saving Project"),
2260 "Error:_Disk_full_or_not_writable"
2261 );
2262 return false;
2263 }
2264
2265 // Open the newly created database
2266 Connection newConn = std::make_unique<DBConnection>(
2267 mProject.shared_from_this(), mpErrors,
2268 [this]{ OnCheckpointFailure(); });
2269
2270 // NOTE: There is a noticeable delay here when dealing with large multi-hour
2271 // projects that we just created. The delay occurs in Open() when it
2272 // calls SafeMode() and is due to the switch from the NONE journal mode
2273 // to the WAL journal mode.
2274 //
2275 // So, we do the Open() in a thread and display a progress dialog. Since
2276 // this is currently the only known instance where this occurs, we do the
2277 // threading here. If more instances are identified, then the threading
2278 // should be moved to DBConnection::Open(), wrapping the SafeMode() call
2279 // there.
2280 {
2281 std::atomic_bool done = {false};
2282 bool success = true;
2283 auto thread = std::thread([&]
2284 {
2285 auto rc = newConn->Open(fileName);
2286 if (rc != SQLITE_OK)
2287 {
2288 // Capture the error string
2289 SetError(Verbatim(sqlite3_errstr(rc)));
2290 success = false;
2291 }
2292 done = true;
2293 });
2294
2295 // Provides a progress dialog with indeterminate mode
2296 using namespace BasicUI;
2297 auto pd = MakeGenericProgress({},
2298 XO("Syncing"), XO("This may take several seconds"));
2299 wxASSERT(pd);
2300
2301 // Wait for the checkpoints to end
2302 while (!done)
2303 {
2304 using namespace std::chrono;
2305 std::this_thread::sleep_for(50ms);
2306 pd->Pulse();
2307 }
2308 thread.join();
2309
2310 if (!success)
2311 {
2312 // Additional help via a Help button links to the manual.
2313 ShowError( {},
2314 XO("Error Saving Project"),
2315 XO("The project failed to open, possibly due to limited space\n"
2316 "on the storage device.\n\n%s").Format(GetLastError()),
2317 "Error:_Disk_full_or_not_writable");
2318
2319 newConn = nullptr;
2320
2321 // Clean up the destination project
2322 if (!wxRemoveFile(fileName))
2323 {
2324 wxLogMessage("Failed to remove destination project after open failure: %s", fileName);
2325 }
2326
2327 return false;
2328 }
2329 }
2330
2331 // Autosave no longer needed in original project file.
2332 if (!AutoSaveDelete())
2333 {
2334 // Additional help via a Help button links to the manual.
2335 ShowError( {},
2336 XO("Error Saving Project"),
2337 XO("Unable to remove autosave information, possibly due to limited space\n"
2338 "on the storage device.\n\n%s").Format(GetLastError()),
2339 "Error:_Disk_full_or_not_writable");
2340
2341 newConn = nullptr;
2342
2343 // Clean up the destination project
2344 if (!wxRemoveFile(fileName))
2345 {
2346 wxLogMessage("Failed to remove destination project after AutoSaveDelete failure: %s", fileName);
2347 }
2348
2349 return false;
2350 }
2351
2352 if (lastSaved) {
2353 // Bug2605: Be sure not to save orphan blocks
2354 bool recovered = mRecovered;
2355 SampleBlockIDSet blockids;
2356 InspectBlocks( *lastSaved, {}, &blockids );
2357 // TODO: Not sure what to do if the deletion fails
2358 DeleteBlocks(blockids, true);
2359 // Don't set mRecovered if any were deleted
2360 mRecovered = recovered;
2361 }
2362
2363 // Try to compact the original project file.
2364 auto empty = TrackList::Create(&mProject);
2365 Compact( { lastSaved ? lastSaved : empty.get() }, true );
2366
2367 // Safe to close the original project file now. Not much we can do if this fails,
2368 // but we should still be in good shape since we'll be switching to the newly
2369 // saved database below.
2370 CloseProject();
2371
2372 // And make it the active project file
2373 UseConnection(std::move(newConn), fileName);
2374 }
2375 else
2376 {
2377 if ( !UpdateSaved( nullptr ) ) {
2378 ShowError( {},
2379 XO("Error Saving Project"),
2381 "Error:_Disk_full_or_not_writable"
2382 );
2383 return false;
2384 }
2385 }
2386
2387 // Reaching this point defines success and all the rest are no-fail
2388 // operations:
2389
2390 // No longer modified
2391 mModified = false;
2392
2393 // No longer recovered
2394 mRecovered = false;
2395
2396 // No longer a temporary project
2397 mTemporary = false;
2398
2399 // Adjust the title
2401
2402 return true;
2403}
2404
2406{
2407 return CopyTo(fileName, XO("Backing up project"), false, true,
2409}
2410
2412{
2413 return OpenConnection();
2414}
2415
2417{
2418 auto &currConn = CurrConn();
2419 if (!currConn)
2420 {
2421 wxLogDebug("Closing project with no database connection");
2422 return true;
2423 }
2424
2425 // Save the filename since CloseConnection() will clear it
2426 wxString filename = mFileName;
2427
2428 // Not much we can do if this fails. The user will simply get
2429 // the recovery dialog upon next restart.
2430 if (CloseConnection())
2431 {
2432 // If this is a temporary project, we no longer want to keep the
2433 // project file.
2434 if (IsTemporary())
2435 {
2436 // This is just a safety check.
2437 wxFileName temp(TempDirectory::TempDir(), wxT(""));
2438 wxFileName file(filename);
2439 file.SetFullName(wxT(""));
2440 if (file == temp)
2441 RemoveProject(filename);
2442 }
2443 }
2444
2445 return true;
2446}
2447
2449{
2450 FilePath fileName = mFileName;
2451 if (!CloseConnection())
2452 {
2453 return false;
2454 }
2455
2456 return OpenConnection(fileName);
2457}
2458
2460{
2461 return mModified;
2462}
2463
2465{
2466 return mTemporary;
2467}
2468
2470{
2471 return mRecovered;
2472}
2473
2475{
2476 wxLongLong freeSpace;
2477 if (wxGetDiskSpace(wxPathOnly(mFileName), NULL, &freeSpace))
2478 {
2480 // 4 GiB per-file maximum
2481 constexpr auto limit = 1ll << 32;
2482
2483 // Opening a file only to find its length looks wasteful but
2484 // seems to be necessary at least on Windows with FAT filesystems.
2485 // I don't know if that is only a wxWidgets bug.
2486 auto length = wxFile{mFileName}.Length();
2487 // auto length = wxFileName::GetSize(mFileName);
2488
2489 if (length == wxInvalidSize)
2490 length = 0;
2491 auto free = std::max<wxLongLong>(0, limit - length);
2492 freeSpace = std::min(freeSpace, free);
2493 }
2494 return freeSpace;
2495 }
2496
2497 return -1;
2498}
2499
2502 const TranslatableString &dlogTitle,
2503 const TranslatableString &message,
2504 const wxString &helpPage)
2505{
2506 using namespace audacity;
2507 using namespace BasicUI;
2508 ShowErrorDialog( placement, dlogTitle, message, helpPage,
2509 ErrorDialogOptions{ ErrorDialogType::ModalErrorReport }
2510 .Log(ToWString(GetLastLog())));
2511}
2512
2514{
2515 return mpErrors->mLastError;
2516}
2517
2519{
2520 return mpErrors->mLibraryError;
2521}
2522
2524{
2525 return mpErrors->mErrorCode;
2526}
2527
2528const wxString &ProjectFileIO::GetLastLog() const
2529{
2530 return mpErrors->mLog;
2531}
2532
2534 const TranslatableString& msg, const TranslatableString& libraryError, int errorCode)
2535{
2536 auto &currConn = CurrConn();
2537 if (currConn)
2538 currConn->SetError(msg, libraryError, errorCode);
2539}
2540
2542 const TranslatableString &msg, const TranslatableString &libraryError, int errorCode)
2543{
2544 auto &currConn = CurrConn();
2545 if (currConn)
2546 currConn->SetDBError(msg, libraryError, errorCode);
2547}
2548
2550{
2551 auto &currConn = CurrConn();
2552 if (!currConn)
2553 return;
2554
2555 // Determine if we can bypass sample block deletes during shutdown.
2556 //
2557 // IMPORTANT:
2558 // If the project was compacted, then we MUST bypass further
2559 // deletions since the new file doesn't have the blocks that the
2560 // Sequences expect to be there.
2561
2562 currConn->SetBypass( true );
2563
2564 // Only permanent project files need cleaning at shutdown
2565 if (!IsTemporary() && !WasCompacted())
2566 {
2567 // If we still have unused blocks, then we must not bypass deletions
2568 // during shutdown. Otherwise, we would have orphaned blocks the next time
2569 // the project is opened.
2570 //
2571 // An example of when dead blocks will exist is when a user opens a permanent
2572 // project, adds a track (with samples) to it, and chooses not to save the
2573 // changes.
2574 if (HadUnused())
2575 {
2576 currConn->SetBypass( false );
2577 }
2578 }
2579
2580 return;
2581}
2582
2584{
2585 auto pConn = CurrConn().get();
2586 if (!pConn)
2587 return 0;
2588 return GetDiskUsage(*pConn, blockid);
2589}
2590
2592 const std::vector<const TrackList*> &trackLists) const
2593{
2594 unsigned long long current = 0;
2595 const auto fn = BlockSpaceUsageAccumulator(current);
2596
2597 // Must pass address of this set, even if not otherwise used, to avoid
2598 // possible multiple count of shared blocks
2599 SampleBlockIDSet seen;
2600 for (auto pTracks: trackLists)
2601 if (pTracks)
2602 InspectBlocks(*pTracks, fn, &seen);
2603
2604 return current;
2605}
2606
2608{
2609 auto pConn = CurrConn().get();
2610 if (!pConn)
2611 return 0;
2612 return GetDiskUsage(*pConn, 0);
2613}
2614
2615//
2616// Returns the estimation of disk space used by the specified sample blockid or all
2617// of the sample blocks if the blockid is 0. This does not include small overhead
2618// of the internal SQLite structures, only the size used by the data
2619//
2621{
2622 sqlite3_stmt* stmt = nullptr;
2623
2624 if (blockid == 0)
2625 {
2626 static const char* statement =
2627R"(SELECT
2628 sum(length(blockid) + length(sampleformat) +
2629 length(summin) + length(summax) + length(sumrms) +
2630 length(summary256) + length(summary64k) +
2631 length(samples))
2632FROM sampleblocks;)";
2633
2634 stmt = conn.Prepare(DBConnection::GetAllSampleBlocksSize, statement);
2635 }
2636 else
2637 {
2638 static const char* statement =
2639R"(SELECT
2640 length(blockid) + length(sampleformat) +
2641 length(summin) + length(summax) + length(sumrms) +
2642 length(summary256) + length(summary64k) +
2643 length(samples)
2644FROM sampleblocks WHERE blockid = ?1;)";
2645
2646 stmt = conn.Prepare(DBConnection::GetSampleBlockSize, statement);
2647 }
2648
2649 auto cleanup = finally(
2650 [stmt]() {
2651 // Clear statement bindings and rewind statement
2652 if (stmt != nullptr)
2653 {
2654 sqlite3_clear_bindings(stmt);
2655 sqlite3_reset(stmt);
2656 }
2657 });
2658
2659 if (blockid != 0)
2660 {
2661 int rc = sqlite3_bind_int64(stmt, 1, blockid);
2662
2663 if (rc != SQLITE_OK)
2664 {
2666 "sqlite3.rc", std::to_string(rc));
2667
2669 "sqlite3.context", "ProjectFileIO::GetDiskUsage::bind");
2670
2671 conn.ThrowException(false);
2672 }
2673 }
2674
2675 int rc = sqlite3_step(stmt);
2676
2677 if (rc != SQLITE_ROW)
2678 {
2679 ADD_EXCEPTION_CONTEXT("sqlite3.rc", std::to_string(rc));
2680
2682 "sqlite3.context", "ProjectFileIO::GetDiskUsage::step");
2683
2684 conn.ThrowException(false);
2685 }
2686
2687 const int64_t size = sqlite3_column_int64(stmt, 0);
2688
2689 return size;
2690}
2691
2693 : mpProject{ AudacityProject::Create() }
2694{
2695}
2696
2698{
2699 auto &projectFileIO = ProjectFileIO::Get( Project() );
2700 projectFileIO.SetBypass();
2701 auto &tracks = TrackList::Get( Project() );
2702 tracks.Clear();
2703
2704 // Consume some delayed track list related events before destroying the
2705 // temporary project
2706 try { wxTheApp->Yield(); } catch(...) {}
2707
2708 // Destroy the project and yield again to let delayed window deletions happen
2709 projectFileIO.CloseProject();
2710 mpProject.reset();
2711 try { wxTheApp->Yield(); } catch(...) {}
2712}
2713
2715static ProjectHistory::AutoSave::Scope scope {
2716[](AudacityProject &project) {
2717 auto &projectFileIO = ProjectFileIO::Get(project);
2718 if ( !projectFileIO.AutoSave() )
2721 XO("Automatic database backup failed."),
2722 XO("Warning"),
2723 "Error:_Disk_full_or_not_writable"
2724 };
2725} };
wxT("CloseDown"))
@ Internal
Indicates internal failure from Audacity.
SimpleGuard< R > MakeSimpleGuard(R value) noexcept(noexcept(SimpleGuard< R >{ value }))
Convert a value to a handler function returning that value, suitable for GuardedCall<R>
Toolkit-neutral facade for basic user interface services.
Declare functions to perform UTF-8 to std::wstring conversions.
int min(int a, int b)
Declare DBConnection, which maintains database connection and associated status and background thread...
std::unique_ptr< DBConnection > Connection
Definition: DBConnection.h:132
const TranslatableString name
Definition: Distortion.cpp:74
FromCharsResult FromChars(const char *buffer, const char *last, float &value) noexcept
Parse a string into a single precision floating point value, always uses the dot as decimal.
Definition: FromChars.cpp:153
Declare functions to convert numeric types to string representation.
XO("Cut/Copy/Paste")
#define THROW_INCONSISTENCY_EXCEPTION
Throw InconsistencyException, using C++ preprocessor to identify the source code location.
#define _TS(s)
Definition: Internat.h:27
#define _(s)
Definition: Internat.h:75
@ pdlgHideStopButton
std::unique_ptr< const BasicUI::WindowPlacement > ProjectFramePlacement(AudacityProject *project)
Make a WindowPlacement object suitable for project (which may be null)
Definition: Project.cpp:129
wxString FilePath
Definition: Project.h:21
#define AUDACITY_FILE_FORMAT_VERSION
static ProjectHistory::AutoSave::Scope scope
Install the callback from undo manager.
static const int ProjectFileID
static const AudacityProject::AttachedObjects::RegisteredFactory sFileIOKey
#define PACK(b1, b2, b3, b4)
static int ExecCallback(void *data, int cols, char **vals, char **names)
static const char * ProjectFileSchema
static void RefreshAllTitles(bool bShowProjectNumbers)
std::unordered_set< SampleBlockID > BlockIDs
Definition: ProjectFileIO.h:46
@ CheckpointFailure
Failure happened in a worker thread.
@ ProjectTitleChange
A normal occurrence.
long long SampleBlockID
Definition: ProjectFileIO.h:42
const ProjectFormatVersion BaseProjectFormatVersion
This is a helper constant for the "most compatible" project version with the value (3,...
const ProjectFormatVersion SupportedProjectFormatVersion
This constant represents the current version of Audacity.
wxFrame * FindProjectFrame(AudacityProject *project)
Get a pointer to the window associated with a project, or null if the given pointer is null,...
AUDACITY_DLL_API wxFrame & GetProjectFrame(AudacityProject &project)
Get the top-level window associated with the project (as a wxFrame only, when you do not need to use ...
accessors for certain important windows associated with each project
std::function< void(const SampleBlock &) > BlockSpaceUsageAccumulator(unsigned long long &total)
Definition: SampleBlock.h:101
#define ADD_EXCEPTION_CONTEXT(name, value)
Definition: SentryHelper.h:21
static TranslatableStrings names
Definition: TagsEditor.cpp:152
TranslatableString Verbatim(wxString str)
Require calls to the one-argument constructor to go through this distinct global function name.
void InspectBlocks(const TrackList &tracks, BlockInspector inspector, SampleBlockIDSet *pIDs)
Definition: WaveTrack.cpp:2778
std::unordered_set< SampleBlockID > SampleBlockIDSet
Definition: WaveTrack.h:600
static const auto fn
std::vector< Attribute > AttributesList
Definition: XMLTagHandler.h:40
const_iterator end() const
Definition: Project.cpp:27
Container::value_type value_type
Definition: Project.h:57
const_iterator begin() const
Definition: Project.cpp:22
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
int GetProjectNumber()
Definition: Project.h:101
const wxString & GetProjectName() const
Definition: Project.cpp:100
Subclasses may hold information such as a parent window pointer for a dialog.
Definition: BasicUI.h:29
BufferedProjectBlobStream(sqlite3 *db, const char *schema, const char *table, int64_t rowID)
static constexpr std::array< const char *, 2 > Columns
bool OpenBlob(size_t index)
bool HasMoreData() const override
size_t ReadData(void *buffer, size_t maxBytes) override
std::optional< SQLiteBlobStream > mBlobStream
A facade-like class, that implements buffered reading from the underlying data stream.
Client code makes static instance from a factory of attachments; passes it to Get or Find as a retrie...
Definition: ClientData.h:266
static ConnectionPtr & Get(AudacityProject &project)
void ThrowException(bool write) const
throw and show appropriate message box
@ GetAllSampleBlocksSize
Definition: DBConnection.h:83
sqlite3_stmt * Prepare(enum StatementID id, const char *sql)
sqlite3 * DB()
static TranslatableString WriteFailureMessage(const wxFileName &fileName)
Abstract base class used in importing a file.
AudacityProject & Project()
std::shared_ptr< AudacityProject > mpProject
A low overhead memory stream with O(1) append, low heap fragmentation and a linear memory view.
const size_t GetSize() const noexcept
CallbackReturn Publish(const ProjectFileIOMessage &message)
Send a message to connected callbacks.
Definition: Observer.h:207
ProgressDialog Class.
ProgressResult Update(int value, const TranslatableString &message={})
BackupProject(ProjectFileIO &projectFileIO, const FilePath &path)
Rename project file at path, and any auxiliary files, to backup path names.
~BackupProject()
if !IsOk() do nothing; else if Discard() was not called, undo the renaming
void Discard()
if !IsOk() do nothing; else remove backup files
Object associated with a project that manages reading and writing of Audacity project file formats,...
Definition: ProjectFileIO.h:64
AudacityProject & mProject
DBConnection & GetConnection()
Return a reference to a connection, creating it as needed on demand; throw on failure.
void RestoreConnection()
bool LoadProject(const FilePath &fileName, bool ignoreAutosave)
bool AutoSave(bool recording=false)
void OnCheckpointFailure()
bool MoveProject(const FilePath &src, const FilePath &dst)
void UpdatePrefs() override
static bool RemoveProject(const FilePath &filename)
Remove any files associated with a project at given path; return true if successful.
FilePath mFileName
bool UpdateSaved(const TrackList *tracks=nullptr)
bool CopyTo(const FilePath &destpath, const TranslatableString &msg, bool isTemporary, bool prune=false, const std::vector< const TrackList * > &tracks={})
const TranslatableString & GetLibraryError() const
void SetProjectTitle(int number=-1)
void UseConnection(Connection &&conn, const FilePath &filePath)
void SetDBError(const TranslatableString &msg, const TranslatableString &libraryError={}, int errorCode=-1)
Set stored errors and write to log; and default libraryError to what database library reports.
void DiscardConnection()
bool GetValue(const char *sql, wxString &value, bool silent=false)
bool CloseConnection()
int64_t GetBlockUsage(SampleBlockID blockid)
static ProjectFileIO & Get(AudacityProject &project)
std::function< int(int cols, char **vals, char **names)> ExecCB
wxString GenerateDoc()
Return a strings representation of the active project XML doc.
void SetFileName(const FilePath &fileName)
const FilePath & GetFileName() const
bool OpenConnection(FilePath fileName={})
bool RenameOrWarn(const FilePath &src, const FilePath &dst)
Rename a file or put up appropriate warning message.
bool SaveProject(const FilePath &fileName, const TrackList *lastSaved)
FilePath mPrevFileName
Connection & CurrConn()
void ShowError(const BasicUI::WindowPlacement &placement, const TranslatableString &dlogTitle, const TranslatableString &message, const wxString &helpPage)
Displays an error dialog with a button that offers help.
bool InstallSchema(sqlite3 *db, const char *schema="main")
void Compact(const std::vector< const TrackList * > &tracks, bool force=false)
int Exec(const char *query, const ExecCB &callback, bool silent=false)
int GetLastErrorCode() const
bool DeleteBlocks(const BlockIDs &blockids, bool complement)
bool SaveCopy(const FilePath &fileName)
bool ShouldCompact(const std::vector< const TrackList * > &tracks)
static FilePath SafetyFileName(const FilePath &src)
Generate a name for short-lived backup project files from an existing project.
bool Query(const char *sql, const ExecCB &callback, bool silent=false)
void WriteXMLHeader(XMLWriter &xmlFile) const
int64_t GetCurrentUsage(const std::vector< const TrackList * > &trackLists) const
XMLTagHandler * HandleXMLChild(const std::string_view &tag) override
const TranslatableString & GetLastError() const
void WriteXML(XMLWriter &xmlFile, bool recording=false, const TrackList *tracks=nullptr)
bool IsRecovered() const
void SetError(const TranslatableString &msg, const TranslatableString &libraryError={}, int errorCode={})
Just set stored errors.
bool AutoSaveDelete(sqlite3 *db=nullptr)
static bool InitializeSQL()
bool IsTemporary() const
Connection mPrevConn
static const std::vector< wxString > & AuxiliaryFileSuffixes()
static void InSet(sqlite3_context *context, int argc, sqlite3_value **argv)
std::shared_ptr< DBConnectionErrors > mpErrors
bool WriteDoc(const char *table, const ProjectSerializer &autosave, const char *schema="main")
bool IsModified() const
ProjectFileIO(AudacityProject &project)
int64_t GetTotalUsage()
sqlite3 * DB()
bool HandleXMLTag(const std::string_view &tag, const AttributesList &attrs) override
wxLongLong GetFreeDiskSpace() const
bool HasConnection() const
Return true if a connection is now open.
static int64_t GetDiskUsage(DBConnection &conn, SampleBlockID blockid)
const wxString & GetLastLog() const
ProjectFormatVersion GetRequiredVersion(const AudacityProject &project) const
Returns the minimum possible version that can be used to save the project.
static const ProjectFormatExtensionsRegistry & Get()
a class used to (de)serialize the project catalog
static bool Decode(BufferedStreamReader &in, XMLTagHandler *handler)
const MemoryStream & GetData() const
const MemoryStream & GetDict() const
int Close() noexcept
bool IsOpen() const noexcept
int Write(const void *ptr, int size) noexcept
sqlite3_blob * mBlob
~SQLiteBlobStream() noexcept
int Read(void *ptr, int &size) noexcept
SQLiteBlobStream(sqlite3_blob *blob, bool readOnly) noexcept
static std::optional< SQLiteBlobStream > Open(sqlite3 *db, const char *schema, const char *table, const char *column, int64_t rowID, bool readOnly) noexcept
SQLiteBlobStream & operator=(SQLiteBlobStream &&rhs) noexcept
bool IsEof() const noexcept
SQLiteBlobStream(SQLiteBlobStream &&rhs) noexcept
static void LogCallback(void *WXUNUSED(arg), int code, const char *msg)
A MessageBoxException that shows a given, unvarying string.
size_t UnnamedCount
TitleRestorer(wxTopLevelWindow &window, AudacityProject &project)
wxString sProjName
wxString sProjNumber
Abstract base class for an object holding data associated with points on a time axis.
Definition: Track.h:225
std::shared_ptr< Track > SubstitutePendingChangedTrack()
Definition: Track.cpp:1210
An in-session identifier of track objects across undo states. It does not persist between sessions.
Definition: Track.h:151
A flat linked list of tracks supporting Add, Remove, Clear, and Contains, serialization of the list o...
Definition: Track.h:1338
static std::shared_ptr< TrackList > Create(AudacityProject *pOwner)
Definition: Track.cpp:502
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:486
RAII for a database transaction, possibly nested.
bool Commit()
Commit the transaction.
Holds a msgid for the translation catalog; may also bind format arguments.
static WaveTrackFactory & Get(AudacityProject &project)
Definition: WaveTrack.cpp:2797
const SampleBlockFactoryPtr & GetSampleBlockFactory() const
Definition: WaveTrack.h:637
XMLTagHandler * CallObjectAccessor(const std::string_view &tag, Host &host)
static XMLMethodRegistry & Get()
Get the unique instance.
void CallWriters(const Host &host, XMLWriter &writer)
Wrapper to output XML data to strings.
Definition: XMLWriter.h:140
This class is an interface which should be implemented by classes which wish to be able to load and s...
Definition: XMLTagHandler.h:42
Base class for XMLFileWriter and XMLStringWriter that provides the general functionality for creating...
Definition: XMLWriter.h:26
virtual void StartTag(const wxString &name)
Definition: XMLWriter.cpp:79
void WriteAttr(const wxString &name, const Identifier &value)
Definition: XMLWriter.h:37
virtual void EndTag(const wxString &name)
Definition: XMLWriter.cpp:102
virtual void Write(const wxString &data)=0
void Add(const FilePath &path)
void Remove(const FilePath &path)
std::unique_ptr< GenericProgressDialog > MakeGenericProgress(const WindowPlacement &placement, const TranslatableString &title, const TranslatableString &message)
Create and display a progress dialog (return nullptr if Services not installed)
Definition: BasicUI.h:302
ProgressResult
Definition: BasicUI.h:145
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:206
void ShowErrorDialog(const WindowPlacement &placement, const TranslatableString &dlogTitle, const TranslatableString &message, const ManualPageID &helpPage, const ErrorDialogOptions &options={})
Show an error dialog with a link to the manual for further help.
Definition: BasicUI.h:254
UTILITY_API const char *const * argv
A copy of argv; responsibility of application startup to assign it.
UTILITY_API int argc
A copy of argc; responsibility of application startup to assign it.
FILES_API bool IsOnFATFileSystem(const FilePath &path)
FILES_API wxString AbbreviatePath(const wxFileName &fileName)
Give enough of the path to identify the device. (On Windows, drive letter plus ':')
FILES_API wxString UnsavedProjectFileName()
FILES_API wxString TempDir()
void swap(std::unique_ptr< Alg_seq > &a, std::unique_ptr< Alg_seq > &b)
Definition: NoteTrack.cpp:752
std::wstring ToWString(const std::string &str)
STL namespace.
Options for variations of error dialogs; the default is for modal dialogs.
Definition: BasicUI.h:49
ErrorDialogOptions && Log(std::wstring log_) &&
Definition: BasicUI.h:61
std::errc ec
A pointer to the first character not matching the pattern.
Definition: FromChars.h:23
A structure that holds the project version.
static ProjectFormatVersion FromPacked(uint32_t) noexcept
uint32_t GetPacked() const noexcept
Returns a version packed to 32-bit integer.