Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
68a39bc476 | |||
74ecaf6803 | |||
72f08d6374 | |||
e342487580 | |||
de93420697 | |||
a3a93d69f6 | |||
a38261980e |
162
.gitignore
vendored
Normal file
162
.gitignore
vendored
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,goland,go
|
||||||
|
|
||||||
|
### Go ###
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
### GoLand ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### GoLand Patch ###
|
||||||
|
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||||
|
|
||||||
|
# *.iml
|
||||||
|
# modules.xml
|
||||||
|
# .idea/misc.xml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# Sonarlint plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||||
|
.idea/**/sonarlint/
|
||||||
|
|
||||||
|
# SonarQube Plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||||
|
.idea/**/sonarIssues.xml
|
||||||
|
|
||||||
|
# Markdown Navigator plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||||
|
.idea/**/markdown-navigator.xml
|
||||||
|
.idea/**/markdown-navigator-enh.xml
|
||||||
|
.idea/**/markdown-navigator/
|
||||||
|
|
||||||
|
# Cache file creation bug
|
||||||
|
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||||
|
.idea/$CACHE_FILE$
|
||||||
|
|
||||||
|
# CodeStream plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||||
|
.idea/codestream.xml
|
||||||
|
|
||||||
|
# Azure Toolkit for IntelliJ plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||||
|
.idea/**/azureSettings.xml
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go
|
||||||
|
/recover.bolt
|
32
cmd/main.go
Normal file
32
cmd/main.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/internal/app"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/internal/initialize"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger, err := zap.NewProduction()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := initialize.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("Failed to load config", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
if err = app.Run(ctx, *config, logger); err != nil {
|
||||||
|
logger.Fatal("App exited with error", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
25
go.mod
Normal file
25
go.mod
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
module gitea.pena/SQuiz/shutterstock
|
||||||
|
|
||||||
|
go 1.23.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/caarlos0/env/v8 v8.0.0
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
|
)
|
45
go.sum
Normal file
45
go.sum
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0=
|
||||||
|
github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo=
|
||||||
|
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/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||||
|
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
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-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
|
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
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=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
66
internal/app/app.go
Normal file
66
internal/app/app.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/internal/client"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/internal/controller"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/internal/initialize"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/internal/server/http"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/pkg/closer"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Error("Recovered from a panic", zap.Any("error", r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
shutdownGroup := closer.NewCloserGroup()
|
||||||
|
|
||||||
|
shutterStockClient := client.NewShutterStockClient(client.Deps{
|
||||||
|
ShutterStockApiToken: cfg.ShutterStockApiToken,
|
||||||
|
ShutterStockUrl: cfg.ShutterStockUrl,
|
||||||
|
Logger: logger,
|
||||||
|
})
|
||||||
|
|
||||||
|
shutterStockController := controller.NewShutterStockController(shutterStockClient)
|
||||||
|
|
||||||
|
srv := http.NewServer(http.ServerConfig{
|
||||||
|
Logger: logger,
|
||||||
|
Controllers: []http.Controller{shutterStockController},
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.Start(cfg.HTTPHost + ":" + cfg.HTTPPort); err != nil {
|
||||||
|
logger.Error("Server startup error", zap.Error(err))
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
srv.ListRoutes()
|
||||||
|
|
||||||
|
shutdownGroup.Add(closer.CloserFunc(srv.Shutdown))
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer timeoutCancel()
|
||||||
|
if err := shutdownGroup.Call(timeoutCtx); err != nil {
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
logger.Error("Shutdown timed out", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
logger.Error("Failed to shutdown services gracefully", zap.Error(err))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Application has stopped")
|
||||||
|
return nil
|
||||||
|
}
|
74
internal/client/shutterstock.go
Normal file
74
internal/client/shutterstock.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/internal/model"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
ShutterStockUrl string
|
||||||
|
ShutterStockApiToken string
|
||||||
|
Logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShutterStockClient struct {
|
||||||
|
shutterStockUrl string
|
||||||
|
shutterStockApiToken string
|
||||||
|
fiberClient *fiber.Client
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShutterStockClient(deps Deps) *ShutterStockClient {
|
||||||
|
fiberClient := fiber.AcquireClient()
|
||||||
|
return &ShutterStockClient{
|
||||||
|
shutterStockUrl: deps.ShutterStockUrl,
|
||||||
|
shutterStockApiToken: deps.ShutterStockApiToken,
|
||||||
|
fiberClient: fiberClient,
|
||||||
|
logger: deps.Logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShutterStockClient) ImgSearch(params map[model.ShutterStockParams]string) (*model.ImgSearchResp, error) {
|
||||||
|
url := fmt.Sprintf("%s/v2/images/search", s.shutterStockUrl)
|
||||||
|
|
||||||
|
if len(params) > 0 {
|
||||||
|
queryParams := make([]string, 0, len(params))
|
||||||
|
for key, value := range params {
|
||||||
|
queryParams = append(queryParams, fmt.Sprintf("%s=%s", key, value))
|
||||||
|
}
|
||||||
|
url = fmt.Sprintf("%s?%s", url, strings.Join(queryParams, "&"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(url)
|
||||||
|
|
||||||
|
agent := s.fiberClient.Get(url)
|
||||||
|
agent.Set("Authorization", fmt.Sprintf("Bearer %s", s.shutterStockApiToken))
|
||||||
|
agent.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
statusCode, resBody, errs := agent.Bytes()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
for _, err := range errs {
|
||||||
|
s.logger.Error("Error Img Search Shutter Stock", zap.Error(err))
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("request failed: %v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusCode != fiber.StatusOK {
|
||||||
|
errorMessage := fmt.Sprintf("received an incorrect response from Img Search Shutter Stock: %d", statusCode)
|
||||||
|
s.logger.Error(errorMessage, zap.Int("status", statusCode), zap.String("respBody", string(resBody)))
|
||||||
|
return nil, errors.New(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := new(model.ImgSearchResp)
|
||||||
|
if err := json.Unmarshal(resBody, resp); err != nil {
|
||||||
|
s.logger.Error("failed to unmarshal Img Search Shutter Stock response", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
71
internal/controller/shutterstock.go
Normal file
71
internal/controller/shutterstock.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/internal/client"
|
||||||
|
"gitea.pena/SQuiz/shutterstock/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShutterStockController struct {
|
||||||
|
shutterStockClient *client.ShutterStockClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShutterStockController(shutterStockClient *client.ShutterStockClient) *ShutterStockController {
|
||||||
|
return &ShutterStockController{
|
||||||
|
shutterStockClient: shutterStockClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShutterStockController) Register(router fiber.Router) {
|
||||||
|
router.Get("/", s.GetShutterStockData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShutterStockController) Name() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShutterStockController) GetShutterStockData(ctx *fiber.Ctx) error {
|
||||||
|
var request model.ImgSearchReq
|
||||||
|
if err := ctx.BodyParser(&request); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make(map[model.ShutterStockParams]string)
|
||||||
|
|
||||||
|
if request.Query != "" {
|
||||||
|
params[model.Query] = request.Query
|
||||||
|
}
|
||||||
|
if request.AspectRatio > 0 {
|
||||||
|
params[model.AspectRatio] = fmt.Sprintf("%f", request.AspectRatio)
|
||||||
|
}
|
||||||
|
if request.AspectRatioMax > 0 {
|
||||||
|
params[model.AspectRatioMax] = fmt.Sprintf("%f", request.AspectRatioMax)
|
||||||
|
}
|
||||||
|
if request.AspectRatioMin > 0 {
|
||||||
|
params[model.AspectRatioMin] = fmt.Sprintf("%f", request.AspectRatioMin)
|
||||||
|
}
|
||||||
|
if request.WidthFrom > 0 {
|
||||||
|
params[model.WidthFrom] = fmt.Sprintf("%d", request.WidthFrom)
|
||||||
|
}
|
||||||
|
if request.WidthTo > 0 {
|
||||||
|
params[model.WidthTo] = fmt.Sprintf("%d", request.WidthTo)
|
||||||
|
}
|
||||||
|
if request.HeightFrom > 0 {
|
||||||
|
params[model.HeightFrom] = fmt.Sprintf("%d", request.HeightFrom)
|
||||||
|
}
|
||||||
|
if request.HeightTo > 0 {
|
||||||
|
params[model.HeightTo] = fmt.Sprintf("%d", request.HeightTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) == 0 {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "need one or more params in request"})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.shutterStockClient.ImgSearch(params)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).JSON(resp)
|
||||||
|
}
|
25
internal/initialize/config.go
Normal file
25
internal/initialize/config.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package initialize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caarlos0/env/v8"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
HTTPHost string `env:"HTTP_HOST" envDefault:"localhost"`
|
||||||
|
HTTPPort string `env:"HTTP_PORT" envDefault:"8080"`
|
||||||
|
ShutterStockUrl string `env:"SHUTTER_STOCK_URL" envDefault:"https://api.shutterstock.com"`
|
||||||
|
ShutterStockApiToken string `env:"SHUTTER_STOCK_API_TOKEN" envDefault:"v2/MHhxanoxNXpDYkZSdFdtWjNZWktHSHEyOXYyZ3kzNUQvNDQzMDU4NjU5L2N1c3RvbWVyLzQvZXFCUnA3cXdORng4TWExZHJDa09iSERWWERLNzIyeFNobFVtOWhkR284eWFqdVlxN2UtNTR3WTN2eUdrZ1h4TE94Tnl6VmFhd0cxMmJhWTNQNDN0U01FZGVOdEtDa1dnVGVjVVVqcnNzX2RfM2lLcmlJTDVHZFFVLUNMcld4bjRpVVE0eWpHbkttWHJrenEycmVCOVF1blpxNHlDX3paNmUtenJQWFBNVUtZTGhtT0FlRDQ5dl9TaDU3S3lhSWExdEhndmtWOFdVcUNJcHFpVGJSZ1FhUS8wWEdQZ0hHUGtCdUppWWQ4SzNOTzhn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Print("No .env file found")
|
||||||
|
}
|
||||||
|
var config Config
|
||||||
|
if err := env.Parse(&config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &config, nil
|
||||||
|
}
|
33
internal/model/model.go
Normal file
33
internal/model/model.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type ShutterStockParams string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AspectRatio ShutterStockParams = "aspect_ratio"
|
||||||
|
AspectRatioMax ShutterStockParams = "aspect_ratio_max"
|
||||||
|
AspectRatioMin ShutterStockParams = "aspect_ratio_min"
|
||||||
|
WidthFrom ShutterStockParams = "width_from"
|
||||||
|
WidthTo ShutterStockParams = "width_to"
|
||||||
|
Query ShutterStockParams = "query"
|
||||||
|
HeightFrom ShutterStockParams = "height_from"
|
||||||
|
HeightTo ShutterStockParams = "height_to"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImgSearchReq struct {
|
||||||
|
AspectRatio float64 `json:"aspect_ratio"`
|
||||||
|
AspectRatioMax float64 `json:"aspect_ratio_max"`
|
||||||
|
AspectRatioMin float64 `json:"aspect_ratio_min"`
|
||||||
|
WidthFrom int `json:"width_from"`
|
||||||
|
WidthTo int `json:"width_to"`
|
||||||
|
HeightFrom int `json:"height_from"`
|
||||||
|
HeightTo int `json:"height_to"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImgSearchResp struct {
|
||||||
|
Data []ImgSearchData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImgSearchData struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
65
internal/server/http/http_server.go
Normal file
65
internal/server/http/http_server.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Logger *zap.Logger
|
||||||
|
Controllers []Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Logger *zap.Logger
|
||||||
|
Controllers []Controller
|
||||||
|
app *fiber.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(config ServerConfig) *Server {
|
||||||
|
app := fiber.New()
|
||||||
|
s := &Server{
|
||||||
|
Logger: config.Logger,
|
||||||
|
Controllers: config.Controllers,
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.registerRoutes()
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start(addr string) error {
|
||||||
|
if err := s.app.Listen(addr); err != nil {
|
||||||
|
s.Logger.Error("Failed to start server", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
|
return s.app.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) registerRoutes() {
|
||||||
|
for _, c := range s.Controllers {
|
||||||
|
router := s.app.Group(c.Name())
|
||||||
|
c.Register(router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Controller interface {
|
||||||
|
Register(router fiber.Router)
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ListRoutes() {
|
||||||
|
fmt.Println("Registered routes:")
|
||||||
|
for _, stack := range s.app.Stack() {
|
||||||
|
for _, route := range stack {
|
||||||
|
fmt.Printf("%s %s\n", route.Method, route.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
87
openapi.yaml
Normal file
87
openapi.yaml
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
openapi: 3.0.0
|
||||||
|
info:
|
||||||
|
title: ShutterStock Service API
|
||||||
|
version: 1.0.0
|
||||||
|
description: API для общения с ShutterStock.
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
summary: Поиск изображений на Shutterstock
|
||||||
|
description: Выполняет поиск изображений на Shutterstock по заданным параметрам
|
||||||
|
tags:
|
||||||
|
- Shutterstock
|
||||||
|
requestBody:
|
||||||
|
description: Параметры поиска изображений
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ImgSearchReq'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ImgSearchResp'
|
||||||
|
'400':
|
||||||
|
description: Bad request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: Internal server error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
ImgSearchReq:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
query:
|
||||||
|
type: string
|
||||||
|
aspect_ratio:
|
||||||
|
type: number
|
||||||
|
format: float
|
||||||
|
aspect_ratio_max:
|
||||||
|
type: number
|
||||||
|
format: float
|
||||||
|
aspect_ratio_min:
|
||||||
|
type: number
|
||||||
|
format: float
|
||||||
|
width_from:
|
||||||
|
type: integer
|
||||||
|
width_to:
|
||||||
|
type: integer
|
||||||
|
height_from:
|
||||||
|
type: integer
|
||||||
|
height_to:
|
||||||
|
type: integer
|
||||||
|
|
||||||
|
ImgSearchResp:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
description: Массив с результатами
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ImgSearchData'
|
||||||
|
|
||||||
|
ImgSearchData:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Идентификатор изображения
|
||||||
|
|
||||||
|
ErrorResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: Error
|
37
pkg/closer/closer.go
Normal file
37
pkg/closer/closer.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package closer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Closer interface {
|
||||||
|
Close(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloserFunc func(ctx context.Context) error
|
||||||
|
|
||||||
|
func (cf CloserFunc) Close(ctx context.Context) error {
|
||||||
|
return cf(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloserGroup struct {
|
||||||
|
closers []Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCloserGroup() *CloserGroup {
|
||||||
|
return &CloserGroup{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cg *CloserGroup) Add(c Closer) {
|
||||||
|
cg.closers = append(cg.closers, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cg *CloserGroup) Call(ctx context.Context) error {
|
||||||
|
var closeErr error
|
||||||
|
for i := len(cg.closers) - 1; i >= 0; i-- {
|
||||||
|
if err := cg.closers[i].Close(ctx); err != nil && closeErr == nil {
|
||||||
|
closeErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closeErr
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user