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