images: added list api
This commit is contained in:
parent
9b04b6edfc
commit
62e3f9778e
170
api/images_list.go
Normal file
170
api/images_list.go
Normal file
|
@ -0,0 +1,170 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/sm"
|
||||
"github.com/tigorlazuardi/redmage/models"
|
||||
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||
)
|
||||
|
||||
type ImageListParams struct {
|
||||
Q string
|
||||
SFW bool
|
||||
OrderBy string
|
||||
Sort string
|
||||
Offset int64
|
||||
Limit int64
|
||||
Device int32
|
||||
Subreddit int32
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (ilp *ImageListParams) FillFromQuery(query Queryable) {
|
||||
ilp.Q = query.Get("q")
|
||||
ilp.SFW, _ = strconv.ParseBool(query.Get("sfw"))
|
||||
ilp.OrderBy = query.Get("order_by")
|
||||
ilp.Sort = strings.ToLower(query.Get("sort"))
|
||||
ilp.Offset, _ = strconv.ParseInt(query.Get("offset"), 10, 64)
|
||||
ilp.Limit, _ = strconv.ParseInt(query.Get("limit"), 10, 64)
|
||||
if ilp.Limit > 100 {
|
||||
ilp.Limit = 100
|
||||
}
|
||||
if ilp.Limit < 1 {
|
||||
ilp.Limit = 25
|
||||
}
|
||||
device, _ := strconv.ParseInt(query.Get("device"), 10, 32)
|
||||
ilp.Device = int32(device)
|
||||
subreddit, _ := strconv.ParseInt(query.Get("subreddit"), 10, 32)
|
||||
ilp.Subreddit = int32(subreddit)
|
||||
|
||||
createdAtint, _ := strconv.ParseInt(query.Get("created_at"), 10, 64)
|
||||
if createdAtint > 0 {
|
||||
ilp.CreatedAt = time.Unix(createdAtint, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (ilp ImageListParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) {
|
||||
if ilp.Q != "" {
|
||||
arg := sqlite.Arg("%" + ilp.Q + "%")
|
||||
expr = append(expr,
|
||||
sm.Where(
|
||||
models.ImageColumns.Title.Like(arg).
|
||||
Or(models.ImageColumns.Poster.Like(arg)).
|
||||
Or(models.ImageColumns.ImageRelativePath.Like(arg)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if ilp.SFW {
|
||||
expr = append(expr, models.SelectWhere.Images.NSFW.EQ(0))
|
||||
}
|
||||
|
||||
if ilp.Device > 0 {
|
||||
expr = append(expr, models.SelectWhere.Images.DeviceID.EQ(ilp.Device))
|
||||
}
|
||||
|
||||
if ilp.Subreddit > 0 {
|
||||
expr = append(expr, models.SelectWhere.Images.SubredditID.EQ(ilp.Subreddit))
|
||||
}
|
||||
|
||||
if !ilp.CreatedAt.IsZero() {
|
||||
expr = append(expr, models.SelectWhere.Images.CreatedAt.GTE(ilp.CreatedAt.Format(time.RFC3339)))
|
||||
}
|
||||
|
||||
return expr
|
||||
}
|
||||
|
||||
func (ilp ImageListParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) {
|
||||
expr = append(expr, ilp.CountQuery()...)
|
||||
if ilp.Limit > 0 {
|
||||
expr = append(expr, sm.Limit(ilp.Limit))
|
||||
}
|
||||
|
||||
if ilp.Offset > 0 {
|
||||
expr = append(expr, sm.Offset(ilp.Offset))
|
||||
}
|
||||
|
||||
if ilp.OrderBy != "" {
|
||||
order := sm.OrderBy(sqlite.Quote(ilp.OrderBy))
|
||||
if ilp.Sort == "desc" {
|
||||
expr = append(expr, order.Desc())
|
||||
} else {
|
||||
expr = append(expr, order.Asc())
|
||||
}
|
||||
} else {
|
||||
expr = append(expr, sm.OrderBy(models.ImageColumns.CreatedAt).Desc())
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
type ImageListResult struct {
|
||||
Total int64
|
||||
Images models.ImageSlice
|
||||
}
|
||||
|
||||
func (im ImageListResult) MarshalJSON() ([]byte, error) {
|
||||
type I struct {
|
||||
*models.Image
|
||||
Device *models.Device `json:"device,omitempty"`
|
||||
Subreddit *models.Subreddit `json:"subreddit,omitempty"`
|
||||
}
|
||||
type A struct {
|
||||
Total int64 `json:"total"`
|
||||
Images []I `json:"images"`
|
||||
}
|
||||
|
||||
a := A{Total: im.Total}
|
||||
a.Images = make([]I, len(im.Images))
|
||||
for i := 0; i < len(a.Images); i++ {
|
||||
a.Images[i].Image = im.Images[i]
|
||||
a.Images[i].Device = im.Images[i].R.Device
|
||||
a.Images[i].Subreddit = im.Images[i].R.Subreddit
|
||||
}
|
||||
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
func (api *API) ImagesList(ctx context.Context, params ImageListParams) (result ImageListResult, err error) {
|
||||
ctx, span := tracer.Start(ctx, "*API.ImagesList")
|
||||
defer span.End()
|
||||
|
||||
result.Images, err = models.Images.Query(ctx, api.db, params.Query()...).All()
|
||||
if err != nil {
|
||||
return result, errs.Wrapw(err, "failed to query for images", "params", params)
|
||||
}
|
||||
|
||||
result.Total, err = models.Images.Query(ctx, api.db, params.CountQuery()...).Count()
|
||||
if err != nil {
|
||||
return result, errs.Wrapw(err, "failed to query for images", "params", params)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (api *API) ImagesListWithDevicesAndSubreddits(ctx context.Context, params ImageListParams) (result ImageListResult, err error) {
|
||||
ctx, span := tracer.Start(ctx, "*API.ImagesListWithDevicesAndSubreddits")
|
||||
defer span.End()
|
||||
|
||||
result, err = api.ImagesList(ctx, params)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if err := result.Images.LoadImageDevice(ctx, api.db); err != nil {
|
||||
return result, errs.Wrapw(err, "failed to load image devices", "params", params)
|
||||
}
|
||||
|
||||
if err := result.Images.LoadImageSubreddit(ctx, api.db); err != nil {
|
||||
return result, errs.Wrapw(err, "failed to load image subreddits", "params", params)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
5
api/queryable.go
Normal file
5
api/queryable.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package api
|
||||
|
||||
type Queryable interface {
|
||||
Get(string) string
|
||||
}
|
|
@ -23,7 +23,7 @@ var DefaultConfig = map[string]any{
|
|||
|
||||
"pubsub.db.driver": "sqlite3",
|
||||
"pubsub.db.string": "pubsub.db",
|
||||
"pubsub.ack.deadline": "3h",
|
||||
"pubsub.ack.deadline": "30m",
|
||||
|
||||
"download.concurrency.images": 5,
|
||||
"download.concurrency.subreddits": 3,
|
||||
|
@ -34,8 +34,6 @@ var DefaultConfig = map[string]any{
|
|||
"download.timeout.idlespeed": "10KB",
|
||||
"download.useragent": "redmage",
|
||||
|
||||
"download.pubsub.ack.deadline": "3h",
|
||||
|
||||
"http.port": "8080",
|
||||
"http.host": "0.0.0.0",
|
||||
"http.shutdown_timeout": "5s",
|
||||
|
|
34
server/routes/images_list.go
Normal file
34
server/routes/images_list.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/tigorlazuardi/redmage/api"
|
||||
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||
)
|
||||
|
||||
func (routes *Routes) ImagesListAPI(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := tracer.Start(r.Context(), "*Routes.ImagesList")
|
||||
defer span.End()
|
||||
|
||||
var params api.ImageListParams
|
||||
|
||||
params.FillFromQuery(r.URL.Query())
|
||||
|
||||
enc := json.NewEncoder(rw)
|
||||
|
||||
result, err := routes.API.ImagesListWithDevicesAndSubreddits(ctx, params)
|
||||
if err != nil {
|
||||
log.New(ctx).Err(err).Error("failed to list images")
|
||||
code, message := errs.HTTPMessage(err)
|
||||
rw.WriteHeader(code)
|
||||
_ = enc.Encode(map[string]string{"error": message})
|
||||
return
|
||||
}
|
||||
|
||||
if err := enc.Encode(result); err != nil {
|
||||
log.New(ctx).Err(err).Error("failed to encode images")
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package routes
|
|||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
|
@ -19,7 +20,7 @@ type Routes struct {
|
|||
}
|
||||
|
||||
func (routes *Routes) Register(router chi.Router) {
|
||||
router.Use(chimiddleware.Compress(5, "text/html", "text/css", "application/javascript", "application/json"))
|
||||
router.Use(chimiddleware.Compress(5, "text/html", "text/css", "application/javascript", "application/json", "image/svg+xml", "image/x-icon"))
|
||||
|
||||
router.HandleFunc("/ping", routes.HealthCheck)
|
||||
router.HandleFunc("/health", routes.HealthCheck)
|
||||
|
@ -45,12 +46,18 @@ func (routes *Routes) registerV1APIRoutes(router chi.Router) {
|
|||
router.Post("/devices", routes.APIDeviceCreate)
|
||||
router.Patch("/devices/{id}", routes.APIDeviceUpdate)
|
||||
|
||||
router.Get("/images", routes.ImagesListAPI)
|
||||
|
||||
router.Get("/events", routes.EventsAPI)
|
||||
}
|
||||
|
||||
func (routes *Routes) registerWWWRoutes(router chi.Router) {
|
||||
router.Mount("/public", http.StripPrefix("/public", http.FileServer(http.FS(routes.PublicDir))))
|
||||
|
||||
imagesDir := http.FS(os.DirFS(routes.Config.String("download.directory")))
|
||||
|
||||
router.Mount("/img", http.StripPrefix("/img", http.FileServer(imagesDir)))
|
||||
|
||||
router.Group(func(r chi.Router) {
|
||||
r.Use(otelchi.Middleware("redmage"))
|
||||
r.Use(chimiddleware.RequestID)
|
||||
|
|
Loading…
Reference in a new issue