v0.2.0 (10/N): bridge:export console command + QML hook
PLAN.md §12 *Data backup / export* called for "a bridge:export console command and a UI hook for backup-to-file from v1". Shipping both now so the v0.2.0 surface has the data-portability story end-to-end: PHP side — `bin/console bridge:export <destination>`: - Reads the source path from DATABASE_URL so it works in dev mode (developer's source-tree var/data.sqlite) and bundled mode (user data dir SQLite) without environment-aware logic. - SQLite-only by design (PLAN.md §6 — single-instance SQLite-first); emits a clear error for non-sqlite:// URLs rather than pretending to support drivers that need driver-specific dump tooling. - Overwrites the destination if it exists (the FileDialog or shell redirect that produced the path has already confirmed). - 4 unit tests: happy path, non-SQLite URL, missing source, overwrite. Test count 24 → 28. QML side — Q_INVOKABLE BackendConnection.exportDatabase(path): - Bundled mode only; dev mode emits databaseExportFailed and returns false (developers own their SQLite directly). - Accepts both filesystem paths and `file://` URLs (FileDialog results). - Returns synchronously with bool but also emits async signals databaseExported(dst) / databaseExportFailed(reason) so QML can drive a snackbar / log without polling the return value. - Removes any existing destination first (QFile::copy refuses to overwrite); the picker has already confirmed the choice. Drive-by: parse_url() rejects sqlite:///abs/path on PHP 8.5+ (the host-less triple-slash trips its strictness). Switched to a prefix-strip — Doctrine DBAL only emits two URL shapes for SQLite anyway (sqlite:///abs and sqlite://relative). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -472,6 +472,46 @@ QStringList BackendConnection::childLogTail() const
|
||||
return out;
|
||||
}
|
||||
|
||||
bool BackendConnection::exportDatabase(const QString& destination)
|
||||
{
|
||||
if (m_mode != Mode::Bundled) {
|
||||
emit databaseExportFailed(QStringLiteral("dev mode: copy var/data.sqlite directly"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString src = m_dataDir + QStringLiteral("/var/data.sqlite");
|
||||
if (!QFileInfo::exists(src)) {
|
||||
const QString msg = QStringLiteral("source not found: %1 (no data yet?)").arg(src);
|
||||
emit databaseExportFailed(msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// QML's FileDialog returns `file://` URLs; unwrap to a local path.
|
||||
QString dst = destination;
|
||||
if (dst.startsWith(QStringLiteral("file://"))) {
|
||||
dst = QUrl(destination).toLocalFile();
|
||||
}
|
||||
if (dst.isEmpty()) {
|
||||
emit databaseExportFailed(QStringLiteral("destination path is empty"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// QFile::copy refuses to overwrite. The FileDialog the user picked
|
||||
// through has already confirmed any overwrite, so unlink first.
|
||||
if (QFileInfo::exists(dst) && !QFile::remove(dst)) {
|
||||
emit databaseExportFailed(QStringLiteral("could not remove existing destination: ") + dst);
|
||||
return false;
|
||||
}
|
||||
if (!QFile::copy(src, dst)) {
|
||||
emit databaseExportFailed(QStringLiteral("copy failed: ") + src + QStringLiteral(" → ") + dst);
|
||||
return false;
|
||||
}
|
||||
|
||||
qCInfo(lcBundled) << "exportDatabase:" << src << "→" << dst;
|
||||
emit databaseExported(dst);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendConnection::onChildFinished(int exitCode, QProcess::ExitStatus status)
|
||||
{
|
||||
Q_UNUSED(status);
|
||||
|
||||
@@ -85,6 +85,15 @@ public:
|
||||
/// mode. Used by `DevConsole.qml` to seed its view on first show.
|
||||
Q_INVOKABLE QStringList childLogTail() const;
|
||||
|
||||
/// Bundled mode: copy `var/data.sqlite` to a user-chosen path
|
||||
/// (typically a `Qt.labs.platform.FileDialog` result). Returns
|
||||
/// true on success; on failure emits `databaseExportFailed` with
|
||||
/// a human-readable reason. Use for an "Export my data" menu
|
||||
/// item paired with the `bridge:export` console command for
|
||||
/// CLI parity. Dev mode: returns false (developers own their
|
||||
/// var/data.sqlite lifecycle directly).
|
||||
Q_INVOKABLE bool exportDatabase(const QString& destination);
|
||||
|
||||
signals:
|
||||
void urlChanged();
|
||||
void tokenChanged();
|
||||
@@ -101,6 +110,9 @@ signals:
|
||||
void updateApplied();
|
||||
void updateApplyFailed(const QString& reason);
|
||||
|
||||
void databaseExported(const QString& destination);
|
||||
void databaseExportFailed(const QString& reason);
|
||||
|
||||
/// Emitted for each newline-terminated chunk read from the bundled
|
||||
/// FrankenPHP child's merged stdout+stderr stream. DevConsole.qml
|
||||
/// listens for these to populate its log view live.
|
||||
|
||||
Reference in New Issue
Block a user