* @brief Write a snapshot of the current ProjectModel state; returns the absolute path (or the * existing path if content is byte-identical to the previous snapshot). */
| 113 | * existing path if content is byte-identical to the previous snapshot). |
| 114 | */ |
| 115 | QString Misc::BackupManager::snapshot(const QString& label) |
| 116 | { |
| 117 | if (!m_enabled) |
| 118 | return {}; |
| 119 | |
| 120 | auto& pm = DataModel::ProjectModel::instance(); |
| 121 | const auto stem = currentProjectStem(); |
| 122 | const auto dir = resolveBackupDir(stem); |
| 123 | if (dir.isEmpty()) |
| 124 | return {}; |
| 125 | |
| 126 | auto serialised = pm.serializeToJson(); |
| 127 | if (serialised.isEmpty()) |
| 128 | return {}; |
| 129 | |
| 130 | const auto payloadBytes = QJsonDocument(serialised).toJson(QJsonDocument::Compact); |
| 131 | const auto newHash = QCryptographicHash::hash(payloadBytes, QCryptographicHash::Sha1); |
| 132 | |
| 133 | QJsonObject meta; |
| 134 | meta.insert(QStringLiteral("format"), kSnapshotFormat); |
| 135 | meta.insert(QStringLiteral("takenAt"), QDateTime::currentDateTimeUtc().toString(Qt::ISODate)); |
| 136 | meta.insert(QStringLiteral("label"), label); |
| 137 | meta.insert(QStringLiteral("sha1"), QString::fromLatin1(newHash.toHex())); |
| 138 | serialised.insert(kBackupMetaKey, meta); |
| 139 | |
| 140 | const auto bytes = QJsonDocument(serialised).toJson(QJsonDocument::Compact); |
| 141 | |
| 142 | if (m_lastContentHash.isEmpty()) |
| 143 | seedDedupFromNewest(dir); |
| 144 | |
| 145 | if (!m_lastContentHash.isEmpty() && newHash == m_lastContentHash) |
| 146 | return m_lastSnapshotPath; |
| 147 | |
| 148 | QString fileName = fsSafeIsoTimestamp(); |
| 149 | if (!label.isEmpty()) { |
| 150 | const auto cleanLabel = sanitiseLabel(label); |
| 151 | if (!cleanLabel.isEmpty()) |
| 152 | fileName += QStringLiteral("__") + cleanLabel; |
| 153 | } |
| 154 | fileName += QStringLiteral(".ssproj"); |
| 155 | |
| 156 | const auto fullPath = QDir(dir).filePath(fileName); |
| 157 | QSaveFile out(fullPath); |
| 158 | if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) |
| 159 | return {}; |
| 160 | |
| 161 | if (out.write(bytes) != bytes.size() || !out.commit()) |
| 162 | return {}; |
| 163 | |
| 164 | m_lastSnapshotPath = fullPath; |
| 165 | m_lastContentHash = newHash; |
| 166 | enforceRetention(dir); |
| 167 | Q_EMIT snapshotTaken(fullPath); |
| 168 | return fullPath; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * @brief Load a snapshot file back into ProjectModel; returns true on success. |
nothing calls this directly
no test coverage detected