audacia/src/ProjectFSCK.cpp

393 lines
16 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
ProjectFSCK.cpp
A function that performs consistency checks on the tree of block files
Paul Licameli split this out of DirManager.cpp
**********************************************************************/
#include "ProjectFSCK.h"
#include <wx/log.h>
#include <wx/string.h>
#include "BlockFile.h"
#include "DirManager.h"
#include "widgets/AudacityMessageBox.h"
#include "Internat.h"
#include "MemoryX.h"
#include "MissingAliasFileDialog.h"
#include "ondemand/ODManager.h"
#include "widgets/MultiDialog.h"
#include "widgets/ProgressDialog.h"
// Check the BlockFiles against the disk state.
// Missing Blockfile data can be regenerated if possible or replaced with silence.
// Orphan blockfiles can be deleted.
// Note that even BlockFiles not referenced by the current savefile (but locked
// by history) will be reflected in the mBlockFileHash, and that's a
// good thing; this is one reason why we use the hash and not the most
// recent savefile.
int ProjectFSCK(
DirManager &dm, const bool bForceError, const bool bAutoRecoverMode)
{
// In earlier versions of this method, enumerations of errors were
// all done in sequence, then the user was prompted for each type of error.
// The enumerations are now interleaved with prompting, because, for example,
// user choosing to replace missing aliased block files with silence
// needs to put in SilentBlockFiles and DELETE the corresponding auf files,
// so those would then not be cumulated in missingAUFHash.
// We still do the FindX methods outside the conditionals,
// so the log always shows all found errors.
int action; // choice of action for each type of error
int nResult = 0;
if (bForceError && !bAutoRecoverMode)
{
// TODO: Replace with more user friendly error message?
/* i18n-hint: The audacity project file is XML and has 'tags' in it,
rather like html tags <something>some stuff</something>.
This error message is about the tags that hold the sequence information.
The error message is confusing to users in English, and could just say
"Found problems with <sequence> when checking project file." */
wxString msg = _("Project check read faulty Sequence tags.");
const wxChar *buttons[] =
{_("Close project immediately with no changes"),
_("Continue with repairs noted in log, and check for more errors. This will save the project in its current state, unless you \"Close project immediately\" on further error alerts."),
NULL};
wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
action = ShowMultiDialog(msg, XO("Warning - Problems Reading Sequence Tags"), buttons);
if (action == 0)
nResult = FSCKstatus_CLOSE_REQ;
else
nResult = FSCKstatus_CHANGED | FSCKstatus_SAVE_AUP;
}
FilePaths filePathArray; // *all* files in the project directory/subdirectories
auto dirPath = ( dm.GetDataFilesDir() );
DirManager::RecursivelyEnumerateWithProgress(
dirPath,
filePathArray, // output: all files in project directory tree
wxEmptyString, // All dirs
wxEmptyString, // All files
true, false,
dm.NumBlockFiles(), // rough guess of how many BlockFiles will be found/processed, for progress
XO("Inspecting project file data"));
//
// MISSING ALIASED AUDIO FILES
//
MissingAliasFilesDialog::SetShouldShow(false);
BlockHash missingAliasFilesAUFHash; // (.auf) AliasBlockFiles whose aliased files are missing
BlockHash missingAliasFilesPathHash; // full paths of missing aliased files
dm.FindMissingAliasFiles(missingAliasFilesAUFHash, missingAliasFilesPathHash);
if ((nResult != FSCKstatus_CLOSE_REQ) && !missingAliasFilesAUFHash.empty())
{
// In auto-recover mode, we always create silent blocks, and do not ask user.
// This makes sure the project is complete next time we open it.
if (bAutoRecoverMode)
action = 2;
else
{
wxString msgA =
_("Project check of \"%s\" folder \
\ndetected %lld missing external audio file(s) \
\n('aliased files'). There is no way for Audacity \
\nto recover these files automatically. \
\n\nIf you choose the first or second option below, \
\nyou can try to find and restore the missing files \
\nto their previous location. \
\n\nNote that for the second option, the waveform \
\nmay not show silence. \
\n\nIf you choose the third option, this will save the \
\nproject in its current state, unless you \"Close \
\nproject immediately\" on further error alerts.");
wxString msg;
msg.Printf(msgA, dm.GetProjectName(), (long long) missingAliasFilesPathHash.size());
const wxChar *buttons[] =
{_("Close project immediately with no changes"),
_("Treat missing audio as silence (this session only)"),
_("Replace missing audio with silence (permanent immediately)."),
NULL};
wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
action = ShowMultiDialog(msg, XO("Warning - Missing Aliased File(s)"), buttons);
}
if (action == 0)
nResult = FSCKstatus_CLOSE_REQ;
else
{
// LL: A progress dialog should probably be used here
BlockHash::iterator iter = missingAliasFilesAUFHash.begin();
while (iter != missingAliasFilesAUFHash.end())
{
// This type cast is safe. We checked that it's an alias block file earlier.
BlockFilePtr b = iter->second.lock();
wxASSERT(b);
if (b) {
auto ab = static_cast< AliasBlockFile * > ( &*b );
if (action == 2)
{
// silence the blockfiles by yanking the filename
// This is done, eventually, in PCMAliasBlockFile::ReadData()
// and ODPCMAliasBlockFile::ReadData, in the stack of b->Recover().
// There, if the mAliasedFileName is bad, it zeroes the data.
wxFileNameWrapper dummy;
dummy.Clear();
ab->ChangeAliasedFileName(std::move(dummy));
// If recovery fails for one file, silence it,
// and don't try to recover other files but
// silence them too. GuardedCall will cause an appropriate
// error message for the user.
GuardedCall(
[&] { ab->Recover(); },
[&] (AudacityException*) { action = 1; }
);
nResult = FSCKstatus_CHANGED | FSCKstatus_SAVE_AUP;
}
if (action == 1)
// Silence error logging for this block in this session.
ab->SilenceAliasLog();
}
++iter;
}
if ((action == 2) && bAutoRecoverMode)
wxLogWarning(_(" Project check replaced missing aliased file(s) with silence."));
}
}
//
// MISSING ALIAS (.AUF) AliasBlockFiles
//
// Alias summary regeneration must happen after checking missing aliased files.
//
BlockHash missingAUFHash; // missing (.auf) AliasBlockFiles
dm.FindMissingAUFs(missingAUFHash);
if ((nResult != FSCKstatus_CLOSE_REQ) && !missingAUFHash.empty())
{
// In auto-recover mode, we just recreate the alias files, and do not ask user.
// This makes sure the project is complete next time we open it.
if (bAutoRecoverMode)
action = 0;
else
{
wxString msgA =
_("Project check of \"%s\" folder \
\ndetected %lld missing alias (.auf) blockfile(s). \
\nAudacity can fully regenerate these files \
\nfrom the current audio in the project.");
wxString msg;
msg.Printf(msgA, dm.GetProjectName(), (long long) missingAUFHash.size());
const wxChar *buttons[] = {_("Regenerate alias summary files (safe and recommended)"),
_("Fill in silence for missing display data (this session only)"),
_("Close project immediately with no further changes"),
NULL};
wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
action = ShowMultiDialog(msg, XO("Warning - Missing Alias Summary File(s)"), buttons);
}
if (action == 2)
nResult = FSCKstatus_CLOSE_REQ;
else
{
// LL: A progress dialog should probably be used here
BlockHash::iterator iter = missingAUFHash.begin();
while (iter != missingAUFHash.end())
{
BlockFilePtr b = iter->second.lock();
wxASSERT(b);
if (b) {
if(action==0) {
//regenerate from data
// If recovery fails for one file, silence it,
// and don't try to recover other files but
// silence them too. GuardedCall will cause an appropriate
// error message for the user.
GuardedCall(
[&] {
b->Recover();
nResult |= FSCKstatus_CHANGED;
},
[&] (AudacityException*) { action = 1; }
);
}
if (action==1){
// Silence error logging for this block in this session.
b->SilenceLog();
}
}
++iter;
}
if ((action == 0) && bAutoRecoverMode)
wxLogWarning(_(" Project check regenerated missing alias summary file(s)."));
}
}
//
// MISSING (.AU) SimpleBlockFiles
//
BlockHash missingAUHash; // missing data (.au) blockfiles
dm.FindMissingAUs(missingAUHash);
if ((nResult != FSCKstatus_CLOSE_REQ) && !missingAUHash.empty())
{
// In auto-recover mode, we just always create silent blocks.
// This makes sure the project is complete next time we open it.
if (bAutoRecoverMode)
action = 2;
else
{
wxString msgA =
_("Project check of \"%s\" folder \
\ndetected %lld missing audio data (.au) blockfile(s), \
\nprobably due to a bug, system crash, or accidental \
\ndeletion. There is no way for Audacity to recover \
\nthese missing files automatically. \
\n\nIf you choose the first or second option below, \
\nyou can try to find and restore the missing files \
\nto their previous location. \
\n\nNote that for the second option, the waveform \
\nmay not show silence.");
wxString msg;
msg.Printf(msgA, dm.GetProjectName(), (long long) missingAUHash.size());
const wxChar *buttons[] =
{_("Close project immediately with no further changes"),
_("Treat missing audio as silence (this session only)"),
_("Replace missing audio with silence (permanent immediately)"),
NULL};
wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
action = ShowMultiDialog(msg, XO("Warning - Missing Audio Data Block File(s)"), buttons);
}
if (action == 0)
nResult = FSCKstatus_CLOSE_REQ;
else
{
// LL: A progress dialog should probably be used here
BlockHash::iterator iter = missingAUHash.begin();
while (iter != missingAUHash.end())
{
BlockFilePtr b = iter->second.lock();
wxASSERT(b);
if (b) {
if (action == 2)
{
//regenerate from data
// If recovery fails for one file, silence it,
// and don't try to recover other files but
// silence them too. GuardedCall will cause an appropriate
// error message for the user.
GuardedCall(
[&] {
//regenerate with zeroes
b->Recover();
nResult |= FSCKstatus_CHANGED;
},
[&] (AudacityException*) { action = 1; }
);
}
if (action == 1)
b->SilenceLog();
}
++iter;
}
if ((action == 2) && bAutoRecoverMode)
wxLogWarning(_(" Project check replaced missing audio data block file(s) with silence."));
}
}
//
// ORPHAN BLOCKFILES (.au and .auf files that are not in the project.)
//
FilePaths orphanFilePathArray; // orphan .au and .auf files
dm.FindOrphanBlockFiles(filePathArray, orphanFilePathArray);
if ((nResult != FSCKstatus_CLOSE_REQ) && !orphanFilePathArray.empty())
{
// In auto-recover mode, leave orphan blockfiles alone.
// They will be deleted when project is saved the first time.
if (bAutoRecoverMode)
{
wxLogWarning(_(" Project check ignored orphan block file(s). They will be deleted when project is saved."));
action = 1;
}
else
{
wxString msgA =
_("Project check of \"%s\" folder \
\nfound %d orphan block file(s). These files are \
\nunused by this project, but might belong to \
other projects. \
\nThey are doing no harm and are small.");
wxString msg;
msg.Printf(msgA, dm.GetProjectName(), (int)orphanFilePathArray.size());
const wxChar *buttons[] =
{_("Continue without deleting; ignore the extra files this session"),
_("Close project immediately with no further changes"),
_("Delete orphan files (permanent immediately)"),
NULL};
wxLog::FlushActive(); // MultiDialog has "Show Log..." button, so make sure log is current.
action = ShowMultiDialog(msg, XO("Warning - Orphan Block File(s)"), buttons);
}
if (action == 1)
nResult = FSCKstatus_CLOSE_REQ;
// Nothing is done if (action == 0).
else if (action == 2)
{
// FSCKstatus_CHANGED was bogus here.
// The files are deleted, so "Undo Project Repair" could not do anything.
// Plus they affect none of the valid tracks, so incorrect to mark them changed,
// and no need for refresh.
// nResult |= FSCKstatus_CHANGED;
for ( const auto &orphan : orphanFilePathArray )
wxRemoveFile(orphan);
}
}
if ((nResult != FSCKstatus_CLOSE_REQ) && !ODManager::HasLoadedODFlag())
{
// Remove any empty directories.
ProgressDialog pProgress(
XO("Progress"),
XO("Cleaning up unused directories in project data"));
// nDirCount is for updating pProgress. +1 because we may DELETE dirPath.
int nDirCount = DirManager::RecursivelyCountSubdirs(dirPath) + 1;
DirManager::RecursivelyRemoveEmptyDirs(dirPath, nDirCount, &pProgress);
}
// Summarize and flush the log.
if (bForceError ||
!missingAliasFilesAUFHash.empty() ||
!missingAUFHash.empty() ||
!missingAUHash.empty() ||
!orphanFilePathArray.empty())
{
wxLogWarning(_("Project check found file inconsistencies inspecting the loaded project data."));
wxLog::FlushActive(); // Flush is modal and will clear the log (both desired).
// In auto-recover mode, we didn't do any ShowMultiDialog calls above, so put up an alert.
if (bAutoRecoverMode)
::AudacityMessageBox(
XO(
"Project check found file inconsistencies during automatic recovery.\n\nSelect 'Help > Diagnostics > Show Log...' to see details."),
XO("Warning: Problems in Automatic Recovery"),
wxOK | wxICON_EXCLAMATION);
}
MissingAliasFilesDialog::SetShouldShow(true);
return nResult;
}