Add native site restore with a two-step, no-coordination account match #558
No reviewers
Labels
No labels
antville.org
bug
compatibility
dependency
duplicate
enhancement
help wanted
invalid
java
javascript
major
needs feedback
needs work
no-issue-activity
runtime
security
urgent
usability
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
antville/antville!558
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "native-site-restore"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stacked on #557 — Phase 1 of the importer rewrite:
Importer.restoreSitefor the native Antville export format (site fields, skins, stories, comments, images, files), plus the cross-instance account-matching design and the two-step preview/confirm import flow.What's here
index.json), and a newaccounts.jsonwith one deduped row per referenced account ({id, name, email_hmac}— HMAC-SHA256 of the account's e-mail, never the plaintext). Also fixesskins.jsonnever having joinedcreator_name/modifier_nameat all.Importer.preview: extracts the zip once, classifies every referenced account as resolved (same username on target + matching e-mail HMAC), ambiguous (same username, e-mail doesn't match — likely a coincidental namesake), or unresolved (no account under that name at all). Nothing is written yet.import_action: uploading a native export shows a review screen (#import_review) before anything happens; confirming merges any manual overrides (by username — there's no e-mail-lookup API to resolve a typed address against) and queues the real restore. The Blogger/compat path is unchanged (still a single immediate queue, no matching to review).Importer.restoreSite: site fields (denylisting transient metadata keys likejob/export/importErrorso they never get copied from source to target), skins, stories, comments (topologically ordered, dangling parents skipped with a warning), images and files (viaMimePartfrom the extractedstatic/tree, following the existingLayout.prototype.import_actionprecedent).Userper unique unmatched name (status: User.BLOCKED, no password — confirmedUser.addneeds neither), not a shared dummy or a null creator.Why the crypto design
A pre-shared secret across installs was considered and rejected — there's no legitimate way to hand one to an anonymous self-installer or a stranger migrating their own instance into antville.org without requiring coordination, which defeats the point. The per-export key travels inside the export itself and is only ever compared against the target's own account table, so matching needs zero coordination in either direction, at the cost of only defending against blind/automated e-mail harvesting rather than a targeted attacker who already has a specific username from the same file (accepted, since
creator_nameis already in the clear everywhere else in the export).Verified end-to-end on a disposable local test instance
Built a throwaway Helma+Antville+H2 environment (
tools/antclick, an existing self-contained build I hadn't known about going in) specifically to actually run this rather than just read it — see~/.claude/plans/kind-sauteeing-goblet.mdfor the setup. Found and fixed four real bugs in the process (see the follow-up commit for detail): an HMAC comparison that silently never matched (java.lang.Stringvs native JS string under===), ares.handlers.siteNPE because restore runs via a background cron job with no real HTTP request behind it, image/file metadata fields read from the wrong place (row.metadata.X, notrow.X), and aFile.removecall on a same-request-created object that Helma's transaction model doesn't support.After those fixes, verified working: full site restore (fields, threaded comments incl. a reply nested under a comment not the story, byte-identical images/files, correct
adminattribution) into both a fresh empty target site and back into the source site itself (restore-in-place), the review screen correctly showing automatic account matches, and the corrupt-file error path surfacingsite.importErrorcleanly instead of failing silently.Known gaps
Test plan
site.importErrorinstead of failing silentlyUpdate: found + fixed one more bug during Phase 3 testing
Exporting and re-importing an account owning many sites (see PR #560) exposed that
image.setMetadata(row.metadata)/file.setMetadata(row.metadata)were re-applying the export's oldfileName/contentType/etc. right afterImage.add/File.add's ownupdate()had already correctly regenerated those exact fields from the actual re-uploaded bytes — silently overwriting the fresh, correct value with one that doesn't exist on disk under the new site, so every restored image/file 404s. Fixed and separately verified (clean site export/import with fresh data, to avoid the already-corrupted metadata that testing itself produced in earlier runs): DB metadata and on-disk filename now correctly agree, and both are byte-identical to the source, confirmed viacurlthrough the running app, not just direct filesystem inspection this time.The rejection branch fell through to render in the same request instead of redirecting like the two branches above it already do. Two problems from that: import_id didn't reliably clear before the same-request re-render (File node realization timing, same as File.remove's documented constraint), and req.data.file — the raw uploaded MimePart — was still populated for the rest of the request, which site.upload's getFormValue('file') echoed straight into the file_origin field as a bare 'helma.util.MimePart@...' toString. Redirecting avoids both: a fresh GET has neither stale import_id nor leftover request data, and this.importError (already wired up to become param.error further down) carries the message across.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.