Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/volumen/internal/diff"
|
||||
)
|
||||
|
||||
// unsafeSlugRe strips anything that is not safe in a header value.
|
||||
var unsafeSlugRe = regexp.MustCompile(`[^a-z0-9._-]`)
|
||||
|
||||
// attachmentName reduces a path segment to a safe Content-Disposition
|
||||
// filename.
|
||||
func attachmentName(value string) string { return unsafeSlugRe.ReplaceAllString(value, "") }
|
||||
|
||||
func (a *Admin) handleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
slug := r.PathValue("slug")
|
||||
p := a.deps.Store.Find(slug, "")
|
||||
if p == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
content, err := p.ToFile()
|
||||
if err != nil {
|
||||
http.Error(w, "export failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition",
|
||||
fmt.Sprintf(`attachment; filename="%s.md"`, attachmentName(slug)))
|
||||
fmt.Fprint(w, content)
|
||||
}
|
||||
|
||||
func (a *Admin) handleHistory(w http.ResponseWriter, r *http.Request) {
|
||||
slug := r.PathValue("slug")
|
||||
p := a.deps.Store.Find(slug, "")
|
||||
if p == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
revisions := a.deps.Store.Revisions(slug)
|
||||
rows := make([]revisionRow, 0, len(revisions))
|
||||
for _, rev := range revisions {
|
||||
rows = append(rows, newRevisionRow(rev))
|
||||
}
|
||||
heading := p.Title()
|
||||
if heading == "" {
|
||||
heading = slug
|
||||
}
|
||||
data := a.pageData(r)
|
||||
data.Slug = slug
|
||||
data.Heading = heading
|
||||
data.Revisions = rows
|
||||
data.Post = newEditorPost(p)
|
||||
data.Crumbs = []Crumb{
|
||||
{Label: "Posts", Href: "/admin/", UI: true},
|
||||
{Label: heading, Href: "/admin/posts/" + slug + "/edit"},
|
||||
{Label: "History", IsLast: true, UI: true},
|
||||
}
|
||||
a.renderPage(w, r, "history.html", data, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *Admin) handleHistoryDownload(w http.ResponseWriter, r *http.Request) {
|
||||
slug := r.PathValue("slug")
|
||||
name := r.PathValue("name")
|
||||
content := a.deps.Store.RevisionContent(slug, name)
|
||||
if content == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
// The revision name is a server-generated stamp, but it arrives from
|
||||
// the URL: the value is reduced to what cannot end the quoted string
|
||||
// (a quote, a backslash, a control byte). Unlike attachmentName this
|
||||
// keeps the stamp's uppercase T and Z.
|
||||
safeName := strings.Map(func(r rune) rune {
|
||||
if r == '"' || r == '\\' || r < 0x20 || r == 0x7f {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, filepath.Base(name))
|
||||
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition",
|
||||
fmt.Sprintf(`attachment; filename="%s-%s"`, attachmentName(slug), safeName))
|
||||
fmt.Fprint(w, content)
|
||||
}
|
||||
|
||||
// handleHistoryDiff compares one archived revision with the current
|
||||
// content, so the editor can judge what a restore would change before
|
||||
// committing to it.
|
||||
func (a *Admin) handleHistoryDiff(w http.ResponseWriter, r *http.Request) {
|
||||
slug := r.PathValue("slug")
|
||||
name := r.PathValue("name")
|
||||
p := a.deps.Store.Find(slug, "")
|
||||
if p == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
revision := a.deps.Store.RevisionContent(slug, name)
|
||||
if revision == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
current, err := p.ToFile()
|
||||
if err != nil {
|
||||
http.Error(w, "export failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var when string
|
||||
for _, rev := range a.deps.Store.Revisions(slug) {
|
||||
if rev.Name == name {
|
||||
when = rev.When
|
||||
break
|
||||
}
|
||||
}
|
||||
heading := p.Title()
|
||||
if heading == "" {
|
||||
heading = slug
|
||||
}
|
||||
data := a.pageData(r)
|
||||
data.Slug = slug
|
||||
data.Heading = heading
|
||||
data.DiffName = name
|
||||
data.DiffWhen = when
|
||||
data.DiffChunks = diff.Chunks(revision, current, 3)
|
||||
data.Post = newEditorPost(p)
|
||||
data.Crumbs = []Crumb{
|
||||
{Label: "Posts", Href: "/admin/", UI: true},
|
||||
{Label: heading, Href: "/admin/posts/" + slug + "/edit"},
|
||||
{Label: "History", Href: "/admin/posts/" + slug + "/history", UI: true},
|
||||
{Label: "Changes", IsLast: true, UI: true},
|
||||
}
|
||||
a.renderPage(w, r, "diff.html", data, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *Admin) handleHistoryRestore(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireCSRF(w, r) {
|
||||
return
|
||||
}
|
||||
slug := r.PathValue("slug")
|
||||
name := r.PathValue("name")
|
||||
p := a.deps.Store.Find(slug, "")
|
||||
if p == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if a.deps.Store.RestoreRevision(p, name) == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/posts/"+slug+"/edit?restored=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// --- uploads and static SVGs ------------------------------------------------
|
||||
Reference in New Issue
Block a user