api fixes, improvements

This commit is contained in:
2026-09-01 10:22:48 +01:00
parent 0e762ac675
commit 2f279e9ea0
7 changed files with 277 additions and 178 deletions
+76
View File
@@ -0,0 +1,76 @@
package api
import (
"fmt"
"net/http"
"strconv"
"code.aneur.in/go/rest"
"code.aneur.in/zampler/zampler/internal/database"
"github.com/gorilla/mux"
)
func (api *API) GetFile(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
id := vars["id"]
r, err := api.db.Select("files").Where("id = ?", id).QueryRecord()
if err != nil {
rest.WriteErrorJSON(w, err)
return
}
rest.WriteResponseJSON(w, http.StatusOK, database.FileFromRecord(r))
}
func (api *API) GetFileByPath(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
r, err := api.db.Select("files").Where("rel_path = ?", fmt.Sprintf("/%s", vars["path"])).QueryRecord()
if err != nil {
rest.WriteErrorJSON(w, err)
}
rest.WriteResponseJSON(w, http.StatusOK, database.FileFromRecord(r))
}
func (api *API) ListFiles(w http.ResponseWriter, req *http.Request) {
uq := req.URL.Query()
// Read in params
input := &database.SearchFilesInput{
Filename: uq.Get("filename"),
Path: uq.Get("path"),
Sort: uq.Get("sort"),
Direction: uq.Get("direction"),
Limit: 100,
}
if uq.Has("offset") {
offset, err := strconv.Atoi(uq.Get("offset"))
if err != nil {
rest.ErrBadRequest.WithMessage("invalid offset").WithError(err).WriteJSON(w)
return
}
input.Offset = offset
}
if uq.Has("limit") {
limit, err := strconv.Atoi(uq.Get("limit"))
if err != nil {
rest.ErrBadRequest.WithMessage("invalid limit").WithError(err).WriteJSON(w)
return
}
input.Limit = limit
}
result, err := api.db.SearchFiles(input)
if err != nil {
rest.WriteErrorJSON(w, err)
return
}
rest.WriteResponseJSON(w, http.StatusOK, result)
}
-155
View File
@@ -1,155 +0,0 @@
package api
import (
"fmt"
"net/http"
"strconv"
"code.aneur.in/go/rest"
"code.aneur.in/go/validate"
"code.aneur.in/zampler/zampler/internal/dto"
"github.com/gorilla/mux"
)
const (
MaxLimit = 100
)
func (api *API) GetFile(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
id := vars["id"]
record, err := api.db.Select("files").Where("id = ?", id).QueryRecord()
if err != nil {
rest.WriteErrorJSON(w, err)
return
}
rest.WriteResponseJSON(w, http.StatusOK, record)
}
func (api *API) GetFileByPath(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
record, err := api.db.Select("files").Where("rel_path = ?", fmt.Sprintf("/%s", vars["path"])).QueryRecord()
if err != nil {
rest.WriteErrorJSON(w, err)
}
rest.WriteResponseJSON(w, http.StatusOK, record)
}
func (api *API) ListFiles(w http.ResponseWriter, req *http.Request) {
uq := req.URL.Query()
// Read in params
filename := ""
path := ""
direction := "asc"
sort := "path"
offset := 0
limit := MaxLimit
if uq.Has("filename") {
filename = uq.Get("filename")
} else if uq.Has("path") {
path = uq.Get("path")
}
switch uq.Get("direction") {
case "", "asc":
direction = "asc"
case "desc":
direction = "desc"
default:
rest.ErrBadRequest.WithMessage("invalid sort direction").WriteJSON(w)
return
}
switch uq.Get("sort") {
case "", "path":
sort = "path"
case "filename":
sort = "filename"
default:
rest.ErrBadRequest.WithMessage("invalid sort").WriteJSON(w)
return
}
if uq.Has("offset") {
o, err := strconv.Atoi(uq.Get("offset"))
if err != nil {
rest.ErrBadRequest.WithMessage("invalid offset").WithError(err).WriteJSON(w)
return
}
if err := validate.Min(0, false)(o); err != nil {
rest.ErrBadRequest.WithMessage("invalid offset").WithError(err).WriteJSON(w)
return
}
offset = o
}
if uq.Has("limit") {
l, err := strconv.Atoi(uq.Get("limit"))
if err != nil {
rest.ErrBadRequest.WithMessage("invalid limit").WithError(err).WriteJSON(w)
return
}
if err := validate.All(validate.Min(0, true), validate.Max(MaxLimit, false))(l); err != nil {
rest.ErrBadRequest.WithMessage("invalid limit").WithError(err).WriteJSON(w)
return
}
limit = l
}
// Apply params
query := api.db.Select("files")
switch sort {
case "filename":
query.OrderBy("filename asc")
case "path":
query.OrderBy("abs_path asc")
}
query.Offset(offset).Limit(limit)
rows, err := query.Query()
if err != nil {
rest.WriteErrorJSON(w, err)
return
}
totalCount := 0
for rows.Next() {
totalCount++
}
if err := rows.Err(); err != nil {
rest.WriteErrorJSON(w, err)
return
}
if filename != "" {
query.Where("filename like ?", filename)
} else if path != "" {
query.Where("abs_path like ?", path)
}
records, err := query.QueryRecords()
if err != nil {
rest.WriteErrorJSON(w, err)
return
}
// Output result
output := dto.NewList(records...).
WithTotalCount(totalCount).
WithParam("sort", sort).
WithDirection(direction).
WithParam("offset", offset).
WithParam("limit", limit).
MaybeWithParam("filename", filename).
MaybeWithParam("path", path)
rest.WriteResponseJSON(w, http.StatusOK, output)
}
+42
View File
@@ -0,0 +1,42 @@
package database
import (
"time"
"code.aneur.in/go/record"
"code.aneur.in/zampler/zampler/internal/types"
)
func FileFromRecord(r record.Record) *types.File {
f := &types.File{
ID: r.String("id"),
Hash: r.String("hash"),
RootDir: r.String("root_dir"),
AbsolutePath: r.String("abs_path"),
RelativeDir: r.String("rel_dir"),
RelativePath: r.String("rel_path"),
Filename: r.String("filename"),
Extension: r.String("ext"),
Size: r.Int64("size"),
Modified: time.UnixMilli(r.Int64("modified")),
}
return f
}
func FileToRecord(f *types.File) record.Record {
r := record.Record{
"id": f.ID,
"hash": f.Hash,
"root_dir": f.RootDir,
"abs_path": f.AbsolutePath,
"rel_dir": f.RelativeDir,
"rel_path": f.RelativePath,
"filename": f.Filename,
"ext": f.Extension,
"size": f.Size,
"modified": f.Modified.UnixMilli(),
}
return r
}
+92 -12
View File
@@ -1,24 +1,104 @@
package database
import (
"fmt"
"code.aneur.in/go/rest"
"code.aneur.in/go/sqimple"
"code.aneur.in/go/validate"
"code.aneur.in/zampler/zampler/internal/lib"
"code.aneur.in/zampler/zampler/internal/types"
)
var (
validateDirection = validate.In("", "asc", "desc")
validateLimit = validate.All(validate.Min(0, true), validate.Max(100, false))
validateOffset = validate.Min(0, false)
validateFileSort = validate.In("", "filename", "rel_path")
)
type SearchFilesInput struct {
Filename string
Path string
Sort string
Direction string
Offset int
Limit int
}
func (db *DB) SearchFiles(input *SearchFilesInput) (*List[*types.File], error) {
// Validate arguments
if err := validateFileSort(input.Sort); err != nil {
return nil, rest.ErrBadRequest.WithError(err).WithValue("param", "sort")
}
if err := validateDirection(input.Direction); err != nil {
return nil, rest.ErrBadRequest.WithError(err).WithValue("param", "direction")
}
if err := validateOffset(input.Offset); err != nil {
return nil, rest.ErrBadRequest.WithError(err).WithValue("param", "offset")
}
if err := validateLimit(input.Limit); err != nil {
return nil, rest.ErrBadRequest.WithError(err).WithValue("param", "limit")
}
// Total count query
countQuery := db.Select("files").Columns("count(*)")
if input.Filename != "" {
countQuery.Where("filename contains ?", input.Filename)
} else if input.Path != "" {
countQuery.Where("path contains ?", input.Path)
}
countRow := countQuery.QueryRow()
if err := countRow.Err(); err != nil {
return nil, err
}
total := 0
countRow.Scan(&total)
// Actual query
query := db.Select("files")
if input.Filename != "" {
countQuery.Where("filename contains ?", input.Filename)
} else if input.Path != "" {
countQuery.Where("path contains ?", input.Path)
}
orderBy := fmt.Sprintf("%s %s", lib.Coalesce(input.Sort, "rel_path"), lib.Coalesce(input.Direction, "asc"))
query.OrderBy(orderBy)
query.Limit(input.Limit)
if input.Offset > 0 {
query.Offset(input.Offset)
}
records, err := query.QueryRecords()
if err != nil {
return nil, err
}
files := []*types.File{}
for _, record := range records {
files = append(files, FileFromRecord(record))
}
l := NewList(files...).
WithTotalCount(total).
WithSort(input.Sort).
WithOffset(input.Offset).
WithLimit(input.Limit)
return l, nil
}
func (db *DB) UpdateFile(file *types.File) error {
r := FileToRecord(file)
_, err := db.Insert("files").
Columns("id", "hash", "root_dir", "abs_path", "rel_dir", "rel_path", "filename", "ext", "size", "modified").
Values(map[string]any{
"id": file.ID,
"hash": file.Hash,
"root_dir": file.RootDir,
"abs_path": file.AbsolutePath,
"rel_dir": file.RelativeDir,
"rel_path": file.RelativePath,
"filename": file.Filename,
"ext": file.Extension,
"size": file.Size,
"modified": file.Modified.UnixMilli(),
}).
Values(sqimple.Values(r)).
Exec()
return err
@@ -1,4 +1,4 @@
package dto
package database
type List[T any] struct {
Items []T `json:"items"`
@@ -32,7 +32,24 @@ func (l *List[T]) WithCount(name string, value int) *List[T] {
}
func (l *List[T]) WithDirection(direction string) *List[T] {
return l.WithParam("direction", direction)
if direction != "" {
return l.WithParam("direction", direction)
}
return l
}
func (l *List[T]) WithLimit(limit int) *List[T] {
if limit > 0 {
return l.WithParam("limit", limit)
}
return l
}
func (l *List[T]) WithOffset(offset int) *List[T] {
if offset > 0 {
return l.WithParam("offset", offset)
}
return l
}
func (l *List[T]) WithParam(name string, value any) *List[T] {
@@ -41,7 +58,10 @@ func (l *List[T]) WithParam(name string, value any) *List[T] {
}
func (l *List[T]) WithSort(value string) *List[T] {
return l.WithParam("sort", value)
if value != "" {
return l.WithParam("sort", value)
}
return l
}
func (l *List[T]) WithTotalCount(value int) *List[T] {
-8
View File
@@ -1,8 +0,0 @@
package dto
type File struct {
ID string `json:"id"`
Path string `json:"path"`
Hash string `json:"hash"`
Size int64 `json:"size"`
}
+44
View File
@@ -0,0 +1,44 @@
package lib
func Coalesce[T any](value, fallback T) T {
if IsZero(value) {
return fallback
}
return value
}
func IsZero(value any) bool {
switch value.(type) {
case bool:
return value == false
case float32:
return value == 0.0
case float64:
return value == 0.0
case int:
return value == 0
case int8:
return value == 0
case int16:
return value == 0
case int32:
return value == 0
case int64:
return value == 0
case uint:
return value == 0
case uint8:
return value == 0
case uint16:
return value == 0
case uint32:
return value == 0
case uint64:
return value == 0
case string:
return value == ""
default:
return value == nil
}
}