🔨 Refactors server code into multiple files

This commit is contained in:
Daniel Svitan 2025-05-13 13:53:21 +02:00
parent 02b43592c2
commit 36f698991d
3 changed files with 243 additions and 199 deletions

130
server/admin.go Normal file
View File

@ -0,0 +1,130 @@
package main
import (
"io"
"net/http"
"os"
"strconv"
"syscall"
"time"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
)
func getClients(c echo.Context) error {
return c.JSONPretty(http.StatusOK, clients, indent)
}
func getClient(c echo.Context, id uuid.UUID) error {
for _, client := range clients {
if client.ID == id || client.Name == id.String() {
return c.JSONPretty(http.StatusOK, client, indent)
}
}
return c.NoContent(http.StatusNotFound)
}
func getLogs(c echo.Context) error {
files, err := os.ReadDir(dataDir)
if err != nil {
log.Error(err)
return c.NoContent(http.StatusInternalServerError)
}
filenames := []string{}
for _, file := range files {
if LogRegex.MatchString(file.Name()) {
filenames = append(filenames, file.Name())
}
}
return c.JSONPretty(http.StatusOK, filenames, indent)
}
func getClientLogs(c echo.Context, id uuid.UUID) error {
date := c.QueryParam("date")
if date == "" {
date = time.Now().Format(DateFormat)
}
skipRaw := c.QueryParam("skip")
skip := 0
if skipRaw != "" {
skip, err = strconv.Atoi(skipRaw)
if err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{"message": "skip is not a number"})
}
}
takeRaw := c.QueryParam("take")
take := 1024
if takeRaw != "" {
take, err = strconv.Atoi(takeRaw)
if err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{"message": "take is not a number"})
}
}
f, err := os.OpenFile(filename(dataDir, id.String(), date), syscall.O_RDONLY, 0644)
if err != nil {
return c.JSON(http.StatusNotFound, echo.Map{"message": "file not found"})
}
if skip != 0 {
_, err = f.Seek(int64(skip), io.SeekStart)
if err != nil {
log.Error(err)
return c.NoContent(http.StatusInternalServerError)
}
}
bytes := make([]byte, take)
n, err := f.Read(bytes)
if err != nil {
log.Error(err)
return c.NoContent(http.StatusInternalServerError)
}
return c.String(http.StatusOK, string(bytes[:n]))
}
func changeClientName(c echo.Context, id uuid.UUID) error {
name := c.QueryParam("name")
if name == "" {
return c.JSON(http.StatusBadRequest, "missing field 'name'")
}
var target *Client = nil
for _, client := range clients {
if client.ID == id || client.Name == id.String() {
target = client
}
if client.Name == name {
return c.JSON(http.StatusBadRequest, echo.Map{"message": "name already used"})
}
}
if target != nil {
target.Name = name
target.UpdatedAt = time.Now()
return c.JSON(http.StatusOK, echo.Map{"message": "ok"})
}
return c.JSON(http.StatusNotFound, echo.Map{"message": "not found"})
}
func exitClient(c echo.Context, id uuid.UUID) error {
for _, client := range clients {
if client.ID == id || client.Name == id.String() {
client.ExitWanted = true
return c.JSON(http.StatusOK, echo.Map{"message": "ok"})
}
}
return c.JSON(http.StatusNotFound, echo.Map{"message": "not found"})
}

105
server/keys.go Normal file
View File

@ -0,0 +1,105 @@
package main
import (
"fmt"
"net/http"
"os"
"syscall"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
)
func keys(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()
id := uuid.New()
idRaw := c.QueryParam("id")
if idRaw != "" {
updatedId, err := uuid.Parse(idRaw)
if err != nil {
log.Errorf("failed to parse id: %s", id)
} else {
id = updatedId
}
}
now := time.Now()
client := Client{
ID: id,
Name: "Dummy",
Connected: true,
ConnectedAt: now,
DisconnectedAt: now,
CreatedAt: now,
UpdatedAt: now,
ExitWanted: false,
conn: ws,
}
clients = append(clients, &client)
fmt.Printf("%s client %s connected\n", time.Now().Format(TimeFormat), client.ID.String())
err = ws.WriteMessage(websocket.TextMessage, []byte("welcome"))
if err != nil {
log.Error(err)
}
f, err := os.OpenFile(filename(dataDir, client.ID.String(), now.Format(DateFormat)), syscall.O_CREAT|syscall.O_APPEND|syscall.O_WRONLY, 0644)
if err != nil {
log.Error(err)
fmt.Printf("%s client %s crashed (couldn't open file)\n", time.Now().Format(TimeFormat), client.ID)
now = time.Now()
client.Connected = false
client.DisconnectedAt = now
client.UpdatedAt = now
return c.NoContent(http.StatusInternalServerError)
}
defer f.Close()
close := func() {
_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
_ = ws.Close()
now = time.Now()
client.ExitWanted = true
client.Connected = false
client.DisconnectedAt = now
client.UpdatedAt = now
}
for {
if client.ExitWanted {
close()
fmt.Printf("%s client %s disconnected\n", time.Now().Format(TimeFormat), client.ID.String())
break
}
_, msg, err := ws.ReadMessage()
if err != nil {
log.Error(err)
close()
fmt.Printf("%s client %s crashed (websocket error)\n", time.Now().Format(TimeFormat), client.ID.String())
return c.NoContent(http.StatusInternalServerError)
}
_, err = f.Write(msg)
if err != nil {
log.Error(err)
close()
fmt.Printf("%s client %s crashed (file write error)\n", time.Now().Format(TimeFormat), client.ID.String())
return c.NoContent(http.StatusInternalServerError)
}
}
return c.NoContent(http.StatusOK)
}

View File

@ -3,12 +3,10 @@ package main
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"regexp"
"strconv"
"syscall"
"time"
@ -22,6 +20,7 @@ import (
const TimeFormat = "2006-01-02 15:04:05"
const DateFormat = "2006-01-02"
const indent = "\t"
var LogRegex = regexp.MustCompile(`(?m)^\d{4}-\d{2}-\d{2}_.*\.txt$`)
@ -113,6 +112,7 @@ func main() {
Format: "${time_custom} ${method} ${uri} ---> ${status} in ${latency_human} (${bytes_out} bytes)\n",
CustomTimeFormat: TimeFormat,
}))
log.SetHeader("${time_rfc3339} ${level}")
app.Use(middleware.RemoveTrailingSlash())
app.OnAddRouteHandler = func(host string, route echo.Route, handler echo.HandlerFunc, middleware []echo.MiddlewareFunc) {
@ -144,8 +144,6 @@ func main() {
}
}
indent := "\t"
// client connection
app.GET("/", func(c echo.Context) error {
return c.JSONPretty(http.StatusOK, "Hello World!", indent)
@ -153,201 +151,12 @@ func main() {
app.GET("/keys", keys)
// administration
app.GET("/admin/clients", authed(func(c echo.Context) error {
return c.JSONPretty(http.StatusOK, clients, indent)
}))
app.GET("/admin/clients/:id", authed(idparam(func(c echo.Context, id uuid.UUID) error {
for _, client := range clients {
if client.ID == id || client.Name == id.String() {
return c.JSONPretty(http.StatusOK, client, indent)
}
}
return c.NoContent(http.StatusNotFound)
})))
app.GET("/admin/logs", authed(func(c echo.Context) error {
files, err := os.ReadDir(dataDir)
if err != nil {
log.Error(err)
return c.NoContent(http.StatusInternalServerError)
}
filenames := []string{}
for _, file := range files {
if LogRegex.MatchString(file.Name()) {
filenames = append(filenames, file.Name())
}
}
return c.JSONPretty(http.StatusOK, filenames, indent)
}))
app.GET("/admin/logs/:id", authed(idparam(func(c echo.Context, id uuid.UUID) error {
date := c.QueryParam("date")
if date == "" {
date = time.Now().Format(DateFormat)
}
skipRaw := c.QueryParam("skip")
skip := 0
if skipRaw != "" {
skip, err = strconv.Atoi(skipRaw)
if err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{"message": "skip is not a number"})
}
}
takeRaw := c.QueryParam("take")
take := 1024
if takeRaw != "" {
take, err = strconv.Atoi(takeRaw)
if err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{"message": "take is not a number"})
}
}
f, err := os.OpenFile(filename(dataDir, id.String(), date), syscall.O_RDONLY, 0644)
if err != nil {
return c.JSON(http.StatusNotFound, echo.Map{"message": "file not found"})
}
if skip != 0 {
_, err = f.Seek(int64(skip), io.SeekStart)
if err != nil {
log.Error(err)
return c.NoContent(http.StatusInternalServerError)
}
}
bytes := make([]byte, take)
n, err := f.Read(bytes)
if err != nil {
log.Error(err)
return c.NoContent(http.StatusInternalServerError)
}
return c.String(http.StatusOK, string(bytes[:n]))
})))
app.POST("/admin/name/:id", authed(idparam(func(c echo.Context, id uuid.UUID) error {
name := c.QueryParam("name")
if name == "" {
return c.JSON(http.StatusBadRequest, "missing field 'name'")
}
var target *Client = nil
for _, client := range clients {
if client.ID == id || client.Name == id.String() {
target = client
}
if client.Name == name {
return c.JSON(http.StatusBadRequest, echo.Map{"message": "name already used"})
}
}
if target != nil {
target.Name = name
target.UpdatedAt = time.Now()
return c.JSON(http.StatusOK, echo.Map{"message": "ok"})
}
return c.JSON(http.StatusNotFound, echo.Map{"message": "not found"})
})))
app.POST("/admin/exit/:id", authed(idparam(func(c echo.Context, id uuid.UUID) error {
for _, client := range clients {
if client.ID == id || client.Name == id.String() {
client.ExitWanted = true
return c.JSON(http.StatusOK, echo.Map{"message": "ok"})
}
}
return c.JSON(http.StatusNotFound, echo.Map{"message": "not found"})
})))
app.GET("/admin/clients", authed(getClients))
app.GET("/admin/clients/:id", authed(idparam(getClient)))
app.GET("/admin/logs", authed(getLogs))
app.GET("/admin/logs/:id", authed(idparam(getClientLogs)))
app.POST("/admin/name/:id", authed(idparam(changeClientName)))
app.POST("/admin/exit/:id", authed(idparam(exitClient)))
log.Fatal(app.Start(":8080"))
}
func keys(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()
now := time.Now()
client := Client{
ID: uuid.New(),
Name: "Dummy",
Connected: true,
ConnectedAt: now,
DisconnectedAt: now,
CreatedAt: now,
UpdatedAt: now,
ExitWanted: false,
conn: ws,
}
clients = append(clients, &client)
fmt.Printf("%s client %s connected\n", time.Now().Format(TimeFormat), client.ID.String())
err = ws.WriteMessage(websocket.TextMessage, []byte("welcome"))
if err != nil {
log.Error(err)
}
f, err := os.OpenFile(filename(dataDir, client.ID.String(), now.Format(DateFormat)), syscall.O_CREAT|syscall.O_APPEND|syscall.O_WRONLY, 0644)
if err != nil {
log.Error(err)
fmt.Printf("%s client %s crashed (couldn't open file)\n", time.Now().Format(TimeFormat), client.ID)
now = time.Now()
client.Connected = false
client.DisconnectedAt = now
client.UpdatedAt = now
return c.NoContent(http.StatusInternalServerError)
}
defer f.Close()
close := func() {
_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
_ = ws.Close()
now = time.Now()
client.ExitWanted = true
client.Connected = false
client.DisconnectedAt = now
client.UpdatedAt = now
}
for {
if client.ExitWanted {
close()
fmt.Printf("%s client %s disconnected\n", time.Now().Format(TimeFormat), client.ID.String())
break
}
_, msg, err := ws.ReadMessage()
if err != nil {
log.Error(err)
close()
fmt.Printf("%s client %s crashed (websocket error)\n", time.Now().Format(TimeFormat), client.ID.String())
return c.NoContent(http.StatusInternalServerError)
}
_, err = f.Write(msg)
if err != nil {
log.Error(err)
close()
fmt.Printf("%s client %s crashed (file write error)\n", time.Now().Format(TimeFormat), client.ID.String())
return c.NoContent(http.StatusInternalServerError)
}
}
return c.NoContent(http.StatusOK)
}