From 7e96bb51d23e8bee9ecff2c8d865b32bb981cba5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 16 Oct 2026 08:23:31 +1300 Subject: [PATCH] Sync the folder after saving, and save from the command line through a temporary file QSaveFile writes a new file, forces it to disk and renames it over the old one, but does not force the folder to disk. On ext4 or XFS a power cut soon after a save can then lose the rename: the previous file comes back, or none. QET::syncFolderOf() now syncs the folder after every project, symbol and title block save (QET::writeXmlText()) and every .qetz save (QetZip::write()). Nothing to do on Windows. --resave and --set-titleblock wrote a .qet straight over the output file (QFile, truncate, write): a run stopped part-way left half a project. They now write through QET::writeXmlText() like the GUI's save. On Windows their .qet now has LF line ends, as the GUI writes. Checked with strace on --resave: fdatasync of the new file, linkat into place, fsync of the folder. The .qet written is byte-identical to before. ctest 106/106. Co-Authored-By: Claude Opus 5.5 --- cmake/qet_compilation_vars.cmake | 1 + sources/cli_export.cpp | 20 +++++------- sources/container/qetzip.cpp | 2 ++ sources/qet.cpp | 2 ++ sources/utils/syncfolder.h | 54 ++++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 sources/utils/syncfolder.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index fcedf5817..516ce5690 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -941,6 +941,7 @@ set(QET_SRC_FILES ${QET_DIR}/sources/utils/qetsettings.h ${QET_DIR}/sources/utils/qetutils.cpp ${QET_DIR}/sources/utils/qetutils.h + ${QET_DIR}/sources/utils/syncfolder.h ${QET_DIR}/sources/materiallist/materialentrydialog.cpp ${QET_DIR}/sources/materiallist/materialentrydialog.h diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index 5c4d5188f..802776cd8 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -885,14 +885,13 @@ int resaveProject(QETProject &project, const QString &output) //what building them would write (QETProject::toXml()) QHash kept_folios; const QDomDocument doc = project.toXml(&kept_folios); - QFile file(output); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - err << "Cannot open '" << output << "' for writing.\n"; + //Through a temporary file, as the GUI saves: a run stopped + //part-way leaves the previous file + QString error; + if (!QET::writeXmlText(QETProject::spliceKeptFolios(doc.toString(4), kept_folios), output, &error)) { + err << "Cannot write '" << output << "': " << error << "\n"; return 1; } - QTextStream fout(&file); - fout << QETProject::spliceKeptFolios(doc.toString(4), kept_folios); - file.close(); out << "Re-saved project -> " << output << "\n"; return 0; } @@ -980,14 +979,11 @@ int setTitleBlock(QETProject &project, const QString &output, //is what building them would write (QETProject::toXml()) QHash kept_folios; const QDomDocument doc = project.toXml(&kept_folios); - QFile file(output); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - err << "Cannot open '" << output << "' for writing.\n"; + QString error; + if (!QET::writeXmlText(QETProject::spliceKeptFolios(doc.toString(4), kept_folios), output, &error)) { + err << "Cannot write '" << output << "': " << error << "\n"; return 1; } - QTextStream fout(&file); - fout << QETProject::spliceKeptFolios(doc.toString(4), kept_folios); - file.close(); } out << "Stamped " << fields.size() << " field(s) on " << folios << " folio(s) -> " << output << "\n"; diff --git a/sources/container/qetzip.cpp b/sources/container/qetzip.cpp index dc9e2c9f7..3e0928905 100644 --- a/sources/container/qetzip.cpp +++ b/sources/container/qetzip.cpp @@ -18,6 +18,7 @@ #include "qetzip.h" #include "miniz/miniz.h" +#include "../utils/syncfolder.h" #include #include @@ -152,6 +153,7 @@ bool QetZip::write(const QString &path, const QList &entries, QString *er setError(error, file.errorString()); return false; } + QET::syncFolderOf(path); return true; } diff --git a/sources/qet.cpp b/sources/qet.cpp index 0f7a1c3af..03a042e89 100644 --- a/sources/qet.cpp +++ b/sources/qet.cpp @@ -19,6 +19,7 @@ #include "qetapp.h" #include "qeticons.h" #include "shortcutmanager.h" +#include "utils/syncfolder.h" #include #include @@ -809,6 +810,7 @@ bool QET::writeXmlText(const QString &text, const QString &filepath, QString *er return false; } + syncFolderOf(filepath); return(true); } diff --git a/sources/utils/syncfolder.h b/sources/utils/syncfolder.h new file mode 100644 index 000000000..d174cd9a1 --- /dev/null +++ b/sources/utils/syncfolder.h @@ -0,0 +1,54 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef SYNCFOLDER_H +#define SYNCFOLDER_H + +#include +#include +#include + +#ifndef Q_OS_WIN +#include +#include +#endif + +namespace QET { + /** + Force to disk the folder holding @p file_path, once + QSaveFile::commit() renamed a new file into it. QSaveFile syncs the + file it wrote but not the folder, and on some file systems (ext4, + XFS) a power cut soon after can then lose the rename: the previous + file comes back, or none. Does nothing on Windows, where a folder + cannot be synced this way. + */ + inline void syncFolderOf(const QString &file_path) + { +#ifndef Q_OS_WIN + const QByteArray folder = QFile::encodeName(QFileInfo(file_path).absolutePath()); + const int fd = ::open(folder.constData(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd >= 0) { + ::fsync(fd); + ::close(fd); + } +#else + Q_UNUSED(file_path) +#endif + } +} + +#endif // SYNCFOLDER_H