Files
Ampandheaust d930d8172f Serve sculpt studio at /sculpt
Keep existing viewer URLs compatible and test embedded pages, assets, and isolation headers.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-09-28 10:25:02 +00:00

263 lines
8.6 KiB
Go

package main
import (
"bytes"
"database/sql"
"embed"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
)
//go:embed web
var webFiles embed.FS
type application struct {
store *playgroundStore
packages map[string]memoryAsset
monaco http.Handler
assets http.Handler
pages map[string][]byte
}
func newApplication(store *playgroundStore, packages map[string]memoryAsset, monacoDirectory string) (*application, error) {
if info, err := os.Stat(monacoDirectory); err != nil || !info.IsDir() {
return nil, fmt.Errorf("Monaco assets not found at %s; run npm install", monacoDirectory)
}
assets, err := fs.Sub(webFiles, "web/assets")
if err != nil {
return nil, err
}
pages := make(map[string][]byte)
for _, name := range []string{"home.html", "docs.html", "playground.html", "gltf.html", "assets/sculpt/index.html"} {
content, err := webFiles.ReadFile("web/" + name)
if err != nil {
return nil, err
}
if name == "home.html" {
logMessage, err := json.Marshal(deploymentLogMessage())
if err != nil {
return nil, err
}
content = bytes.Replace(content, []byte("__YAWN_DEPLOYMENT_LOG__"), logMessage, 1)
}
pages[name] = content
}
return &application{
store: store,
packages: packages,
monaco: http.StripPrefix("/assets/monaco/", http.FileServer(http.Dir(monacoDirectory))),
assets: http.StripPrefix("/assets/", http.FileServer(http.FS(assets))),
pages: pages,
}, nil
}
func deploymentLogMessage() string {
deployedAt := "unknown"
if value := os.Getenv("YAWN_DEPLOYED_AT"); value != "" {
deployedAt = value
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
ist := time.FixedZone("IST", 5*60*60+30*60)
deployedAt = parsed.In(ist).Format("2 January 2006, 03:04:05 PM MST")
}
}
commitMessage := strings.TrimSpace(os.Getenv("YAWN_DEPLOY_COMMIT_MESSAGE"))
if commitMessage == "" {
commitMessage = "unknown"
}
return fmt.Sprintf("Yawn deployed at %s\nLatest commit: %s", deployedAt, commitMessage)
}
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", app.health)
mux.HandleFunc("/pkg/", app.servePackage)
mux.HandleFunc("/api/playgrounds", app.savePlayground)
mux.HandleFunc("/api/playgrounds/", app.getPlayground)
mux.Handle("/assets/monaco/", app.monaco)
mux.Handle("/assets/", app.assets)
mux.HandleFunc("/docs", app.servePage("docs.html"))
mux.HandleFunc("/docs/", app.servePage("docs.html"))
mux.HandleFunc("/playground", app.servePlayground)
mux.HandleFunc("/playground/", app.servePlayground)
mux.HandleFunc("/view/gltf", app.servePage("gltf.html"))
mux.HandleFunc("/view/gltf/", app.servePage("gltf.html"))
mux.HandleFunc("/sculpt", app.servePage("assets/sculpt/index.html"))
mux.HandleFunc("/sculpt/", app.servePage("assets/sculpt/index.html"))
mux.HandleFunc("/view/sculpt", app.servePage("assets/sculpt/index.html"))
mux.HandleFunc("/view/sculpt/", app.servePage("assets/sculpt/index.html"))
mux.HandleFunc("/", app.serveHome)
return app.headers(mux)
}
func (app *application) health(response http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodGet && request.Method != http.MethodHead {
response.Header().Set("Allow", "GET, HEAD")
http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
return
}
response.WriteHeader(http.StatusNoContent)
}
func (app *application) headers(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
response.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
response.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
response.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
response.Header().Set("X-Content-Type-Options", "nosniff")
response.Header().Set("X-Frame-Options", "SAMEORIGIN")
next.ServeHTTP(response, request)
})
}
func (app *application) serveHome(response http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/" {
http.NotFound(response, request)
return
}
app.writePage(response, request, "home.html")
}
func (app *application) servePage(name string) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
app.writePage(response, request, name)
}
}
func (app *application) servePlayground(response http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/playground" && request.URL.Path != "/playground/" {
parts := strings.Split(strings.Trim(request.URL.Path, "/"), "/")
if len(parts) != 3 || parts[0] != "playground" || !playgroundIDPattern.MatchString(parts[1]) {
http.NotFound(response, request)
return
}
if revision, err := strconv.Atoi(parts[2]); err != nil || revision < 1 {
http.NotFound(response, request)
return
}
}
app.writePage(response, request, "playground.html")
}
func (app *application) writePage(response http.ResponseWriter, request *http.Request, name string) {
response.Header().Set("Content-Type", "text/html; charset=utf-8")
response.Header().Set("Cache-Control", "no-cache")
if request.Method == http.MethodHead {
return
}
if request.Method != http.MethodGet {
response.Header().Set("Allow", "GET, HEAD")
http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
return
}
_, _ = response.Write(app.pages[name])
}
func (app *application) servePackage(response http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodGet && request.Method != http.MethodHead {
response.Header().Set("Allow", "GET, HEAD")
http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
return
}
name := strings.TrimPrefix(path.Clean(request.URL.Path), "/pkg/")
asset, exists := app.packages[name]
if !exists || name == "." {
http.NotFound(response, request)
return
}
response.Header().Set("Access-Control-Allow-Origin", "*")
response.Header().Set("Cache-Control", "no-cache")
response.Header().Set("Content-Type", asset.contentType)
response.Header().Set("Cross-Origin-Resource-Policy", "cross-origin")
response.Header().Set("ETag", asset.etag)
if request.Header.Get("If-None-Match") == asset.etag {
response.WriteHeader(http.StatusNotModified)
return
}
http.ServeContent(response, request, name, asset.modified, bytes.NewReader(asset.content))
}
func (app *application) savePlayground(response http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost {
response.Header().Set("Allow", "POST")
http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
return
}
request.Body = http.MaxBytesReader(
response,
request.Body,
maximumCodeBytes+maximumLoadoutsBytes+4096,
)
var input struct {
ID string `json:"id"`
Title string `json:"title"`
Code string `json:"code"`
Loadouts json.RawMessage `json:"loadouts"`
}
decoder := json.NewDecoder(request.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&input); err != nil {
writeJSONError(response, "The save request is not valid.", http.StatusBadRequest)
return
}
result, err := app.store.save(
request.Context(),
input.ID,
strings.TrimSpace(input.Title),
input.Code,
string(input.Loadouts),
)
if err != nil {
writeJSONError(response, err.Error(), http.StatusBadRequest)
return
}
writeJSON(response, result, http.StatusCreated)
}
func (app *application) getPlayground(response http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodGet {
response.Header().Set("Allow", "GET")
http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
return
}
parts := strings.Split(strings.TrimPrefix(request.URL.Path, "/api/playgrounds/"), "/")
if len(parts) != 2 {
http.NotFound(response, request)
return
}
revision, err := strconv.Atoi(parts[1])
if err != nil {
http.NotFound(response, request)
return
}
result, err := app.store.get(request.Context(), parts[0], revision)
if errors.Is(err, sql.ErrNoRows) {
writeJSONError(response, "Playground not found.", http.StatusNotFound)
return
}
if err != nil {
writeJSONError(response, "Could not load the playground.", http.StatusInternalServerError)
return
}
response.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
writeJSON(response, result, http.StatusOK)
}
func writeJSON(response http.ResponseWriter, value any, status int) {
response.Header().Set("Content-Type", "application/json; charset=utf-8")
response.WriteHeader(status)
_ = json.NewEncoder(response).Encode(value)
}
func writeJSONError(response http.ResponseWriter, message string, status int) {
writeJSON(response, map[string]string{"error": message}, status)
}