reorganise code into func areas

This commit is contained in:
2026-07-17 14:59:55 +01:00
parent 5b625b56f5
commit 3ed4bc949f
7 changed files with 6 additions and 319 deletions
+1
View File
@@ -4,6 +4,7 @@ go 1.25.6
require (
code.aneur.in/go/rest v0.0.0-20260710144250-d3e2b415e4ac
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/rs/zerolog v1.35.1
github.com/spf13/cobra v1.10.2
+2
View File
@@ -1,6 +1,8 @@
code.aneur.in/go/rest v0.0.0-20260710144250-d3e2b415e4ac h1:rmdYhq+CsQzPqK/tQ6G/DtWIS+IIME47Draei/mR9BM=
code.aneur.in/go/rest v0.0.0-20260710144250-d3e2b415e4ac/go.mod h1:HtXodmPBkxQyYkXn4UHURJEs44a+Jm+4tUaKXj246uE=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
-142
View File
@@ -1,142 +0,0 @@
package internal
import (
"errors"
"fmt"
"net"
"net/http"
"strconv"
"code.aneur.in/go/rest"
"code.aneur.in/zampler/zampler/internal/dto"
"github.com/gorilla/mux"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
)
type App struct {
Files Files
FileIDsByPath map[string]string
Log zerolog.Logger
}
func (app *App) Cmd() *cobra.Command {
cmd := &cobra.Command{
Use: "zampler root-dir ...",
Args: cobra.MinimumNArgs(1),
PreRunE: app.PreRunE,
RunE: app.RunE,
}
f := cmd.Flags()
f.StringP("address", "a", "", "HTTP bind address")
f.IntP("port", "p", 7777, "HTTP listen port")
f.StringP("log-level", "l", "info", "logging level (trace, debug, info, warn, error)")
f.StringArrayP("extension", "x", []string{"wav"}, "allowed audio file extensions")
return cmd
}
func (app *App) PreRunE(cmd *cobra.Command, _ []string) error {
app.Log = zerolog.New(zerolog.NewConsoleWriter()).With().Timestamp().Logger()
levelStr, _ := cmd.Flags().GetString("log-level")
level, err := zerolog.ParseLevel(levelStr)
if err != nil {
return err
}
app.Log = app.Log.Level(level)
return nil
}
func (app *App) RunE(cmd *cobra.Command, rootDirs []string) error {
extensions, _ := cmd.Flags().GetStringArray("extension")
host, _ := cmd.Flags().GetString("host")
port, _ := cmd.Flags().GetInt("port")
if len(extensions) == 0 {
return errors.New("at least one extension required")
}
files, err := app.MultiScan(rootDirs...)
if err != nil {
return err
}
app.Files = files.WithExtensions(extensions...)
app.FileIDsByPath = app.Files.PathMap()
router := mux.NewRouter()
router.Path("/api/files").Methods(http.MethodGet).HandlerFunc(app.listFiles)
router.Path("/api/files/{path:.+}").Methods(http.MethodGet).HandlerFunc(app.getFileByPath)
router.Path("/api/file/{id}").Methods(http.MethodGet).HandlerFunc(app.getFile)
errch := make(chan error)
go func() {
addr := net.JoinHostPort(host, strconv.Itoa(port))
errch <- http.ListenAndServe(addr, router)
}()
app.Log.Info().Int("port", port).Msg("Listening for HTTP connections")
return <-errch
}
func (app *App) getFile(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
id := vars["id"]
file := app.Files[id]
if file == nil {
rest.ErrNotFound.WriteJSON(w)
return
}
rest.WriteResponseJSON(w, http.StatusOK, dto.File{
ID: file.ID(),
Path: file.Path,
Size: file.Size(),
})
}
func (app *App) getFileByPath(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
path := fmt.Sprintf("/%s", vars["path"])
id := app.FileIDsByPath[path]
if id == "" {
rest.ErrNotFound.WriteJSON(w)
}
file := app.Files[id]
if file == nil {
rest.ErrNotFound.WriteJSON(w)
return
}
rest.WriteResponseJSON(w, http.StatusOK, dto.File{
ID: file.ID(),
Path: file.Path,
Size: file.Size(),
})
}
func (app *App) listFiles(w http.ResponseWriter, req *http.Request) {
dtos := []dto.File{}
for _, file := range app.Files {
dtos = append(dtos, dto.File{
ID: file.ID(),
Path: file.Path,
Size: file.Size(),
})
}
rest.WriteResponseJSON(w, http.StatusOK, dtos)
}
-65
View File
@@ -1,65 +0,0 @@
package internal
import (
"crypto/sha256"
"encoding/hex"
"os"
"path"
"strings"
)
type File struct {
RootDir string
Path string
}
func (file *File) AbsPath() string {
return path.Join(file.RootDir, file.Path)
}
func (f *File) Extension() string {
if i := strings.LastIndexByte(f.Path, '.'); i > -1 {
return strings.ToLower(f.Path[i+1:])
}
return ""
}
func (file *File) Filename() string {
return path.Base(file.Path)
}
func (file *File) Hash() (string, error) {
b, err := os.ReadFile(file.AbsPath())
if err != nil {
return "", err
}
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[0:]), nil
}
func (f *File) ID() string {
b := []byte(f.AbsPath())
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[0:])
}
func (f *File) Info() (os.FileInfo, error) {
return os.Stat(f.AbsPath())
}
func (f *File) Size() int64 {
if info, err := f.Info(); err == nil {
return info.Size()
}
return 0
}
func (file *File) String() string {
return file.AbsPath()
}
-29
View File
@@ -1,29 +0,0 @@
package internal
import (
"slices"
)
type Files map[string]*File
func (files Files) PathMap() map[string]string {
out := map[string]string{}
for _, file := range files {
out[file.Path] = file.ID()
}
return out
}
func (files Files) WithExtensions(exts ...string) Files {
out := Files{}
for id, file := range files {
if slices.Contains(exts, file.Extension()) {
out[id] = file
}
}
return out
}
-80
View File
@@ -1,80 +0,0 @@
package internal
import (
"errors"
"maps"
"os"
"path"
)
func (app *App) MultiScan(rootDirs ...string) (Files, error) {
files := Files{}
for _, rootDir := range rootDirs {
moreFiles, err := app.Scan(rootDir)
if err != nil {
return nil, err
}
maps.Copy(files, moreFiles)
}
return files, nil
}
func (app *App) Scan(rootDir string) (Files, error) {
if !path.IsAbs(rootDir) {
return nil, errors.New("rootDir must be absolute path")
}
log := app.Log.With().Str("rootDir", rootDir).Logger()
log.Debug().Msg("Scanning for files")
paths, err := scanRecursive(rootDir)
if err != nil {
return nil, err
}
l := len(rootDir)
files := Files{}
for _, path := range paths {
file := &File{
RootDir: rootDir,
Path: path[l:],
}
id := file.ID()
files[id] = file
log.Trace().Str("id", id).Str("path", files[id].Path).Msg("Found")
}
log.Debug().Int("files", len(files)).Msg("Completed scan")
return files, nil
}
func scanRecursive(dir string) ([]string, error) {
paths := []string{}
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, entry := range entries {
path := path.Join(dir, entry.Name())
if entry.IsDir() {
subPaths, err := scanRecursive(path)
if err != nil {
return nil, err
}
paths = append(paths, subPaths...)
} else {
paths = append(paths, path)
}
}
return paths, nil
}
+3 -3
View File
@@ -1,13 +1,13 @@
package main
import (
"code.aneur.in/zampler/zampler/internal"
"code.aneur.in/zampler/zampler/internal/cli"
)
func main() {
app := internal.App{}
cli := cli.NewCLI()
if err := app.Cmd().Execute(); err != nil {
if err := cli.Cmd().Execute(); err != nil {
panic(err)
}
}