Ask before saving over a project file someone else changed

Saving replaced the project file whatever had happened to it since it was
opened. If another user, another program or a second QET had saved the same
file in between (a shared drive, a copy synced from another PC), their work
was lost without a word.

QETProject now remembers the project file's size and modification time when
it reads it and after each save of its own. Before writing, write() compares
them with the file on disk and, if they differ, writes nothing and returns
"someone else changed the file on disk at <time> since it was opened".

- Interactive save (Ctrl+S, and the save on closing a project) asks:
  "Someone else changed <file> on disk at <time> since you opened it" with
  Save as a copy (opens Save As), Overwrite, or Cancel.
- Scripts (--run), the command line, MCP and the autosave timer have no one
  to ask, so they keep the refusal (autosave logs it). Setting
  QET_OVERWRITE_CHANGED=1 makes a script overwrite on purpose.
- A file that does not exist yet, and a Save As to another name, are not a
  conflict; after Save As the remembered state is the new file's.

No change to what is written, so .qet and .qetz stay as they were.

Evidence: new tst_changedondisk (unchanged file saves twice; a file written
behind the open project is refused; the override overwrites; a Save As
target is no conflict) fails on the changed-file case without the source
change and passes with it. Full ctest 107/107. Physical check with
qet-gui-dialog.sh: file modified behind the open project, Ctrl+S shows the
three-button dialog.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgPFANhWgD66V3v9KVKV58
This commit is contained in:
ispyisail
2026-10-16 12:17:21 +13:00
parent c4b5cf2f11
commit e8a5d6aec0
6 changed files with 257 additions and 5 deletions
+77 -2
View File
@@ -386,7 +386,13 @@ void QETProject::init()
connect(&m_autosave_timer, &QTimer::timeout, this, [this]()
{
if(!this->m_file_path.isEmpty())
this->write();
{
//No one is there to ask: a file changed behind this
//QET is left alone
const QETResult result = this->write();
if (!result.isOk())
qWarning() << "autosave:" << result.errorMessage();
}
});
m_autosave_timer.start();
}
@@ -471,6 +477,7 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
//Extract the content of the xml. The raw bytes are kept : a project
//file without a persisted uuid derives its uuid from them.
const QByteArray content = file->readAll();
rememberDiskState();
//Each folio's text is kept while it is not built (readDiagramsXml()):
//from a .qet only, a .qetz is a zip of parts
if (!content.startsWith("PK\x03\x04"))
@@ -2723,7 +2730,64 @@ QETResult QETProject::writeCopyForOlderVersions(const QString &path)
return(QETResult());
}
QETResult QETProject::write()
/**
@brief QETProject::rememberDiskState
Remember the size and modification time of the project file, as this QET
has just read or written it. changedOnDisk() compares against them.
*/
void QETProject::rememberDiskState()
{
const QFileInfo info(m_file_path);
m_disk_path = m_file_path;
m_disk_size = info.exists() ? info.size() : -1;
m_disk_mtime = info.exists() ? info.lastModified() : QDateTime();
}
/**
@brief QETProject::writeTarget
@return the file write() writes to: the .qetz beside a project read
from a .qet, else the project's own file
*/
QString QETProject::writeTarget() const
{
return m_opened_from_qet ? besideContainerPath() : m_file_path;
}
/**
@brief QETProject::changedOnDisk
@param when set to the modification time of the file now on disk
@return true if the file write() would replace is not the one this QET
last read or wrote: another user, another program or another QET saved
it since. A file that does not exist yet, or one this QET never read
or wrote (Save As to another name), is not a change.
*/
bool QETProject::changedOnDisk(QDateTime *when) const
{
const QString target = writeTarget();
if (target.isEmpty() || target != m_disk_path || m_disk_size < 0)
return false;
const QFileInfo info(target);
if (!info.exists())
return false;
if (info.size() == m_disk_size && info.lastModified() == m_disk_mtime)
return false;
if (when)
*when = info.lastModified();
return true;
}
/**
@brief QETProject::write
Save the project in a file.
If the file was changed on disk since this QET read or wrote it, nothing
is written and the result says so: the interactive save asks the user
first (ProjectView) and calls again with @p overwrite_changed; scripts,
the command line, autosave and MCP have no one to ask and keep the
refusal. Setting QET_OVERWRITE_CHANGED=1 in the environment overwrites
without asking, for a script that means to.
@param overwrite_changed write even if the file changed on disk
*/
QETResult QETProject::write(bool overwrite_changed)
{
// this operation requires a filepath
if (m_file_path.isEmpty())
@@ -2759,6 +2823,15 @@ QETResult QETProject::write()
return(QString("the file %1 was opened read-only and thus will not be written").arg(m_file_path));
}
if (!overwrite_changed && qEnvironmentVariableIntValue("QET_OVERWRITE_CHANGED") == 0) {
QDateTime when;
if (changedOnDisk(&when))
return(failed(tr("someone else changed the file on disk at %1 since it was opened, "
"and saving would overwrite that. Save to another name, or "
"set QET_OVERWRITE_CHANGED=1 to overwrite it.")
.arg(QLocale::system().toString(when, QLocale::ShortFormat))));
}
QString error_message;
if (isContainerPath(m_file_path))
{
@@ -2778,6 +2851,8 @@ QETResult QETProject::write()
return(failed(error_message));
}
rememberDiskState();
// The project has just been written to a writable file (e.g. saved to
// a new location with "Save As"), so it is no longer read-only.
if (isReadOnly())