3 Commits

Author SHA1 Message Date
librelad
aa44e0b542 feat(setup): Import step — bring apps in from .lpapp files by path
Adds the wizard step for importing existing apps, so the common case is
answerable in the WebUI rather than only from a terminal.

Path-based, not upload, and that is the design rather than a shortcut. A
.lpapp is a plain tarball and the file is already on the server, so
nothing secret crosses into the browser — which is exactly why this can
live in the WebUI when the encrypted-repository restore cannot (§4.1).
Accepts a single file or a folder of them.

Check first, then accept: the step enqueues `app import-check --publish`,
polls the document it writes, and renders one row per file with its
verdict — ready, a warning (its old storage location is gone, so it will
land on the default), or a refusal (already installed, no longer shipped,
will not fit). Refused rows are shown greyed with the reason rather than
hidden, and cannot be selected.

setupApplyConfig re-runs appImport's own checks rather than trusting the
payload: the machine can change between the check and the apply, and the
list arrives from a browser.

The backend route shell-quotes the path — it reaches a command line and
is user input.

Verified: the step renders as "Step 6 of 7", and the underlying check was
proven against real .lpapp files (correct app name from the tar, size
from the manifest, warning for a missing storage location, refusals for
an already-installed app and a non-export).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 10:10:13 +01:00
librelad
da0136149b feat(app): import-check — inspect an export without importing it
`libreportal app import-check <file-or-directory>` reports what an import
would do, as one JSON object per file, without touching anything. It runs
the same checks appImport makes — app still shipped, not already
installed, fits on the target, storage location still exists — so a UI can
show them and ask for acceptance before acting.

This is what makes a path-based import safe to drive from the WebUI when
the repository restore is not. A .lpapp is a plain tarball, not encrypted,
so there is no password to collect and nothing secret crosses from the
browser to the host — the blocker recorded in first-run-restore.md §4.1
simply does not apply.

Accepts a single file or a directory of them, so "point at this folder"
works as well as "point at this file".

Verified against real .lpapp files built for the purpose: app name read
from the tar's top-level directory rather than the filename, size read
from the manifest, a location the machine does not have downgraded to a
warning naming the fallback, and refusals for an already-installed app
and a file that is not an export at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 10:04:32 +01:00
librelad
ef60cce98a feat(app): portable .lpapp export and import — phase 4
`libreportal app export <app>` writes one app to a single file;
`libreportal app import <file>` installs it here. This is the thing the
original request described as "upload or navigate to the backup file" — a
restic repository is not a file, but the want behind the phrasing is real.

The format is deliberately boring: gzipped tar of the app directory with
its .libreportal-manifest.json at the root. That manifest already records
size, images, volumes, databases and storage location, so import reuses
the phase-3 checks for free — refusing an app this version no longer
ships, or one that will not fit, before unpacking anything.

Export stops the app first. A tar of a running Postgres is a corrupt
Postgres, and a file that looks fine until you restore it is worse than a
refusal. tar runs as the owning user with --numeric-owner so container
sub-UIDs survive the round trip instead of being remapped through this
machine's /etc/passwd.

Import re-runs the normal install pipeline after unpacking, because the
compose still carries the SOURCE machine's ports, IPs and domains — that
pipeline is what re-allocates them here, and migrateUrlRewrite fixes the
host-bound CFG_* fields.

Documented throughout as a courier format, not a backup: no history, no
retention, no encryption. Importing under a different name is refused
outright rather than half-working — the CFG_<APP>_* namespace and compose
identities would all need rewriting, and `instance create` already
answers "a second copy".

Fixes a bug this surfaced: _appDirIntended did an indirect expansion on
CFG_<SLUG>_STORAGE without checking <SLUG> can be a variable name, so a
hyphenated or mistyped app name emitted "invalid variable name" and then
reported the misleading "storage location is not mounted" for an app that
simply did not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 09:08:41 +01:00