396 lines
15 KiB
Python
396 lines
15 KiB
Python
# Generate handlers.go based on the known API routes
|
|||
|
|
handlers = r'''package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"mime"
|
||
|
|
"net/http"
|
||
|
|
"os"
|
||
|
|
"os/exec"
|
||
|
|
"path/filepath"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"write-server/internal/ai"
|
||
|
|
"write-server/internal/artalk"
|
||
|
|
"write-server/internal/config"
|
||
|
|
"write-server/internal/images"
|
||
|
|
"write-server/internal/posts"
|
||
|
|
"write-server/internal/recycle"
|
||
|
|
"write-server/internal/rssapi"
|
||
|
|
"write-server/internal/wechat"
|
||
|
|
)
|
||
|
|
|
||
|
|
const adminHTML = ` + "`" + `<!DOCTYPE html>
|
||
|
|
<html lang="zh-CN">
|
||
|
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||
|
|
<title>WriteApp</title>
|
||
|
|
<style>:root{color-scheme:dark;--bg:#111827;--card:#1f2937;--text:#f3f4f6;--muted:#9ca3af;--accent:#60a5fa;--accent-hover:#93bbfd;--line:#374151;--success:#86efac}*{box-sizing:border-box;margin:0;padding:0}body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px}.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:28px;max-width:420px;width:100%%}h1{font-size:18px;font-weight:700;margin-bottom:4px}.sub{font-size:13px;color:var(--muted);margin-bottom:20px}.row{display:flex;align-items:center;gap:10px;padding:12px 16px;background:rgba(255,255,255,0.03);border-radius:8px;margin-bottom:16px}.dot{width:10px;height:10px;border-radius:50%%;flex-shrink:0;background:var(--success)}.text{font-size:14px;font-weight:500}.ip{font-size:12px;color:var(--muted);margin-top:2px}.actions{display:flex;gap:8px;flex-wrap:wrap}.btn{padding:10px 20px;border-radius:8px;font-size:14px;font-weight:600;border:none;cursor:pointer;transition:all 0.15s;color:#fff;text-decoration:none;display:inline-block}.btn-primary{background:var(--accent)}.btn-primary:hover{background:var(--accent-hover)}.btn-secondary{background:transparent;border:1px solid var(--line);color:var(--text)}.btn-secondary:hover{background:rgba(255,255,255,0.05)}.link-row{margin-top:16px;padding:10px 14px;background:rgba(255,255,255,0.03);border-radius:8px;font-size:13px;color:var(--muted);word-break:break-all}.link-row a{color:var(--accent);text-decoration:none}.footer{margin-top:20px;font-size:11px;color:var(--muted);text-align:center}</style></head>
|
||
|
|
<body><div class="card"><h1>WriteApp</h1><p class="sub">Blog CMS + Telegram Bot — Go Edition</p>
|
||
|
|
<div class="row"><div class="dot"></div><div><div class="text">All Systems Running</div><div class="ip">Port 8016 · LAN: %s</div></div></div>
|
||
|
|
<div class="actions"><a class="btn btn-primary" href="/">Open Editor</a><a class="btn btn-secondary" href="/api/stats" target="_blank">API Stats</a></div>
|
||
|
|
<div class="link-row">Next.js Frontend: <a href="/">/</a></div>
|
||
|
|
<div class="link-row">API: <a href="/api/posts">/api/posts</a> · <a href="/api/stats">/api/stats</a></div>
|
||
|
|
<div class="footer">Single Process · Go 1.26</div></div></body></html>` + "`" + `
|
||
|
|
|
||
|
|
func jsonOK(w http.ResponseWriter, data interface{}) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
json.NewEncoder(w).Encode(data)
|
||
|
|
}
|
||
|
|
|
||
|
|
func jsonErr(w http.ResponseWriter, status int, msg string) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.WriteHeader(status)
|
||
|
|
json.NewEncoder(w).Encode(map[string]string{"error": msg})
|
||
|
|
}
|
||
|
|
|
||
|
|
func readJSON(r *http.Request) (map[string]interface{}, error) {
|
||
|
|
body, _ := io.ReadAll(r.Body)
|
||
|
|
defer r.Body.Close()
|
||
|
|
var data map[string]interface{}
|
||
|
|
err := json.Unmarshal(body, &data)
|
||
|
|
return data, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func gs(m map[string]interface{}, key string) string {
|
||
|
|
if v, ok := m[key]; ok {
|
||
|
|
if s, ok := v.(string); ok { return s }
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func gb(m map[string]interface{}, key string) bool {
|
||
|
|
if v, ok := m[key]; ok {
|
||
|
|
if b, ok := v.(bool); ok { return b }
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func gsa(m map[string]interface{}, key string) []string {
|
||
|
|
if v, ok := m[key]; ok {
|
||
|
|
switch arr := v.(type) {
|
||
|
|
case []interface{}:
|
||
|
|
var result []string
|
||
|
|
for _, item := range arr {
|
||
|
|
if s, ok := item.(string); ok { result = append(result, s) }
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
case []string:
|
||
|
|
return arr
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func handlePosts(w http.ResponseWriter, r *http.Request) {
|
||
|
|
switch r.Method {
|
||
|
|
case "GET":
|
||
|
|
search := r.URL.Query().Get("search")
|
||
|
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||
|
|
if page < 1 { page = 1 }
|
||
|
|
perPage, _ := strconv.Atoi(r.URL.Query().Get("perPage"))
|
||
|
|
if perPage < 1 { perPage = 20 }
|
||
|
|
includeDrafts := r.URL.Query().Get("includeDrafts") != "false"
|
||
|
|
recycle.CleanExpired()
|
||
|
|
list, total, totalPages := posts.ListPosts(search, page, perPage, includeDrafts)
|
||
|
|
totalCount := posts.GetPostCount()
|
||
|
|
jsonOK(w, map[string]interface{}{
|
||
|
|
"posts": list, "total": total, "totalPages": totalPages, "totalCount": totalCount,
|
||
|
|
})
|
||
|
|
case "POST":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
fmRaw, _ := body["frontMatter"].(map[string]interface{})
|
||
|
|
if fmRaw == nil { jsonErr(w, 400, "frontMatter required"); return }
|
||
|
|
fm := posts.FrontMatter{
|
||
|
|
Title: gs(fmRaw, "title"), Date: gs(fmRaw, "date"), Slug: gs(fmRaw, "slug"),
|
||
|
|
Draft: gb(fmRaw, "draft"), Categories: gsa(fmRaw, "categories"),
|
||
|
|
Tags: gsa(fmRaw, "tags"), Author: gs(fmRaw, "author"),
|
||
|
|
Layout: gs(fmRaw, "layout"), AiComment: gs(fmRaw, "ai_comment"),
|
||
|
|
}
|
||
|
|
if fm.Layout == "" { fm.Layout = "post" }
|
||
|
|
if fm.Title == "" || fm.Slug == "" { jsonErr(w, 400, "Title and slug required"); return }
|
||
|
|
dirPath, err := posts.CreatePost(fm, gs(body, "content"))
|
||
|
|
if err != nil { jsonErr(w, 500, err.Error()); return }
|
||
|
|
jsonOK(w, map[string]interface{}{"success": true, "dirPath": dirPath})
|
||
|
|
default:
|
||
|
|
jsonErr(w, 405, "method not allowed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func handlePostBySlug(w http.ResponseWriter, r *http.Request) {
|
||
|
|
slug := extractSlug(r.URL.Path, "/api/posts/")
|
||
|
|
switch r.Method {
|
||
|
|
case "GET":
|
||
|
|
p := posts.GetPost(slug)
|
||
|
|
if p == nil { jsonErr(w, 404, "Post not found"); return }
|
||
|
|
jsonOK(w, p)
|
||
|
|
case "PUT":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
fmRaw, _ := body["frontMatter"].(map[string]interface{})
|
||
|
|
if fmRaw == nil { fmRaw = make(map[string]interface{}) }
|
||
|
|
var contentPtr *string
|
||
|
|
if c, ok := body["content"]; ok {
|
||
|
|
if s, ok := c.(string); ok { contentPtr = &s }
|
||
|
|
}
|
||
|
|
result, err := posts.UpdatePost(slug, fmRaw, contentPtr)
|
||
|
|
if err != nil { jsonErr(w, 404, err.Error()); return }
|
||
|
|
jsonOK(w, map[string]interface{}{"success": true, "dirPath": result.DirPath})
|
||
|
|
case "DELETE":
|
||
|
|
p := posts.GetPost(slug)
|
||
|
|
if p == nil { jsonErr(w, 404, "Post not found"); return }
|
||
|
|
if err := recycle.MoveToRecycle(slug, p.DirPath, p.FrontMatter.Title); err != nil {
|
||
|
|
jsonErr(w, 500, err.Error()); return
|
||
|
|
}
|
||
|
|
jsonOK(w, map[string]interface{}{"success": true})
|
||
|
|
default:
|
||
|
|
jsonErr(w, 405, "method not allowed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func handlePostsBatch(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != "PUT" { jsonErr(w, 405, "method not allowed"); return }
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
slugsArr, _ := body["slugs"].([]interface{})
|
||
|
|
if len(slugsArr) == 0 { jsonErr(w, 400, "slugs array required"); return }
|
||
|
|
action := gs(body, "action")
|
||
|
|
categories := gsa(body, "categories")
|
||
|
|
tags := gsa(body, "tags")
|
||
|
|
draft := gb(body, "draft")
|
||
|
|
var results []string
|
||
|
|
for _, s := range slugsArr {
|
||
|
|
slug := fmt.Sprintf("%v", s)
|
||
|
|
if posts.GetPost(slug) == nil { results = append(results, "Not found: "+slug); continue }
|
||
|
|
updates := make(map[string]interface{})
|
||
|
|
switch action {
|
||
|
|
case "categories": updates["categories"] = categories
|
||
|
|
case "tags": updates["tags"] = tags
|
||
|
|
case "draft": updates["draft"] = draft
|
||
|
|
case "all": updates["categories"] = categories; updates["tags"] = tags; updates["draft"] = draft
|
||
|
|
default: jsonErr(w, 400, "invalid action"); return
|
||
|
|
}
|
||
|
|
posts.UpdatePost(slug, updates, nil)
|
||
|
|
results = append(results, "Updated: "+slug)
|
||
|
|
}
|
||
|
|
jsonOK(w, map[string]interface{}{"results": results})
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleRecycle(w http.ResponseWriter, r *http.Request) {
|
||
|
|
switch r.Method {
|
||
|
|
case "GET":
|
||
|
|
jsonOK(w, map[string]interface{}{"items": recycle.ListRecycle()})
|
||
|
|
case "POST":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
slug := gs(body, "slug")
|
||
|
|
if slug == "" { jsonErr(w, 400, "slug required"); return }
|
||
|
|
if recycle.Restore(slug) { jsonOK(w, map[string]interface{}{"success": true}) } else { jsonErr(w, 404, "not found") }
|
||
|
|
case "DELETE":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
slug := gs(body, "slug")
|
||
|
|
if slug == "" { jsonErr(w, 400, "slug required"); return }
|
||
|
|
if recycle.PermanentDelete(slug) { jsonOK(w, map[string]interface{}{"success": true}) } else { jsonErr(w, 404, "not found") }
|
||
|
|
default:
|
||
|
|
jsonErr(w, 405, "method not allowed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleAI(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return }
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
prompt := gs(body, "prompt")
|
||
|
|
if prompt == "" { jsonErr(w, 400, "prompt required"); return }
|
||
|
|
resp, err := ai.Chat(prompt)
|
||
|
|
if err != nil { jsonErr(w, 500, err.Error()); return }
|
||
|
|
jsonOK(w, map[string]interface{}{"response": resp})
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleImages(w http.ResponseWriter, r *http.Request) {
|
||
|
|
switch r.Method {
|
||
|
|
case "GET":
|
||
|
|
jsonOK(w, map[string]interface{}{"images": images.ScanImages()})
|
||
|
|
case "DELETE":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
filename := gs(body, "filename")
|
||
|
|
if filename == "" { jsonErr(w, 400, "filename required"); return }
|
||
|
|
if images.DeleteImage(filename) { jsonOK(w, map[string]interface{}{"success": true}) } else { jsonErr(w, 404, "not found") }
|
||
|
|
default:
|
||
|
|
jsonErr(w, 405, "method not allowed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleUpload(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return }
|
||
|
|
r.ParseMultipartForm(32 << 20)
|
||
|
|
file, header, err := r.FormFile("file")
|
||
|
|
if err != nil { jsonErr(w, 400, "file required"); return }
|
||
|
|
defer file.Close()
|
||
|
|
ext := filepath.Ext(header.Filename)
|
||
|
|
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
|
||
|
|
destPath := filepath.Join(config.BLOG_ROOT, "static", "uploads", filename)
|
||
|
|
os.MkdirAll(filepath.Dir(destPath), 0755)
|
||
|
|
dst, err := os.Create(destPath)
|
||
|
|
if err != nil { jsonErr(w, 500, err.Error()); return }
|
||
|
|
defer dst.Close()
|
||
|
|
io.Copy(dst, file)
|
||
|
|
url := "/uploads/" + filename
|
||
|
|
jsonOK(w, map[string]interface{}{"url": url, "success": true})
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleHugo(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return }
|
||
|
|
if hugoStarting { jsonOK(w, map[string]interface{}{"running": false, "starting": true}); return }
|
||
|
|
hugoStarting = true
|
||
|
|
defer func() { hugoStarting = false }()
|
||
|
|
exec.Command("taskkill", "/F", "/IM", "hugo.exe").Run()
|
||
|
|
cmd := exec.Command("hugo", "serve", "--port", "1313", "--noHTTPCache")
|
||
|
|
cmd.Dir = config.BLOG_ROOT
|
||
|
|
if err := cmd.Start(); err != nil { jsonErr(w, 500, err.Error()); return }
|
||
|
|
jsonOK(w, map[string]interface{}{"running": true, "url": "http://localhost:1313"})
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleDeploy(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return }
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
title := gs(body, "title")
|
||
|
|
if title == "" { jsonErr(w, 400, "Title required"); return }
|
||
|
|
runGit := func(args ...string) (string, error) {
|
||
|
|
cmd := exec.Command("git", args...)
|
||
|
|
cmd.Dir = config.BLOG_ROOT
|
||
|
|
out, err := cmd.CombinedOutput()
|
||
|
|
return string(out), err
|
||
|
|
}
|
||
|
|
var output string
|
||
|
|
if o, err := runGit("add", "content/post", "static"); err != nil { jsonErr(w, 500, string(o)); return } else { output += o }
|
||
|
|
if _, err := runGit("diff", "--cached", "--quiet"); err != nil {
|
||
|
|
if o, err2 := runGit("commit", "-m", "发布: "+title); err2 != nil { jsonErr(w, 500, string(o)); return } else { output += o }
|
||
|
|
}
|
||
|
|
o, _ := runGit("push", "origin", "main")
|
||
|
|
output += o
|
||
|
|
jsonOK(w, map[string]interface{}{"success": true, "output": output})
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleStats(w http.ResponseWriter, r *http.Request) {
|
||
|
|
postCount := posts.GetPostCount()
|
||
|
|
linkCount := 0; feedCount := 0
|
||
|
|
if links, err := rssapi.GetLinks(true); err == nil { linkCount = len(links) }
|
||
|
|
if feeds, err := rssapi.GetFeeds(); err == nil { feedCount = len(feeds) }
|
||
|
|
jsonOK(w, map[string]interface{}{"posts": postCount, "feeds": feedCount, "links": linkCount})
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleRSSLinks(w http.ResponseWriter, r *http.Request) {
|
||
|
|
switch r.Method {
|
||
|
|
case "GET":
|
||
|
|
all := r.URL.Query().Get("all") == "1"
|
||
|
|
links, err := rssapi.GetLinks(all)
|
||
|
|
if err != nil { jsonErr(w, 500, err.Error()); return }
|
||
|
|
jsonOK(w, map[string]interface{}{"links": links})
|
||
|
|
case "POST":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
url := gs(body, "url"); title := gs(body, "title"); desc := gs(body, "description")
|
||
|
|
if url == "" || title == "" { jsonErr(w, 400, "url and title required"); return }
|
||
|
|
rssapi.AddLink(url, title, desc)
|
||
|
|
jsonOK(w, map[string]interface{}{"success": true})
|
||
|
|
case "PUT":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
id := gs(body, "id")
|
||
|
|
enabled := gb(body, "enabled")
|
||
|
|
rssapi.ToggleLink(id, enabled)
|
||
|
|
jsonOK(w, map[string]interface{}{"success": true})
|
||
|
|
case "DELETE":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
id := gs(body, "id")
|
||
|
|
rssapi.DeleteLink(id)
|
||
|
|
jsonOK(w, map[string]interface{}{"success": true})
|
||
|
|
default:
|
||
|
|
jsonErr(w, 405, "method not allowed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleRSSFeeds(w http.ResponseWriter, r *http.Request) {
|
||
|
|
switch r.Method {
|
||
|
|
case "GET":
|
||
|
|
feeds, err := rssapi.GetFeeds()
|
||
|
|
if err != nil { jsonErr(w, 500, err.Error()); return }
|
||
|
|
jsonOK(w, map[string]interface{}{"feeds": feeds})
|
||
|
|
case "POST":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
url := gs(body, "url"); title := gs(body, "title")
|
||
|
|
if url == "" || title == "" { jsonErr(w, 400, "url and title required"); return }
|
||
|
|
rssapi.AddFeed(url, title)
|
||
|
|
jsonOK(w, map[string]interface{}{"success": true})
|
||
|
|
case "DELETE":
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
id := gs(body, "id")
|
||
|
|
rssapi.DeleteFeed(id)
|
||
|
|
jsonOK(w, map[string]interface{}{"success": true})
|
||
|
|
default:
|
||
|
|
jsonErr(w, 405, "method not allowed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleWechatHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
switch r.Method {
|
||
|
|
case "GET":
|
||
|
|
articles, err := wechat.GetArticles()
|
||
|
|
if err != nil { jsonErr(w, 500, err.Error()); return }
|
||
|
|
jsonOK(w, map[string]interface{}{"articles": articles})
|
||
|
|
case "POST":
|
||
|
|
msg := r.URL.Query().Get("msg")
|
||
|
|
jsonOK(w, map[string]interface{}{"ok": true, "msg": msg})
|
||
|
|
default:
|
||
|
|
jsonErr(w, 405, "method not allowed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleCommentsStats(w http.ResponseWriter, r *http.Request) {
|
||
|
|
stats := artalk.GetStats()
|
||
|
|
jsonOK(w, stats)
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleCommentsCleanup(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return }
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
slug := gs(body, "slug")
|
||
|
|
if slug == "" { jsonErr(w, 400, "slug required"); return }
|
||
|
|
result := artalk.CleanupComments(slug)
|
||
|
|
jsonOK(w, result)
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleCommentsLogin(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return }
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
name := gs(body, "name"); email := gs(body, "email"); password := gs(body, "password")
|
||
|
|
if name == "" || email == "" || password == "" { jsonErr(w, 400, "name, email, password required"); return }
|
||
|
|
result := artalk.Login(name, email, password)
|
||
|
|
jsonOK(w, result)
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleCommentsSub(w http.ResponseWriter, r *http.Request) {
|
||
|
|
slug := extractSlug(r.URL.Path, "/api/comments/")
|
||
|
|
comments := artalk.GetComments(slug)
|
||
|
|
jsonOK(w, comments)
|
||
|
|
}
|
||
|
|
|
||
|
|
func handleComments(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != "POST" { jsonErr(w, 405, "method not allowed"); return }
|
||
|
|
body, _ := readJSON(r)
|
||
|
|
slug := gs(body, "slug"); content := gs(body, "content"); pageKey := gs(body, "page_key")
|
||
|
|
if slug == "" { jsonErr(w, 400, "slug required"); return }
|
||
|
|
result := artalk.PostComment(slug, content, pageKey)
|
||
|
|
jsonOK(w, result)
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractSlug(path, prefix string) string {
|
||
|
|
p := strings.TrimPrefix(path, prefix)
|
||
|
|
p = strings.TrimSuffix(p, "/")
|
||
|
|
return p
|
||
|
|
}
|
||
|
|
'''
|
||
|
|
|
||
|
|
import "time" # needed below
|
||
|
|
|
||
|
|
with open(r"E:\GitHub\blog\write-go\cmd\writeserver\handlers.go", "w", encoding="utf-8") as f:
|
||
|
|
f.write(handlers)
|
||
|
|
|
||
|
|
print("handlers.go written, size:", len(handlers))
|