From ea8871af1b5ebd6397b5110426728ac068db3c72 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 16 Oct 2026 10:39:29 +1300 Subject: [PATCH] Offer a damaged project's previous version, or what can be read of it A project file that could not be read showed "The file ... does not appear to be a QElectroTech project file", for a .qetz missing one picture as for a picture file renamed .qet: wrong, and no way out. Now the message says what is damaged (QETProject::openError()) and offers, when there are any: - the newest previous version kept beside it (PreviousVersions); - for a .qetz, what can be read of it (QetContainer::recover()), then a list of what was left out. Either opens as a new project, " (recovered)" beside the file, numbered if that is taken, not saved yet. The damaged file is never moved or written. Checked in the program (scripts/qet-gui-dialog.sh, File > Open on a .qetz whose picture is missing, a previous version beside it): the dialog says which picture; "Recover what can be read" opens plant (recovered).qetz without the picture, saved with Ctrl+S it opens again; "Open the version saved ..." opens plant (recovered 2).qetz with its picture; plant.qetz unchanged (same sha256). ctest 107/107. Stacked on "Keep the previous versions of a project when saving it" and "Add recovering what can be read of a damaged .qetz". Co-Authored-By: Claude Opus 5.5 --- sources/qetdiagrameditor.cpp | 126 ++++++++++++++++++++++++++++++++++- sources/qetdiagrameditor.h | 3 + sources/qetproject.cpp | 11 +++ sources/qetproject.h | 3 + 4 files changed, 142 insertions(+), 1 deletion(-) diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index e582ac26f..a646f942a 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -16,6 +16,9 @@ along with QElectroTech. If not, see . */ #include "qetdiagrameditor.h" +#include "container/qetcontainer.h" +#include "container/qetzip.h" +#include "utils/previousversions.h" #include "shownkinds.h" #ifdef QET_HAS_SCRIPTING #include "scripting/qetscripting.h" @@ -25,6 +28,8 @@ #include "scripting/macrorecorder.h" #include "scripting/assistantinfo.h" #endif +#include +#include #include #include #include @@ -1888,6 +1893,115 @@ bool QETDiagramEditor::closeProject(QETProject *project) return(true); } +/** + @brief QETDiagramEditor::offerToRecover + @p filepath could not be read (@p reason): say why, and offer its newest + previous version (PreviousVersions) and, for a .qetz, what can be read + of it (QetContainer::recover()). Either opens as a new project, + " (recovered)"; @p filepath is never moved or written. + @return true if a project was opened +*/ +bool QETDiagramEditor::offerToRecover(const QString &filepath, const QString &reason) +{ + const QFileInfo info(filepath); + QFile file(filepath); + const bool zip = file.open(QIODevice::ReadOnly) && QetContainer::isZip(file.peek(4)); + file.close(); + const QStringList versions = PreviousVersions::kept(filepath); + + QMessageBox box(QMessageBox::Warning, tr("Cannot open %1").arg(info.fileName()), + reason.isEmpty() + ? tr("The file %1 cannot be read as a QElectroTech project.").arg(info.fileName()) + : tr("The project file is damaged:\n%1").arg(reason), + QMessageBox::Cancel, this); + QAbstractButton *previous = nullptr, *recover = nullptr; + if (!versions.isEmpty()) { + const QDateTime saved = QFileInfo(versions.first()).lastModified(); + previous = box.addButton(tr("Open the version saved %1") + .arg(QLocale().toString(saved, QLocale::ShortFormat)), + QMessageBox::AcceptRole); + } + if (zip) + recover = box.addButton(tr("Recover what can be read"), QMessageBox::AcceptRole); + if (previous || recover) + box.setInformativeText(tr("Either opens as a new project, \"%1\"; %2 is left as it is.") + .arg(recoveredPath(filepath, info.suffix()).section(QLatin1Char('/'), -1), + info.fileName())); + box.exec(); + + if (previous && box.clickedButton() == previous) + return openAsRecovered(versions.first(), filepath); + if (recover && box.clickedButton() == recover) { + file.open(QIODevice::ReadOnly); + QDomDocument document; + QStringList lost; + QString error; + if (!QetContainer::recover(file.readAll(), &document, &lost, &error)) { + QET::QetMessageBox::warning(this, tr("Cannot open %1").arg(info.fileName()), + tr("Nothing can be recovered from it:\n%1").arg(error)); + return false; + } + //Opened from a .qetz written of it, as the project it replaces + QTemporaryDir dir; + const QString read = dir.filePath(info.completeBaseName() + QStringLiteral(".qetz")); + const QList parts = QetContainer::split(document, QString(), &error); + if (!dir.isValid() || parts.isEmpty() || !QetZip::write(read, parts, &error) + || !openAsRecovered(read, filepath)) { + if (!error.isEmpty()) + QET::QetMessageBox::warning(this, tr("Cannot open %1").arg(info.fileName()), error); + return false; + } + if (!lost.isEmpty()) { + QMessageBox done(QMessageBox::Information, tr("Project recovered"), + tr("What could be read of %1 is open as a new project. " + "Some parts could not be read and were left out: see the details.") + .arg(info.fileName()), + QMessageBox::Ok, this); + done.setDetailedText(lost.join(QLatin1Char('\n'))); + done.exec(); + } + return true; + } + return false; +} + +/** + @brief QETDiagramEditor::recoveredPath + @return where a project recovered from @p filepath is saved: beside it, + " (recovered).", numbered if that is taken +*/ +QString QETDiagramEditor::recoveredPath(const QString &filepath, const QString &suffix) +{ + const QFileInfo info(filepath); + QString path = info.absoluteDir().filePath(tr("%1 (recovered)").arg(info.completeBaseName()) + + QLatin1Char('.') + suffix); + for (int n = 2 ; QFileInfo::exists(path) ; ++n) + path = info.absoluteDir().filePath(tr("%1 (recovered %2)").arg(info.completeBaseName()).arg(n) + + QLatin1Char('.') + suffix); + return path; +} + +/** + @brief QETDiagramEditor::openAsRecovered + Open @p source as a new project saved beside @p original, of its type + (recoveredPath()), not saved yet. + @return true if it opened +*/ +bool QETDiagramEditor::openAsRecovered(const QString &source, const QString &original) +{ + QETProject *project = new QETProject(source); + if (project->state() != QETProject::Ok) { + QET::QetMessageBox::warning(this, tr("Cannot open %1").arg(QFileInfo(source).fileName()), + project->openError()); + delete project; + return false; + } + project->setFilePath(recoveredPath(original, QFileInfo(original).suffix())); + project->setModified(true); + addProject(project); + return true; +} + /** Ouvre un projet depuis un fichier et l'ajoute a cet editeur @param filepath Chemin du projet a ouvrir @@ -1979,7 +2093,17 @@ bool QETDiagramEditor::openAndAddProject( QETProject *project = new QETProject(filepath); if (project -> state() != QETProject::Ok) { - if (interactive && project -> state() != QETProject::FileOpenDiscard) + //A damaged project: its previous version or what can be read + //of it, as a new project; the file itself is left as it is + if (interactive && project->state() == QETProject::XmlParsingFailed + && offerToRecover(filepath, project->openError())) + { + delete project; + DialogWaiting::dropInstance(); + return(true); + } + if (interactive && project -> state() != QETProject::FileOpenDiscard + && project -> state() != QETProject::XmlParsingFailed) { QET::QetMessageBox::warning( this, diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index c8ff27793..41a8a365f 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -86,6 +86,9 @@ class QETDiagramEditor : public QETMainWindow protected: bool event(QEvent *) override; private: + bool offerToRecover(const QString &filepath, const QString &reason); + bool openAsRecovered(const QString &source, const QString &original); + static QString recoveredPath(const QString &filepath, const QString &suffix); // Declared first so it is initialised before any member whose // constructor may dispatch a Qt event calling event() (e.g. the // QActionGroup members below trigger QObject::setParent events). diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 8f04cca20..33f164bca 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -515,6 +515,7 @@ QETProject::ProjectState QETProject::openFile(QFile *file) if (!read) { qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error; + m_open_error = read_error; m_container_pictures.clear(); m_folio_parts.clear(); if(opened_here) { @@ -603,6 +604,16 @@ QETProject::ProjectState QETProject::state() const return(m_state); } +/** + @brief QETProject::openError + @return why the file could not be read, when state() is + XmlParsingFailed: "picture ... is missing", say; empty otherwise +*/ +QString QETProject::openError() const +{ + return m_open_error; +} + /** @return la liste des schemas de ce projet */ diff --git a/sources/qetproject.h b/sources/qetproject.h index e2c7f0f9d..05603ca42 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -123,6 +123,7 @@ class QETProject : public QObject QUuid derivedItemUuid(const QString &kind, const QString &key); QByteArray containerPicture(const QString &file) const; ProjectState state() const; + QString openError() const; QList diagrams() const; //The folios whether their items are built or not //(lazy folios); diagrams() builds them @@ -584,6 +585,8 @@ class QETProject : public QObject QFuture m_backup_future; /// Crash-recovery snapshots, written in turn by writeBackup() std::array m_backup_files; + /// Why the file could not be read, see openError() + QString m_open_error; int m_next_backup_slot = 0; QUuid m_uuid = QUuid::createUuid(); QHash m_derived_uuid_keys;