From b55f254d829ed68f8d36c03a3e67800e9960d21d Mon Sep 17 00:00:00 2001 From: Yi-Ting Shih Date: Fri, 7 Nov 2025 05:26:09 +0800 Subject: [PATCH] Init: lab4 done --- .gitignore | 2 + Makefile | 21 ++++ cmds/root.go | 33 ++++++ cmds/server.go | 77 ++++++++++++++ docs/docs.go | 123 ++++++++++++++++++++++ docs/swagger.json | 97 +++++++++++++++++ docs/swagger.yaml | 61 +++++++++++ go.mod | 70 +++++++++++++ go.sum | 200 ++++++++++++++++++++++++++++++++++++ go.work | 3 + go.work.sum | 14 +++ handlers/deleteUrl.go | 33 ++++++ handlers/getUrl.go | 46 +++++++++ handlers/handlers.go | 22 ++++ handlers/postUrl.go | 52 ++++++++++ main.go | 7 ++ middlewares/accessLog.go | 17 +++ middlewares/errorHandler.go | 56 ++++++++++ models/state.go | 13 +++ utils/initDB.go | 17 +++ utils/success.go | 14 +++ utils/urlFile.go | 48 +++++++++ workflows/fetch.go | 114 ++++++++++++++++++++ 23 files changed, 1140 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmds/root.go create mode 100644 cmds/server.go create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 handlers/deleteUrl.go create mode 100644 handlers/getUrl.go create mode 100644 handlers/handlers.go create mode 100644 handlers/postUrl.go create mode 100644 main.go create mode 100644 middlewares/accessLog.go create mode 100644 middlewares/errorHandler.go create mode 100644 models/state.go create mode 100644 utils/initDB.go create mode 100644 utils/success.go create mode 100644 utils/urlFile.go create mode 100644 workflows/fetch.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dc970c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +lab4 +urlList.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6af0abe --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.PHONY: all swagger clean + +SWAG ?= ~/go/bin/swag +SWAG_INIT_ARGS += -o docs -g cmds/server.go + +GO_ENV += CGO_ENABLED=0 + +SOURCE := $(shell find . -type f -name '*.go') +TARGET := lab4 + +all: swagger $(TARGET) + +swagger: + $(SWAG) fmt + $(SWAG) init $(SWAG_INIT_ARGS) + +$(TARGET): $(SOURCE) + $(GO_ENV) go build -o $@ + +clean: + -rm $(TARGET) diff --git a/cmds/root.go b/cmds/root.go new file mode 100644 index 0000000..dbb3d6e --- /dev/null +++ b/cmds/root.go @@ -0,0 +1,33 @@ +package cmds + +import ( + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +var RootCmd = &cobra.Command{ + Use: "lab4", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.BindPFlags(cmd.PersistentFlags()) + viper.BindPFlags(cmd.Flags()) + + cfg := zap.NewProductionConfig() + logger, err := cfg.Build() + if err != nil { + panic(err) + } + + zap.ReplaceGlobals(logger) + }, +} + +func init() { + cobra.EnableTraverseRunHooks = true + + RootCmd.AddCommand(serverCmd) +} diff --git a/cmds/server.go b/cmds/server.go new file mode 100644 index 0000000..ae0ee21 --- /dev/null +++ b/cmds/server.go @@ -0,0 +1,77 @@ +package cmds + +import ( + "database/sql" + "golang-lab4/handlers" + "golang-lab4/middlewares" + "golang-lab4/utils" + "golang-lab4/workflows" + "log" + "net/http" + + _ "golang-lab4/docs" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + httpSwagger "github.com/swaggo/http-swagger" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/sqlitedialect" + "github.com/uptrace/bun/driver/sqliteshim" + "github.com/uptrace/bunrouter" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +// @title Golang Lab4 +// @version 0.0.1-rc1 +// @termsOfService http://swagger.io/terms + +// @license.name 0BSD +// @BasePath / +var serverCmd = &cobra.Command{ + Use: "server", + Run: func(cmd *cobra.Command, args []string) { + // sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN( + // viper.GetString("pg-connection-string")))) + // bunDB := bun.NewDB(sqldb, pgdialect.New()) + + sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared") + if err != nil { + panic(err) + } + db := bun.NewDB(sqldb, sqlitedialect.New()) + utils.InitDB(db) + + fetcher := workflows.NewFetcher(db) + go fetcher.Run() + + h := handlers.NewHandlers(db, fetcher) + + router := bunrouter.New() + backend := router.NewGroup(""). + Use(middlewares.ErrorHandler). + Use(middlewares.AccessLog) + + if viper.GetBool("swagger") { + backend.GET("/swagger/*any", + bunrouter.HTTPHandlerFunc( + httpSwagger.Handler())) + } + + backend.GET("/url", h.GetUrl) + backend.POST("/url", h.PostUrl) + backend.DELETE("/url", h.DeleteUrl) + + log.Println(http.ListenAndServe( + ":"+viper.GetString("port"), + otelhttp.NewHandler(router, ""))) + }, +} + +func init() { + serverCmd.Flags(). + String("port", "8080", "") + serverCmd.Flags(). + Bool("swagger", true, "") + serverCmd.Flags(). + String("savefile", "./urlList.json", "") +} diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..7455175 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,123 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms", + "contact": {}, + "license": { + "name": "0BSD" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/url": { + "get": { + "parameters": [ + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UrlState" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "query params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.PostUrlInput" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "parameters": [ + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "definitions": { + "handlers.PostUrlInput": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + } + }, + "models.UrlState": { + "type": "object", + "properties": { + "isSuccess": { + "type": "boolean" + }, + "latency": { + "type": "integer" + }, + "url": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "0.0.1-rc1", + Host: "", + BasePath: "/", + Schemes: []string{}, + Title: "Golang Lab4", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..2221a8a --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,97 @@ +{ + "swagger": "2.0", + "info": { + "title": "Golang Lab4", + "termsOfService": "http://swagger.io/terms", + "contact": {}, + "license": { + "name": "0BSD" + }, + "version": "0.0.1-rc1" + }, + "basePath": "/", + "paths": { + "/url": { + "get": { + "parameters": [ + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UrlState" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "query params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.PostUrlInput" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "parameters": [ + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "definitions": { + "handlers.PostUrlInput": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + } + }, + "models.UrlState": { + "type": "object", + "properties": { + "isSuccess": { + "type": "boolean" + }, + "latency": { + "type": "integer" + }, + "url": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..f9c46a1 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,61 @@ +basePath: / +definitions: + handlers.PostUrlInput: + properties: + url: + type: string + type: object + models.UrlState: + properties: + isSuccess: + type: boolean + latency: + type: integer + url: + type: string + type: object +info: + contact: {} + license: + name: 0BSD + termsOfService: http://swagger.io/terms + title: Golang Lab4 + version: 0.0.1-rc1 +paths: + /url: + delete: + parameters: + - description: url + in: query + name: url + required: true + type: string + responses: + "200": + description: OK + get: + parameters: + - description: url + in: query + name: url + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UrlState' + post: + consumes: + - application/json + parameters: + - description: query params + in: body + name: request + required: true + schema: + $ref: '#/definitions/handlers.PostUrlInput' + responses: + "200": + description: OK +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0a8df8b --- /dev/null +++ b/go.mod @@ -0,0 +1,70 @@ +module golang-lab4 + +go 1.25.2 + +require ( + github.com/go-resty/resty/v2 v2.16.5 + github.com/spf13/cobra v1.10.1 + github.com/spf13/viper v1.21.0 + github.com/swaggo/http-swagger v1.3.4 + github.com/swaggo/swag v1.16.6 + github.com/uptrace/bun v1.2.15 + github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 + github.com/uptrace/bun/driver/sqliteshim v1.2.15 + github.com/uptrace/bunrouter v1.0.23 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.35.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.38.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..331b9b1 --- /dev/null +++ b/go.sum @@ -0,0 +1,200 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= +github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.2.15 h1:Ut68XRBLDgp9qG9QBMa9ELWaZOmzHNdczHQdrOZbEFE= +github.com/uptrace/bun v1.2.15/go.mod h1:Eghz7NonZMiTX/Z6oKYytJ0oaMEJ/eq3kEV4vSqG038= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 h1:7upGMVjFRB1oI78GQw6ruNLblYn5CR+kxqcbbeBBils= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.15/go.mod h1:c7YIDaPNS2CU2uI1p7umFuFWkuKbDcPDDvp+DLHZnkI= +github.com/uptrace/bun/driver/sqliteshim v1.2.15 h1:M/rZJSjOPV4OmfTVnDPtL+wJmdMTqDUn8cuk5ycfABA= +github.com/uptrace/bun/driver/sqliteshim v1.2.15/go.mod h1:YqwxFyvM992XOCpGJtXyKPkgkb+aZpIIMzGbpaw1hIk= +github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU= +github.com/uptrace/bunrouter v1.0.23/go.mod h1:O3jAcl+5qgnF+ejhgkmbceEk0E/mqaK+ADOocdNpY8M= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= +golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= +modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/go.work b/go.work new file mode 100644 index 0000000..864cc4b --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.25.2 + +use . diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..dbdeaa4 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,14 @@ +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/handlers/deleteUrl.go b/handlers/deleteUrl.go new file mode 100644 index 0000000..7aeef9f --- /dev/null +++ b/handlers/deleteUrl.go @@ -0,0 +1,33 @@ +package handlers + +import ( + "golang-lab4/middlewares" + "golang-lab4/utils" + "net/http" + + "github.com/uptrace/bunrouter" + "go.uber.org/zap" +) + +// DeleteUrl +// +// @param url query string true "url" +// @success 200 +// @router /url [delete] +func (self *Handlers) DeleteUrl( + w http.ResponseWriter, + req bunrouter.Request, +) error { + url := req.URL.Query().Get("url") + if url == "" { + zap.L().Warn("url not exist") + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "url not provided", + } + } + + self.fetcher.Remove(url) + + return utils.Success(w) +} diff --git a/handlers/getUrl.go b/handlers/getUrl.go new file mode 100644 index 0000000..0432213 --- /dev/null +++ b/handlers/getUrl.go @@ -0,0 +1,46 @@ +package handlers + +import ( + "golang-lab4/middlewares" + "golang-lab4/models" + "net/http" + + "github.com/uptrace/bunrouter" + "go.uber.org/zap" +) + +// GetUrl +// +// @param url query string true "url" +// @success 200 {object} models.UrlState +// @router /url [get] +func (self *Handlers) GetUrl( + w http.ResponseWriter, + req bunrouter.Request, +) error { + ctx := req.Context() + + url := req.URL.Query().Get("url") + if url == "" { + zap.L().Warn("url not exist") + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "url not provided", + } + } + + var ret models.UrlState + err := self.db.NewSelect(). + Model(&ret). + Where("url = ?", url). + Scan(ctx) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to query db", + OriginError: err, + } + } + + return bunrouter.JSON(w, ret) +} diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..84e7f03 --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "golang-lab4/workflows" + + "github.com/uptrace/bun" +) + +type Handlers struct { + db *bun.DB + fetcher *workflows.Fetcher +} + +func NewHandlers( + db *bun.DB, + fetcher *workflows.Fetcher, +) *Handlers { + return &Handlers{ + db: db, + fetcher: fetcher, + } +} diff --git a/handlers/postUrl.go b/handlers/postUrl.go new file mode 100644 index 0000000..52e49d5 --- /dev/null +++ b/handlers/postUrl.go @@ -0,0 +1,52 @@ +package handlers + +import ( + "encoding/json" + "golang-lab4/middlewares" + "golang-lab4/utils" + "io" + "net/http" + + "github.com/uptrace/bunrouter" + "go.uber.org/zap" +) + +type PostUrlInput struct { + Url string `json:"url"` +} + +// PostUrl +// +// @param request body PostUrlInput true "query params" +// @accept json +// @success 200 +// @router /url [post] +func (self *Handlers) PostUrl( + w http.ResponseWriter, + req bunrouter.Request, +) error { + b, err := io.ReadAll(req.Body) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "read payload failed", + OriginError: err, + } + } + + var input PostUrlInput + if err := json.Unmarshal(b, &input); err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "failed to unmarshal input into json", + OriginError: err, + } + } + + zap.L().Info("add url", + zap.String("url", input.Url)) + + self.fetcher.Add(input.Url) + + return utils.Success(w) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..1142131 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "golang-lab4/cmds" + +func main() { + cmds.RootCmd.Execute() +} diff --git a/middlewares/accessLog.go b/middlewares/accessLog.go new file mode 100644 index 0000000..46f427c --- /dev/null +++ b/middlewares/accessLog.go @@ -0,0 +1,17 @@ +package middlewares + +import ( + "net/http" + + "github.com/uptrace/bunrouter" + "go.uber.org/zap" +) + +func AccessLog(next bunrouter.HandlerFunc) bunrouter.HandlerFunc { + return func(w http.ResponseWriter, req bunrouter.Request) error { + zap.L().Info("access", + zap.String("method", req.Method), + zap.String("route", req.Params().Route())) + return next(w, req) + } +} diff --git a/middlewares/errorHandler.go b/middlewares/errorHandler.go new file mode 100644 index 0000000..e376ae9 --- /dev/null +++ b/middlewares/errorHandler.go @@ -0,0 +1,56 @@ +package middlewares + +import ( + "net/http" + + "github.com/uptrace/bunrouter" + "go.uber.org/zap" +) + +type HTTPError struct { + StatusCode int `json:"code"` + Message string `json:"message"` + OriginError error `json:"-"` +} + +func (e HTTPError) Error() string { + return e.Message +} + +func NewHTTPError(err error) HTTPError { + return HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "Internal server error with unknown reason", + OriginError: err, + } +} + +func ErrorHandler(next bunrouter.HandlerFunc) bunrouter.HandlerFunc { + return func(w http.ResponseWriter, req bunrouter.Request) error { + err := next(w, req) + + var httpErr HTTPError + switch err := err.(type) { + case nil: + return nil + + case HTTPError: + httpErr = err + + default: + httpErr = NewHTTPError(err) + } + + if httpErr.OriginError == nil { + zap.L().Warn(httpErr.Message) + } else { + zap.L().Warn(httpErr.Message, + zap.Error(httpErr.OriginError)) + } + + w.WriteHeader(httpErr.StatusCode) + _ = bunrouter.JSON(w, httpErr) + + return err + } +} diff --git a/models/state.go b/models/state.go new file mode 100644 index 0000000..b7d5c29 --- /dev/null +++ b/models/state.go @@ -0,0 +1,13 @@ +package models + +import ( + "github.com/uptrace/bun" +) + +type UrlState struct { + bun.BaseModel `bun:"table:url_state" json:"-"` + + Url string `bun:"url,pk" json:"url"` + IsSuccess bool `bun:"is_success" json:"isSuccess"` + Latency uint `bun:"latency" json:"latency"` +} diff --git a/utils/initDB.go b/utils/initDB.go new file mode 100644 index 0000000..eb14b61 --- /dev/null +++ b/utils/initDB.go @@ -0,0 +1,17 @@ +package utils + +import ( + "context" + "golang-lab4/models" + + "github.com/uptrace/bun" +) + +func InitDB(db *bun.DB) { + err := db.ResetModel(context.Background(), + (*models.UrlState)(nil), + ) + if err != nil { + panic(err) + } +} diff --git a/utils/success.go b/utils/success.go new file mode 100644 index 0000000..6fd2f3b --- /dev/null +++ b/utils/success.go @@ -0,0 +1,14 @@ +package utils + +import ( + "io" + "net/http" +) + +func Success(w http.ResponseWriter) error { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/plain") + _, err := io.WriteString(w, + `{"code":200, "message": "success"}`+"\n") + return err +} diff --git a/utils/urlFile.go b/utils/urlFile.go new file mode 100644 index 0000000..51a570f --- /dev/null +++ b/utils/urlFile.go @@ -0,0 +1,48 @@ +package utils + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" +) + +func LoadUrls(filename string) ([]string, error) { + file, err := os.OpenFile(filename, os.O_RDONLY, 0644) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return []string{}, nil + } + return nil, fmt.Errorf("failed to open file, %w", err) + } + + b, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("failed to read file, %w", err) + } + + var res []string + if err := json.Unmarshal(b, &res); err != nil { + return nil, fmt.Errorf("failed to unmarshal from json, %w", err) + } + + return res, nil +} + +func SaveUrls(filename string, urls []string) error { + file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to open file, %w", err) + } + + b, err := json.Marshal(urls) + if err != nil { + return fmt.Errorf("failed to marshal to json, %w", err) + } + + if _, err := file.Write(b); err != nil { + return fmt.Errorf("failed to write to file, %w", err) + } + return nil +} diff --git a/workflows/fetch.go b/workflows/fetch.go new file mode 100644 index 0000000..b353e85 --- /dev/null +++ b/workflows/fetch.go @@ -0,0 +1,114 @@ +package workflows + +import ( + "context" + "golang-lab4/models" + "golang-lab4/utils" + "time" + + "github.com/go-resty/resty/v2" + "github.com/spf13/viper" + "github.com/uptrace/bun" + "go.uber.org/zap" +) + +const ( + REFETCH_TICK = 5 * time.Second +) + +type Fetcher struct { + db *bun.DB + client *resty.Client + urls map[string]struct{} + doneCh chan string +} + +func NewFetcher(db *bun.DB) *Fetcher { + urlSlice, err := utils.LoadUrls(viper.GetString("savefile")) + if err != nil { + panic(err) + } + + urls := make(map[string]struct{}) + for _, url := range urlSlice { + urls[url] = struct{}{} + } + + return &Fetcher{ + db: db, + client: resty.New(), + urls: urls, + doneCh: make(chan string), + } +} + +func (self *Fetcher) fetch(url string) models.UrlState { + before := time.Now() + zap.L().Info("fetch", + zap.String("url", url)) + resp, err := self.client.R(). + Get(url) + + return models.UrlState{ + Url: url, + IsSuccess: (err == nil && resp.IsSuccess()), + Latency: uint(time.Now().Sub(before) / time.Millisecond), + } +} + +func (self *Fetcher) fetchAndSleep(url string) { + res := self.fetch(url) + + time.Sleep(REFETCH_TICK) + + _, err := self.db.NewInsert(). + Model(&res). + On("CONFLICT (url) DO UPDATE"). + Set("is_success = ?", res.IsSuccess). + Set("latency = ?", res.Latency). + Exec(context.Background()) + if err != nil { + panic(err) + } + self.doneCh <- url +} + +func (self *Fetcher) SyncFile() { + urlSlice := []string{} + for url := range self.urls { + urlSlice = append(urlSlice, url) + } + + err := utils.SaveUrls(viper.GetString("savefile"), urlSlice) + if err != nil { + panic(err) + } +} + +func (self *Fetcher) Add(url string) { + self.urls[url] = struct{}{} + self.SyncFile() + go self.fetchAndSleep(url) +} + +func (self *Fetcher) Remove(url string) { + delete(self.urls, url) + self.SyncFile() + self.db.NewDelete(). + Model((*models.UrlState)(nil)). + Where("url = ?", url). + Exec(context.Background()) +} + +func (self *Fetcher) Run() { + for url := range self.urls { + go self.fetchAndSleep(url) + } + + for { + url := <-self.doneCh + if _, ok := self.urls[url]; ok { + go self.fetchAndSleep(url) + } + } +}