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