2024-04-04 20:37:54 +00:00
|
|
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
2024-04-04 20:30:41 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"io"
|
|
|
|
"context"
|
|
|
|
_ "fmt"
|
|
|
|
_ "log"
|
|
|
|
"strings"
|
|
|
|
"net/http"
|
|
|
|
_ "encoding/json"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gin-contrib/cors"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
"tagger/internal/data"
|
|
|
|
"tagger/internal/models"
|
|
|
|
"tagger/internal/service"
|
|
|
|
docs "tagger/docs"
|
|
|
|
swaggerfiles "github.com/swaggo/files"
|
|
|
|
ginswagger "github.com/swaggo/gin-swagger"
|
|
|
|
"encoding/base64"
|
|
|
|
)
|
|
|
|
|
|
|
|
// @title Tagger API
|
|
|
|
// @version 0.31
|
|
|
|
// @description This is the Tagger API.
|
|
|
|
// @termsOfService http://swagger.io/terms/
|
|
|
|
|
|
|
|
// @contact.name API Support
|
|
|
|
// @contact.url http://www.swagger.io/support
|
|
|
|
// @contact.email matthewrich.conf@gmail.com
|
|
|
|
|
|
|
|
// @license.name Apache 2.0
|
|
|
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
|
|
|
|
|
|
|
// @host localhost:8080
|
|
|
|
// @BasePath /api/v1
|
|
|
|
|
|
|
|
type App struct {
|
|
|
|
apiBasePath string
|
|
|
|
service *service.Service
|
|
|
|
router *gin.Engine
|
|
|
|
connector service.DataConnector
|
|
|
|
}
|
|
|
|
|
|
|
|
type tag struct {
|
|
|
|
Id string `json:"id",omitempty`
|
|
|
|
Name string `json:"name",omitempty`
|
|
|
|
Resources []string `json:"resources"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type query struct {
|
|
|
|
Terms string `form:"terms[]"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// @BasePath /api/v1
|
|
|
|
|
|
|
|
// Ping godoc
|
|
|
|
// @Summary ping endpoint
|
|
|
|
// @Schemes
|
|
|
|
// @Description ping alive endpoint
|
|
|
|
// @Tags ping
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {string} pong
|
|
|
|
// @Router /ping [get]
|
|
|
|
func (a *App) Ping(c *gin.Context) {
|
|
|
|
c.String(200, "pong")
|
|
|
|
}
|
|
|
|
|
|
|
|
func SSEHeadersMiddleware() gin.HandlerFunc {
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
if c.GetHeader("Content-Type") == "text/event-stream" {
|
|
|
|
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
|
|
|
c.Writer.Header().Set("Cache-Control", "no-cache")
|
|
|
|
c.Writer.Header().Set("Connection", "keep-alive")
|
|
|
|
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
|
|
|
}
|
|
|
|
c.Next()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @BasePath /api/v1
|
|
|
|
|
|
|
|
// Tag godoc
|
|
|
|
// @Summary Update a tag
|
|
|
|
// @Schemes
|
|
|
|
// @Description Updates the resources associated with a tag
|
|
|
|
// @Tags tag resource
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} tag
|
|
|
|
// @Param name path string true "Tag name"
|
|
|
|
// @Param tag body string true "Tag JSON object"
|
|
|
|
// @Router /tags/{name} [put]
|
|
|
|
func (a *App) updateTag(c *gin.Context) {
|
|
|
|
var t tag
|
|
|
|
tagName := c.Param("tag")
|
|
|
|
|
|
|
|
if err := c.ShouldBindJSON(&t); err != nil {
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _,resource := range(t.Resources) {
|
|
|
|
a.service.AddTag(c, tagName, resource)
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
|
|
|
}
|
|
|
|
|
|
|
|
// @BasePath /api/v1
|
|
|
|
|
|
|
|
// Tag godoc
|
|
|
|
// @Summary Add a tag for a resource
|
|
|
|
// @Schemes
|
|
|
|
// @Description Add a tag for a resource
|
|
|
|
// @Tags tag resource
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} tag
|
|
|
|
// @Param tag body string true "Tag JSON object"
|
|
|
|
// @Router /tags [post]
|
|
|
|
func (a *App) createTag(c *gin.Context) {
|
|
|
|
var t tag
|
|
|
|
if err := c.ShouldBindJSON(&t); err != nil {
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _,resource := range(t.Resources) {
|
|
|
|
a.service.AddTag(c, t.Name, resource)
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
|
|
|
}
|
|
|
|
|
|
|
|
// @BasePath /api/v1
|
|
|
|
|
|
|
|
// Tags godoc
|
|
|
|
// @Summary Get list of resources tagged
|
|
|
|
// @Schemes
|
|
|
|
// @Description Get resources associated with a Tag
|
|
|
|
// @Tags Tags
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {array} string
|
|
|
|
// @Param name path string true "Tag name"
|
|
|
|
// @Router /tags/{name} [get]
|
|
|
|
func (a *App) getTag(c *gin.Context) {
|
|
|
|
var tag models.Tag
|
|
|
|
tag.Name = c.Param("tag")
|
|
|
|
tag.Resources = a.service.Query(c, []string { tag.Name })
|
|
|
|
c.JSON(http.StatusOK, tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// @BasePath /api/v1
|
|
|
|
|
|
|
|
// Tag godoc
|
|
|
|
// @Summary Add a resource to a tag
|
|
|
|
// @Schemes
|
|
|
|
// @Description Add a resource to a tag
|
|
|
|
// @Tags tag resource
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} tag
|
|
|
|
// @Param tag path string true "Tag name"
|
|
|
|
// @Param resource body string true "Resource JSON object"
|
|
|
|
// @Router /tags/{tag} [post]
|
|
|
|
func (a *App) createResource(c *gin.Context) {
|
|
|
|
var body map[string]string
|
|
|
|
tagName := c.Param("tag")
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
a.service.AddTag(c, tagName, body["resource"])
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *App) streamTags(c *gin.Context) {
|
|
|
|
c.Stream(func(w io.Writer) bool {
|
|
|
|
tags := a.service.Tags(c)
|
|
|
|
for _,v := range(tags) {
|
|
|
|
//jsonData, _ := json.Marshal(message)
|
|
|
|
c.SSEvent("message", v)
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
<-c.Writer.CloseNotify()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *App) streamResourceTags(c *gin.Context, resourceUrl string) {
|
|
|
|
c.Stream(func(w io.Writer) bool {
|
|
|
|
tags := a.service.ResourceTags(c, resourceUrl)
|
|
|
|
for _,v := range(tags) {
|
|
|
|
//jsonData, _ := json.Marshal(message)
|
|
|
|
c.SSEvent("message", v)
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
<-c.Writer.CloseNotify()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *App) getTags(c *gin.Context) {
|
|
|
|
if c.GetHeader("Content-Type") == "text/event-stream" {
|
|
|
|
a.streamTags(c)
|
|
|
|
} else {
|
|
|
|
c.JSON(http.StatusOK, a.service.Tags(c))
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// @BasePath /api/v1
|
|
|
|
|
|
|
|
// Tag godoc
|
|
|
|
// @Summary Get resource tags
|
|
|
|
// @Schemes
|
|
|
|
// @Description Get resource to tags
|
|
|
|
// @Tags tag resource
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} models.Resource
|
|
|
|
// @Param resource path string true "Base64 encoded Resource URL"
|
|
|
|
// @Router /resources/{resource} [get]
|
|
|
|
func (a *App) getResourceTags(c *gin.Context) {
|
|
|
|
var result models.Resource
|
|
|
|
resourceUrl, e := base64.StdEncoding.DecodeString(c.Param("resource"))
|
|
|
|
if e != nil {
|
|
|
|
c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
|
|
|
|
}
|
|
|
|
result.Resource = string(resourceUrl)
|
|
|
|
if c.GetHeader("Content-Type") == "text/event-stream" {
|
|
|
|
a.streamResourceTags(c, result.Resource)
|
|
|
|
} else {
|
|
|
|
result.Tags = a.service.ResourceTags(c, result.Resource)
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// @BasePath /api/v1
|
|
|
|
|
|
|
|
// Tag godoc
|
|
|
|
// @Summary Get resource tags
|
|
|
|
// @Schemes
|
|
|
|
// @Description Get resource to tags
|
|
|
|
// @Tags tag resource
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {object} models.Resource
|
|
|
|
// @Param resource query string true "Resource URL"
|
|
|
|
// @Router /resources [get]
|
|
|
|
func (a *App) getResourceTagsByQuery(c *gin.Context) {
|
|
|
|
var result models.Resource
|
|
|
|
result.Resource = c.DefaultQuery("resource", "")
|
|
|
|
if result.Resource == "" {
|
|
|
|
// return list of unfiltered Resource models
|
|
|
|
} else {
|
|
|
|
if c.GetHeader("Content-Type") == "text/event-stream" {
|
|
|
|
a.streamResourceTags(c, result.Resource)
|
|
|
|
} else {
|
|
|
|
result.Tags = a.service.ResourceTags(c, result.Resource)
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *App) BasePath() string {
|
|
|
|
return a.apiBasePath
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *App) Run() {
|
|
|
|
a.router.Run(":8080")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *App) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
a.router.ServeHTTP(rw,req)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *App) queryFormHandler(c *gin.Context) {
|
|
|
|
var form query
|
|
|
|
c.Bind(&form)
|
|
|
|
terms := strings.Fields(form.Terms)
|
|
|
|
results := a.service.Query(c, terms)
|
|
|
|
c.HTML(http.StatusOK, "query.tmpl", gin.H{ "resources": results, "terms": terms })
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewApp(connector service.DataConnector) *App {
|
|
|
|
if connector == nil {
|
|
|
|
connector = data.NewRedisConnector(redis.NewClient(&redis.Options{
|
|
|
|
Addr: "localhost:6379",
|
|
|
|
Password: "",
|
|
|
|
DB: 0,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
a := &App {
|
|
|
|
apiBasePath: "/api/v1",
|
|
|
|
service: service.NewService(),
|
|
|
|
router: gin.Default(),
|
|
|
|
connector: connector,
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
|
|
|
|
log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
docs.SwaggerInfo.BasePath = a.apiBasePath
|
|
|
|
|
|
|
|
a.service.UseDataConnector(context.Background(), a.connector)
|
|
|
|
a.router.Use(cors.Default())
|
|
|
|
a.router.GET("/swagger/*any", ginswagger.WrapHandler(swaggerfiles.Handler))
|
|
|
|
|
|
|
|
a.router.StaticFile("/favicon.ico", "./static/icons/tagger16-mb.ico")
|
|
|
|
a.router.Static("/icons", "./static/icons/")
|
|
|
|
a.router.LoadHTMLGlob("templates/*")
|
|
|
|
|
|
|
|
api := a.router.Group(a.apiBasePath)
|
|
|
|
api.GET("/ping", a.Ping)
|
|
|
|
api.GET("/resources/:resource", SSEHeadersMiddleware(), a.getResourceTags)
|
|
|
|
api.GET("/resources", SSEHeadersMiddleware(), a.getResourceTagsByQuery)
|
|
|
|
api.GET("/tags", SSEHeadersMiddleware(), a.getTags)
|
|
|
|
//r.GET("/tags", SSEHeadersMiddleware(), getTags)
|
|
|
|
api.GET("/tags/:tag", a.getTag)
|
|
|
|
api.POST("/tags", a.createTag)
|
|
|
|
api.POST("/tags/:tag", a.createResource)
|
|
|
|
api.PUT("/tags/:tag", a.updateTag)
|
|
|
|
|
|
|
|
a.router.GET("/tags", func(c *gin.Context) {
|
|
|
|
c.HTML(http.StatusOK, "resources.tmpl", gin.H{"title": "Resources", "tags": a.service.Tags(c) })
|
|
|
|
})
|
|
|
|
a.router.GET("/", func(c *gin.Context) {
|
|
|
|
c.HTML(http.StatusOK, "index.tmpl", gin.H{ "title": "Resources" })
|
|
|
|
})
|
|
|
|
a.router.POST("/", a.queryFormHandler)
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
app := NewApp(nil)
|
|
|
|
app.Run()
|
|
|
|
}
|