Files
bruno_server/main.go
2026-02-18 21:10:23 +01:00

335 lines
8.1 KiB
Go

package main
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
)
var safePathTokenRe = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
func safePathToken(s string) string {
s = strings.TrimSpace(s)
s = strings.ReplaceAll(s, string(filepath.Separator), "_")
// In case the client sent URL-like separators.
s = strings.ReplaceAll(s, "/", "_")
s = strings.ReplaceAll(s, "\\", "_")
s = safePathTokenRe.ReplaceAllString(s, "_")
s = strings.Trim(s, "._-")
if s == "" || s == "." {
return ""
}
if len(s) > 128 {
s = s[:128]
}
return s
}
func safeFilename(name string) string {
name = strings.TrimSpace(name)
name = filepath.Base(name)
if name == "" || name == "." || name == ".." {
return ""
}
// Disallow any remaining separators just in case.
name = strings.ReplaceAll(name, string(filepath.Separator), "_")
name = strings.ReplaceAll(name, "/", "_")
name = strings.ReplaceAll(name, "\\", "_")
if name == "" || name == "." || name == ".." {
return ""
}
if len(name) > 255 {
name = name[:255]
}
return name
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8082"
}
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
})
app.Use(recover.New())
app.Use(logger.New())
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "Origin, Content-Type, Accept, Authorization",
AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
}))
// Ensure preflight requests are handled.
app.Options("/*", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
})
app.Get("/health", func(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).SendString("ok")
})
// Serve built SPA from disk (no embedding).
// Expected layout:
// server/static/index.html
// server/static/assets/...
app.Static("/", "./static")
app.Post("/upload", func(c *fiber.Ctx) error {
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid multipart form")
}
user := safePathToken(c.FormValue("user"))
session := safePathToken(c.FormValue("session"))
prop := safePathToken(c.FormValue("prop"))
if user == "" {
return fiber.NewError(fiber.StatusBadRequest, "missing form field: user")
}
if session == "" {
return fiber.NewError(fiber.StatusBadRequest, "missing form field: session")
}
files := form.File["documents"]
if len(files) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "missing files: documents")
}
dir := filepath.Join("docs", user, session)
if prop != "" {
dir = filepath.Join(dir, prop)
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "cannot create upload directory")
}
uploaded := make([]string, 0, len(files))
for _, file := range files {
name := safeFilename(file.Filename)
if name == "" {
return fiber.NewError(fiber.StatusBadRequest, "invalid filename")
}
dst := filepath.Join(dir, name)
if err := c.SaveFile(file, dst); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("cannot save file %s", name))
}
uploaded = append(uploaded, name)
}
return c.JSON(fiber.Map{
"ok": true,
"files": uploaded,
})
})
app.Post("/loadattachments", func(c *fiber.Ctx) error {
// Accept either JSON body { id, session } or form fields.
var body struct {
ID string `json:"id"`
Session string `json:"session"`
Prop string `json:"prop"`
}
_ = c.BodyParser(&body)
id := safePathToken(body.ID)
if id == "" {
id = safePathToken(c.FormValue("id"))
}
session := safePathToken(body.Session)
if session == "" {
session = safePathToken(c.FormValue("session"))
}
prop := safePathToken(body.Prop)
if prop == "" {
prop = safePathToken(c.FormValue("prop"))
}
if id == "" {
return fiber.NewError(fiber.StatusBadRequest, "missing field: id")
}
if session == "" {
return fiber.NewError(fiber.StatusBadRequest, "missing field: session")
}
dir := filepath.Join("docs", id, session)
if prop != "" {
dir = filepath.Join(dir, prop)
}
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return c.JSON([]string{})
}
return fiber.NewError(fiber.StatusInternalServerError, "cannot read attachments directory")
}
files := make([]string, 0, len(entries))
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if name == "" || name == "." {
continue
}
files = append(files, name)
}
return c.JSON(files)
})
app.Post("/loadattachmentslist", func(c *fiber.Ctx) error {
// Returns a JSON object keyed by the actual subfolder name.
// Example: { "salaryCertificate": ["file1.pdf"], "avsCertificate": ["file2.pdf"] }
// It scans docs/<id>/<session>/ and for each subdirectory lists its files.
var body struct {
ID string `json:"id"`
Session string `json:"session"`
}
_ = c.BodyParser(&body)
id := safePathToken(body.ID)
if id == "" {
id = safePathToken(c.FormValue("id"))
}
session := safePathToken(body.Session)
if session == "" {
session = safePathToken(c.FormValue("session"))
}
if id == "" {
return fiber.NewError(fiber.StatusBadRequest, "missing field: id")
}
if session == "" {
return fiber.NewError(fiber.StatusBadRequest, "missing field: session")
}
baseDir := filepath.Join("docs", id, session)
entries, err := os.ReadDir(baseDir)
if err != nil {
if os.IsNotExist(err) {
return c.JSON(fiber.Map{})
}
return fiber.NewError(fiber.StatusInternalServerError, "cannot read session directory")
}
result := make(map[string][]string)
for _, entry := range entries {
if !entry.IsDir() {
continue
}
folderName := entry.Name()
if safePathToken(folderName) == "" {
continue
}
propDir := filepath.Join(baseDir, folderName)
propEntries, err := os.ReadDir(propDir)
if err != nil {
// Ignore unreadable prop folder.
continue
}
files := make([]string, 0, len(propEntries))
for _, pe := range propEntries {
if pe.IsDir() {
continue
}
name := pe.Name()
if name == "" || name == "." {
continue
}
files = append(files, name)
}
result[folderName] = files
}
return c.JSON(result)
})
app.Post("/deleteattachment", func(c *fiber.Ctx) error {
// Accept either JSON body { id, session, filename } or form fields.
var body struct {
ID string `json:"id"`
Session string `json:"session"`
Prop string `json:"prop"`
Filename string `json:"filename"`
}
_ = c.BodyParser(&body)
id := safePathToken(body.ID)
if id == "" {
id = safePathToken(c.FormValue("id"))
}
session := safePathToken(body.Session)
if session == "" {
session = safePathToken(c.FormValue("session"))
}
prop := safePathToken(body.Prop)
if prop == "" {
prop = safePathToken(c.FormValue("prop"))
}
filename := safeFilename(body.Filename)
if filename == "" {
filename = safeFilename(c.FormValue("filename"))
}
if id == "" {
return fiber.NewError(fiber.StatusBadRequest, "missing field: id")
}
if session == "" {
return fiber.NewError(fiber.StatusBadRequest, "missing field: session")
}
if filename == "" {
return fiber.NewError(fiber.StatusBadRequest, "missing field: filename")
}
path := filepath.Join("docs", id, session)
if prop != "" {
path = filepath.Join(path, prop)
}
path = filepath.Join(path, filename)
err := os.Remove(path)
if err != nil {
if os.IsNotExist(err) {
return c.JSON(fiber.Map{
"ok": true,
"deleted": false,
"file": filename,
})
}
return fiber.NewError(fiber.StatusInternalServerError, "cannot delete attachment")
}
return c.JSON(fiber.Map{
"ok": true,
"deleted": true,
"file": filename,
})
})
// History API fallback for SPA routes.
app.Use(func(c *fiber.Ctx) error {
return c.SendFile("./static/index.html")
})
addr := ":" + port
log.Printf("Starting Fiber server on %s", addr)
if err := app.Listen(addr); err != nil {
log.Fatalf("server failed: %v", err)
}
}