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