7 Commits

Author SHA1 Message Date
608a083861 patch: fix the back option for archives 2026-01-03 16:51:16 -05:00
50a45b6a82 update archive example 2026-01-03 16:50:33 -05:00
19752a0c89 change logging format 2026-01-03 09:53:07 -05:00
5192919645 update gitignore 2026-01-02 10:49:04 -05:00
f763f57001 rewrite Dockerfile 2026-01-02 10:06:27 -05:00
629fd06be0 exit beta 2026-01-01 23:06:30 -05:00
bedcfe781d updated logging 2026-01-01 23:05:29 -05:00
8 changed files with 167 additions and 77 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
fes
*.tar.gz

View File

@@ -4,24 +4,17 @@ WORKDIR /src
RUN apk add --no-cache git build-base
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
RUN make
RUN go build -ldflags="-X fes/modules/version.gitCommit=$(git rev-parse --short HEAD) -s -w" -o fes
FROM alpine:3.19
FROM scratch
COPY --from=builder /src/fes /fes
COPY --from=builder /src/fes /usr/local/bin/fes
WORKDIR /app
EXPOSE 8080
EXPOSE 3000
ENTRYPOINT ["/fes"]
ENTRYPOINT ["/usr/local/bin/fes"]
CMD ["run", "/app"]

View File

@@ -0,0 +1,22 @@
Pinnipeds [2] are the seals and their relatives, a group of semi-aquatic marine
mammals. The Pinnipedia is in the Order Carnivora. There are three seal
families: Odobenidae (walruses), Otariidae (eared seals, including sea lions
and fur seals), and Phocidae (true seals).[3]
Seals are sleek-bodied and barrel-shaped. Their bodies are well adapted to the
aquatic habitat where they spend most of their lives. Pinnipeds have flippers
for hands, big bulky bodies, doggish faces, and big eyes. Unlike cetaceans,
pinnipeds have their noses on their faces, and each nostril of the nose closes
when the pinniped goes underwater. Like cetaceans, pinnipeds have a thick layer
of blubber (fat) just under their skin: this blubber keeps them warm in cold
waters and keeps them fed during times when food is not easily found. When they
cannot find food, they live off the fat in the blubber.
Pinnipeds are carnivorous. This means they eat only meat (such as fish or
squid) and not plants. However, almost all pinnipeds can be eaten by polar
bears, sharks and killer whales.
Seals are often trained in zoos or aquariums to put on shows. However, in
Sweden, it is illegal to train a seal to balance a ball on its nose.[4]
From [Pinniped Wikipedia](https://simple.wikipedia.org/wiki/Pinniped)

27
main.go
View File

@@ -6,6 +6,7 @@ import (
"flag"
"fmt"
"os"
"runtime"
"github.com/fatih/color"
@@ -13,6 +14,7 @@ import (
"fes/modules/doc"
"fes/modules/new"
"fes/modules/server"
"fes/modules/ui"
"fes/modules/version"
)
@@ -29,18 +31,21 @@ func init() {
config.Docker = flag.Bool("docker", false, "Create a docker project")
config.Lib = lib
config.Doc = documentation
config.Verbose = flag.Bool("verbose", false, "Enable verbose logging")
}
func main() {
var m runtime.MemStats
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <command> <project_dir>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Commands:")
fmt.Fprintf(os.Stderr, " new <project_dir> Create a new project")
fmt.Fprintf(os.Stderr, " doc Open documentation")
fmt.Fprintf(os.Stderr, " run <project_dir> Start the server")
fmt.Fprintf(os.Stderr, "Options:")
fmt.Fprintln(flag.CommandLine.Output(), "Commands:")
fmt.Fprintln(flag.CommandLine.Output(), " new <project_dir> Create a new project")
fmt.Fprintln(flag.CommandLine.Output(), " doc Open documentation")
fmt.Fprintln(flag.CommandLine.Output(), " run <project_dir> Start the server")
fmt.Fprintln(flag.CommandLine.Output(), "Options:")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.")
fmt.Fprintln(flag.CommandLine.Output(), "For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.")
}
showVersion := flag.Bool("version", false, "Show version and exit")
@@ -60,6 +65,10 @@ func main() {
color.NoColor = true
}
if *config.Port == 3000 {
ui.WARNING("Using default port, this may lead to conflicts with other services")
}
args := flag.Args()
if len(args) < 1 {
flag.Usage()
@@ -89,6 +98,12 @@ func main() {
os.Exit(1)
}
case "run":
ui.Log("Fes is starting")
ui.Log("Fes version=%s, commit=%s, just started", version.VERSION, version.GetCommit())
runtime.ReadMemStats(&m)
ui.Log("FRE memory usage when created %v Mb", m.TotalAlloc/1024/1024)
if err := server.Start(dir); err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(os.Stderr, "%s does not exist\n", dir)

View File

@@ -11,6 +11,7 @@ var Port *int
var Color *bool
var Static *bool
var Docker *bool
var Verbose *bool
type AppConfig struct {
App struct {

View File

@@ -1,11 +1,10 @@
package server
import (
"errors"
"fes/modules/config"
"fes/modules/ui"
"fmt"
"github.com/pelletier/go-toml/v2"
lua "github.com/yuin/gopher-lua"
"html/template"
"io/fs"
"net/http"
@@ -15,6 +14,9 @@ import (
"sort"
"strings"
"time"
"github.com/pelletier/go-toml/v2"
lua "github.com/yuin/gopher-lua"
)
/* this is the request data we pass over the bus to the application, via the fes.bus interface */
@@ -284,23 +286,25 @@ func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
urlPath = basePath(strings.TrimPrefix(urlPath, "/archive"))
var b strings.Builder
b.WriteString("<html>\n<head><title>Index of ")
b.WriteString(template.HTMLEscapeString(urlPath))
b.WriteString("</title></head>\n<body>\n<h1>Index of ")
b.WriteString(template.HTMLEscapeString(urlPath))
b.WriteString("</h1><hr><pre>")
if urlPath != "/archive" && urlPath != "/archive/" {
up := path.Dir(urlPath)
if up == "." {
up = "/archive"
}
if !strings.HasSuffix(up, "/") {
up = "/archive" + filepath.Dir(up) + "/"
}
b.WriteString(`<a href="` + template.HTMLEscapeString(up) + `">../</a>` + "\n")
if urlPath != "/" {
b.WriteString(
`<a href="/archive` +
template.HTMLEscapeString(path.Dir(strings.TrimSuffix(urlPath, "/"))) +
`">../</a>` + "\n",
)
} else {
b.WriteString(`<a href="../">../</a>` + "\n")
b.WriteString(
`<a href="/">../</a>` + "\n",
)
}
nameCol := 50
for _, ei := range list {
escapedName := template.HTMLEscapeString(ei.name)
@@ -378,18 +382,26 @@ func loadDirs() map[string]string {
/* helper to parse the Fes.toml and generate config */
func parseConfig() config.AppConfig {
defaultCfg := config.AppConfig{}
defaultCfg.App.Authors = []string{"unknown"}
defaultCfg.App.Name = "unknown"
defaultCfg.App.Version = "unknown"
tomlDocument, err := os.ReadFile("Fes.toml")
if err != nil {
ui.Error("failed to read Fes.toml", err)
os.Exit(1)
if errors.Is(err, os.ErrNotExist) {
ui.WARN("no config file found, using the default config. In order to specify a config file write to Fes.toml")
return defaultCfg
} else {
ui.Error("failed to read Fes.toml", err)
os.Exit(1)
}
}
docStr := fixMalformedToml(string(tomlDocument))
var cfg config.AppConfig
if err := toml.Unmarshal([]byte(docStr), &cfg); err != nil {
ui.Warning("failed to parse Fes.toml", err)
cfg.App.Authors = []string{"unknown"}
cfg.App.Name = "unknown"
cfg.App.Version = "unknown"
cfg = defaultCfg
}
return cfg
}
@@ -414,6 +426,8 @@ func Start(dir string) error {
return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err)
}
ui.Log("Running root=%s, port=%d.", filepath.Clean(dir), *config.Port)
cfg := parseConfig()
notFoundData := generateNotFoundData(&cfg)
routes := loadDirs()
@@ -465,7 +479,8 @@ func Start(dir string) error {
w.Write(data)
})
ui.Log("Server initialized")
fmt.Printf("Server is running on http://localhost:%d\n", *config.Port)
ui.Log("Ready to accept connections tcp")
return http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *config.Port), nil)
}

View File

@@ -4,75 +4,114 @@ import (
"errors"
"fmt"
"strings"
"time"
"fes/modules/config"
"fes/modules/version"
"github.com/fatih/color"
)
const (
hint_color = 0xbda02a
hintColor = 0xbda02a
)
/* print out the current path (route) and relevant error */
func Path(path string, err error) {
path = strings.TrimPrefix(path, "/")
func formatTimestamp() string {
return time.Now().Format("02 Jan 2006 15:04")
}
if path == "" {
path = "(null)"
}
fmt.Printf(" > %s ", path)
if err == nil {
OK("ok")
return
} else if errors.Is(err, config.ErrRouteMiss) {
WARN(config.ErrRouteMiss.Error())
func logMessage(prefix string, msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
if prefix == "" {
fmt.Printf("%s * %s\n", formatTimestamp(), formatted)
} else {
ERROR("bad")
fmt.Printf("%s * %s: %s\n", formatTimestamp(), prefix, formatted)
}
}
/* print general system warning */
func Warning(msg string, err error) error {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.MagentaString("warning"), err)
return err
// Generic log
func Log(msg string, args ...any) {
logMessage("", msg, args...)
}
/* print general system error */
func Error(msg string, err error) error {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("error"), err)
return err
// OK message (green)
func OK(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Green("%s * %s\n", formatTimestamp(), formatted)
}
/* print fatality and panic */
func Fatal(msg string, err error) error {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("fatal"), err)
panic(err)
// Warning message (magenta)
func WARN(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Magenta("%s # %s\n", formatTimestamp(), formatted)
}
/* print a useful hint to the user */
func Hint(format string, args ...any) {
// Warning message (magenta)
func WARNING(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Magenta("%s # WARNING %s\n", formatTimestamp(), formatted)
}
// Error message (red)
func ERROR(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Red("%s ! %s\n", formatTimestamp(), formatted)
}
// Fatal message and panic
func FATAL(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Red("%s % %s\n", formatTimestamp(), formatted)
panic(formatted)
}
// Hint message (custom color)
func Hint(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.RGB(func(hex int) (r, g, b int) {
r = (hex >> 16) & 0xFF
g = (hex >> 8) & 0xFF
b = hex & 0xFF
return
}(hint_color)).Printf("hint: %s\n", fmt.Sprintf(format, args...))
}(hintColor)).Printf("%s * hint: %s\n", formatTimestamp(), formatted)
}
/* print message using the ok status color */
func OK(msg string) {
color.Green(msg)
// Path logging: prints route and status
func Path(path string, err error) {
path = strings.TrimPrefix(path, "/")
if path == "" {
path = "(null)"
}
if err == nil {
OK("Route: %s - ok", path)
} else if errors.Is(err, config.ErrRouteMiss) {
WARN("Route: %s - %s", path, config.ErrRouteMiss.Error())
} else {
ERROR("Route: %s - bad", path)
}
}
/* print message using the warning status color */
func WARN(msg string) {
color.Magenta(msg)
// System warning with prefix
func Warning(msg string, err error) error {
WARN("%s: %v", msg, err)
return err
}
/* print message using the error status color */
func ERROR(msg string) {
color.Red(msg)
// System error with prefix
func Error(msg string, err error) error {
ERROR("%s: %v", msg, err)
return err
}
// Fatal system error
func Fatal(msg string, err error) error {
FATAL("%s: %v", msg, err)
return err
}
// Log on Verbose
func LogVerbose(msg string, args ...any) {
if *config.Verbose {
Log(msg, args...)
}
}

View File

@@ -9,7 +9,7 @@ var gitCommit string = "devel"
const PROGRAM_NAME string = "fes"
const PROGRAM_NAME_LONG string = "fes/fSD"
const VERSION string = "beta"
const VERSION string = "0.2.1"
func Version() {
fmt.Printf("%s version %s\n", PROGRAM_NAME_LONG, VERSION)
@@ -20,3 +20,7 @@ func FullVersion() {
fmt.Printf("%s+%s\n", VERSION, gitCommit)
os.Exit(0)
}
func GetCommit() string {
return gitCommit
}