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