api fixes, improvements
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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] {
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user