跪拜 Guibai
← All articles
Frontend · Backend · JavaScript

A Self-Built Static Demo Publisher That Treats Releases as Pointer Switches

By 勇宝趣学前端 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Teams that run frequent client demos often outgrow free static hosts but don't need a full CMS. A small, self-hosted system with explicit version switching, path-traversal hardening, and automatic expiry removes the operational anxiety of broken links and stale deployments without adding platform dependency.

Summary

Client demos that rely on third-party static hosts introduce friction: platform switching, ads, unclear security boundaries, and links lost in chat history. A self-built system solves this by keeping every project under one admin panel with explicit states — upload, publish, offline, expire, delete. The flow creates a project, uploads a ZIP, extracts it to a temp directory, runs security checks, generates a version directory, and only then confirms publication behind a random-token URL.

Upload and publish are deliberately separated. An upload creates a pending version; an admin confirms it before switching the live pointer. This prevents half-finished pages from reaching a client during a demo and leaves room for version comparison and rollback. On the backend, ZIP extraction is sandboxed with path-normalization checks that reject traversal attempts like `../../application.properties`, plus limits on archive size, extracted size, file count, and allowed extensions. An `index.html` must exist at the root or one subdirectory deep.

Published versions live in independent directories (`v1/`, `v2/`, `v3/`), and the database simply records which version is current. A publish action updates that relationship without touching existing files, so a failed upload never breaks the live demo. Nginx handles HTTPS termination and proxies requests to the backend, with a note that X-Accel-Redirect could offload large static-file transfers later. Expiry and offline logic runs on access checks and scheduled cleanup tasks, preventing the server from becoming a museum of forgotten uploads.

Takeaways
Upload and publish are two separate API calls; an upload creates a pending version, and an explicit publish action switches the live pointer.
ZIP extraction must resolve and normalize every entry path against the temp directory root, rejecting any path that escapes it.
Allowed file extensions are whitelisted to HTML, CSS, JS, JSON, and common image and font formats; an index.html must exist at the root or one level down.
Version directories are never overwritten — each upload produces a new directory, and the database updates a current-version reference.
Project source code and runtime data directories are kept separate so redeploying the app doesn't wipe uploaded demos.
Access checks reject unpublished or expired projects; a scheduled job cleans up expired projects and abandoned temp uploads.
Nginx terminates HTTPS and proxies `/demo/` to the backend; X-Accel-Redirect is flagged as a future optimization for serving large static files.
Conclusions

The two-step upload-then-publish pattern is underused in internal tools. It costs almost nothing to implement and prevents the most common demo disaster: a client seeing a broken page while the developer assumes the deploy succeeded.

Treating a publish action as a database pointer switch rather than a file copy makes rollback trivial and keeps the live version stable even when a new upload fails mid-extraction.

Path-traversal checks are often taught as a security afterthought, but here they are the central design constraint — the system's entire trust model rests on the assumption that ZIP contents are hostile.

Concepts & terms
Path traversal in ZIP extraction
An attack where a ZIP entry name contains sequences like `../` to write files outside the intended extraction directory. Mitigated by resolving the target path and verifying it still starts with the temp directory root.
X-Accel-Redirect
An Nginx feature that lets a backend application instruct Nginx to serve a static file directly, freeing the app thread from reading and transmitting large files.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗