From ff96a3f674d4bc3ea463b245cecf9a29fc38ee05 Mon Sep 17 00:00:00 2001 From: Stanislav Demin Date: Sat, 3 May 2025 14:33:23 +0300 Subject: [PATCH 01/23] first commit --- .gitignore | 25 ++++ .golangci.yml | 70 +++++++++ .scripts/.sync | 0 .scripts/lint.sh | 17 +++ .scripts/sync.sh | 16 ++ .scripts/update.sh | 12 ++ LICENSE | 21 +++ Makefile | 34 +++++ README.md | 78 ++++++++++ build/Dockerfile | 39 +++++ cmd/previewer/main.go | 61 ++++++++ cmd/previewer/version.go | 27 ++++ configs/config.toml | 0 go.mod | 32 ++++ go.sum | 33 +++++ internal/file_modifier/file_modifier.go | 23 +++ internal/file_search/file_search.go | 52 +++++++ internal/lru_cache/cache.go | 89 ++++++++++++ internal/lru_cache/cache_test.go | 90 ++++++++++++ internal/lru_cache/list.go | 118 +++++++++++++++ internal/lru_cache/list_test.go | 65 +++++++++ internal/server/http/middleware.go | 28 ++++ internal/server/http/server.go | 185 ++++++++++++++++++++++++ pkg/config/config.go | 46 ++++++ pkg/errors/alert.go | 130 +++++++++++++++++ pkg/logger/logger.go | 36 +++++ 26 files changed, 1327 insertions(+) create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .scripts/.sync create mode 100755 .scripts/lint.sh create mode 100755 .scripts/sync.sh create mode 100755 .scripts/update.sh create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 build/Dockerfile create mode 100644 cmd/previewer/main.go create mode 100644 cmd/previewer/version.go create mode 100644 configs/config.toml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/file_modifier/file_modifier.go create mode 100644 internal/file_search/file_search.go create mode 100644 internal/lru_cache/cache.go create mode 100644 internal/lru_cache/cache_test.go create mode 100644 internal/lru_cache/list.go create mode 100644 internal/lru_cache/list_test.go create mode 100644 internal/server/http/middleware.go create mode 100644 internal/server/http/server.go create mode 100644 pkg/config/config.go create mode 100644 pkg/errors/alert.go create mode 100644 pkg/logger/logger.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f72f89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# 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 +go.work.sum + +# env file +.env diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8552d18 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,70 @@ +run: + tests: true + build-tags: + - bench + - !bench + +linters-settings: + funlen: + lines: 150 + statements: 80 + +issues: + exclude-rules: + - path: _test\.go + linters: + - errcheck + - dupl + - gocyclo + - gosec + - depguard + - path: .*\.go + linters: + - depguard + +linters: + disable-all: true + enable: + - asciicheck + - depguard + - dogsled + - dupl + - bodyclose + - durationcheck + - errorlint + - exhaustive + - funlen + - gci + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - gofmt + - gofumpt + - goheader + - goprintffuncname + - gosec + - gosimple + - govet + - importas + - ineffassign + - lll + - makezero + - misspell + - nestif + - nilerr + - noctx + - nolintlint + - prealloc + - predeclared + - revive + - staticcheck + - stylecheck + - tagliatelle + - thelper + - typecheck + - unconvert + - unparam + - unused + - whitespace diff --git a/.scripts/.sync b/.scripts/.sync new file mode 100644 index 0000000..e69de29 diff --git a/.scripts/lint.sh b/.scripts/lint.sh new file mode 100755 index 0000000..dba0737 --- /dev/null +++ b/.scripts/lint.sh @@ -0,0 +1,17 @@ +#!/bin/zsh + +sed -i.bak '/- deadcode/d' .golangci.yml +sed -i '' '/- unused/d' .golangci.yml +sed -i '' '/- structcheck/d' .golangci.yml + +for d in $(ls) +do + if [[ $d == hw* ]]; then + cd $d + echo "Lint ${d}..." + golangci-lint run ./... + cd .. + fi +done + +mv .golangci.yml.bak .golangci.yml diff --git a/.scripts/sync.sh b/.scripts/sync.sh new file mode 100755 index 0000000..6ab8c46 --- /dev/null +++ b/.scripts/sync.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +dst=$1 +if [[ ! -d "${dst}" ]]; then + echo "Usage: ./.scripts/sync.sh ." + echo "The destination dir should exist" + exit 1 +fi + +GLOBIGNORE=".:..:.git" +for f in *; do + [[ -d "${dst}/${f}" ]] && [[ ! -f "${dst}/${f}/.sync" ]] && continue + + echo "syncing ${f}..." + cp -R "${f}" "${dst}" +done diff --git a/.scripts/update.sh b/.scripts/update.sh new file mode 100755 index 0000000..69c6e78 --- /dev/null +++ b/.scripts/update.sh @@ -0,0 +1,12 @@ +#!/bin/zsh + +for d in $(ls) +do + if [[ $d == hw* ]]; then + cd $d + echo "Update deps in ${d}..." + go mod tidy + go get -t -u ./... + cd .. + fi +done diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a77f32b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Stas Demin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1553480 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +BIN := "./bin/previewer" +DOCKER_IMG="previewer:develop" + +GIT_HASH := $(shell git log --format="%h" -n 1) +LDFLAGS := -X main.release="develop" -X main.buildDate=$(shell date -u +%Y-%m-%dT%H:%M:%S) -X main.gitHash=$(GIT_HASH) + +build: + go build -v -o $(BIN) -ldflags "$(LDFLAGS)" ./cmd/previewer + +run: build + $(BIN) -config ./configs/config.toml + +build-img: + docker build \ + --build-arg=LDFLAGS="$(LDFLAGS)" \ + -t $(DOCKER_IMG) \ + -f build/Dockerfile . + +run-img: build-img + docker run $(DOCKER_IMG) + +version: build + $(BIN) version + +test: + go test -race ./internal/... + +install-lint-deps: + (which golangci-lint > /dev/null) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.64.6 + +lint: install-lint-deps + golangci-lint run ./... + +.PHONY: build run build-img run-img version test lint diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd6ce15 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# ТЗ на разработку сервиса "Превьювер изображений" + +## Общее описание +Сервис предназначен для изготовления preview (создания изображения +с новыми размерами на основе имеющегося изображения). + +#### Пример превьюшек в папке [examples](./examples/image-previewer) + +## Архитектура +Сервис представляет собой web-сервер (прокси), загружающий изображения, +масштабирующий/обрезающий их до нужного формата и возвращающий пользователю. + +## Основной обработчик +http://cut-service.com/fill/300/200/raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg + +<---- микросервис ----><- размеры превью -><--------- URL исходного изображения ---------------------------------> + +В URL выше мы видим: +- http://cut-service.com/fill/300/200/ - endpoint нашего сервиса, + в котором 300x200 - это размеры финального изображения. +- https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg - + адрес исходного изображения; сервис должен скачать его, произвести resize, закэшировать и отдать клиенту. + +Сервис должен получить URL исходного изображения, скачать его, изменить до необходимых размеров и вернуть как HTTP-ответ. + +- Работаем только с HTTP. +- Ошибки удалённого сервиса или проксируем как есть, или логируем и отвечаем клиенту 502 Bad Gateway. +- Поддержка JPEG является минимальным и достаточным требованием. + +**Важно**: необходимо проксировать все заголовки исходного HTTP запроса к целевому сервису (raw.githubusercontent.com в примере). + +Сервис должен сохранить (кэшировать) полученное preview на локальном диске и при повторном запросе +отдавать изображение с диска, без запроса к удаленному HTTP-серверу. + +Поскольку размер места для кэширования ограничен, то для удаления редко используемых изображений +необходимо использовать алгоритм **"Least Recent Used"**. + +## Конфигурация +Основной параметр конфигурации сервиса - разрешенный размер LRU-кэша. + +Он может измеряться как количеством закэшированных изображений, так и суммой их байт (на выбор разработчика). + +## Развертывание +Развертывание микросервиса должно осуществляться командой `make run` (внутри `docker compose up`) +в директории с проектом. + +## Тестирование +Реализацию алгоритма LRU нужно покрыть unit-тестами. + +Для интеграционного тестирования можно использовать контейнер с Nginx в качестве удаленного HTTP-сервера, +раздающего вам заданный набор изображений. + +Необходимо проверить работу сервера в разных сценариях: +* картинка найдена в кэше; +* удаленный сервер не существует; +* удаленный сервер существует, но изображение не найдено (404 Not Found); +* удаленный сервер существует, но изображение не изображение, а скажем, exe-файл; +* удаленный сервер вернул ошибку; +* удаленный сервер вернул изображение; +* изображение меньше, чем нужный размер; + и пр. + +## Разбалловка +Максимум - **15 баллов** +(при условии выполнения [обязательных требований](./README.md)): + +* Реализован HTTP-сервер, проксирующий запросы к удаленному серверу - 2 балла. +* Реализована нарезка изображений - 2 балла. +* Кэширование нарезанных изображений на диске - 1 балл. +* Ограничение кэша одним из способов (LRU кэш) - 1 балл. +* Прокси сервер правильно передает заголовки запроса - 1 балл. +* Написаны интеграционные тесты - 3 балла. +* Тесты адекватны и полностью покрывают функциональность - 1 балл. +* Проект возможно собрать через `make build`, запустить через `make run` + и протестировать через `make test` - 1 балл. +* Понятность и чистота кода - до 3 баллов. + +#### Зачёт от 10 баллов \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..830eef9 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,39 @@ +# Собираем в гошке +FROM golang:1.23 AS build + +ENV BIN_FILE=/opt/previewer/previewer-app +ENV CODE_DIR=/go/src/ + +WORKDIR ${CODE_DIR} + +# Кэшируем слои с модулями +COPY go.mod . +COPY go.sum . +RUN go mod download + +COPY . ${CODE_DIR} + +# Собираем статический бинарник Go (без зависимостей на Си API), +# иначе он не будет работать в alpine образе. +ARG LDFLAGS +RUN CGO_ENABLED=0 go build \ + -ldflags "$LDFLAGS" \ + -o ${BIN_FILE} cmd/calendar/* + +# На выходе тонкий образ +FROM alpine:3.9 + +LABEL ORGANIZATION="OTUS Online Education" +LABEL SERVICE="previewer" +LABEL MAINTAINERS="otdupli@gmail.com" + +ENV BIN_FILE=/opt/previewer/previewer-app +COPY --from=build ${BIN_FILE} ${BIN_FILE} + +ENV CONFIG_FILE=/etc/previewer/config.toml + +COPY ./configs/config.toml ${CONFIG_FILE} + +RUN chmod +x ${BIN_FILE} + +CMD ["/opt/previewer/previewer-app", "-config", "/etc/previewer/config.toml"] \ No newline at end of file diff --git a/cmd/previewer/main.go b/cmd/previewer/main.go new file mode 100644 index 0000000..57b9f4d --- /dev/null +++ b/cmd/previewer/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "fmt" + lrucache "github.com/DEMAxx/project_work/internal/lru_cache" + internalhttp "github.com/DEMAxx/project_work/internal/server/http" + "net" + "os" + "os/signal" + "syscall" + "time" + + "github.com/DEMAxx/project_work/pkg/config" + "github.com/DEMAxx/project_work/pkg/logger" +) + +func main() { + const op = "cmd.previewer.main" + + cnf := config.MustLoad() + + logs := logger.MustSetupLogger(config.AppName, cnf.Env, cnf.Debug || cnf.Local, cnf.LogLevel) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ctx = logs.WithContext(ctx) + + cache := lrucache.NewCache(cnf.Capability) + + server := internalhttp.NewServer( + &logs, + net.JoinHostPort(cnf.Server.Host, cnf.Server.Port), + cache, + cnf, + ) + + ctx, cancel = signal.NotifyContext(ctx, + syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + defer cancel() + + if err := server.Start(ctx); err != nil { + logs.Error().Msg(fmt.Sprintf("failed to start http server: %s", err.Error())) + cancel() + os.Exit(1) //nolint:gocritic + } + + logs.Info().Msg("calendar is running...") + + go func() { + <-ctx.Done() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + + if err := server.Stop(ctx); err != nil { + logs.Error().Msg(fmt.Sprintf("failed to stop http server: %s", err.Error())) + } + }() +} diff --git a/cmd/previewer/version.go b/cmd/previewer/version.go new file mode 100644 index 0000000..7404eee --- /dev/null +++ b/cmd/previewer/version.go @@ -0,0 +1,27 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" +) + +var ( + release = "UNKNOWN" + buildDate = "UNKNOWN" + gitHash = "UNKNOWN" +) + +func printVersion() { + if err := json.NewEncoder(os.Stdout).Encode(struct { + Release string + BuildDate string + GitHash string + }{ + Release: release, + BuildDate: buildDate, + GitHash: gitHash, + }); err != nil { + fmt.Printf("error while decode version info: %v\n", err) + } +} diff --git a/configs/config.toml b/configs/config.toml new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..712a329 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module github.com/DEMAxx/project_work + +go 1.24.0 + +require ( + github.com/gofiber/fiber/v2 v2.52.6 + github.com/google/uuid v1.6.0 + github.com/h2non/bimg v1.1.9 + github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/rs/zerolog v1.34.0 + github.com/stretchr/testify v1.10.0 + golang.org/x/text v0.24.0 +) + +require ( + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/compress v1.17.9 // 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.16 // indirect + github.com/pmezard/go-difflib v1.0.0 // 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 + golang.org/x/sys v0.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eebef92 --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/h2non/bimg v1.1.9/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/internal/file_modifier/file_modifier.go b/internal/file_modifier/file_modifier.go new file mode 100644 index 0000000..cbcffdf --- /dev/null +++ b/internal/file_modifier/file_modifier.go @@ -0,0 +1,23 @@ +package file_modifier + +import "github.com/h2non/bimg" + +// ResizeImage resizes an image to the specified width and height. +func ResizeImage(inputPath string, width int, height int) ([]byte, error) { + image, err := bimg.Read(inputPath) + if err != nil { + return nil, err + } + + resizedImage, err := bimg.NewImage(image).Process(bimg.Options{ + Width: width, + Height: height, + Type: bimg.JPEG, + }) + + if err != nil { + return nil, err + } + + return resizedImage, nil +} diff --git a/internal/file_search/file_search.go b/internal/file_search/file_search.go new file mode 100644 index 0000000..cb26787 --- /dev/null +++ b/internal/file_search/file_search.go @@ -0,0 +1,52 @@ +package file_search + +import ( + "fmt" + "github.com/rs/zerolog" + "io" + "net/http" + "os" + "strings" +) + +func FetchFileFromURL(imageUrl, outputPath string, logger *zerolog.Logger) (*http.Response, error) { + if !strings.HasPrefix(imageUrl, "http://") && !strings.HasPrefix(imageUrl, "https://") { + imageUrl = fmt.Sprintf("https://%s", imageUrl) + } + + resp, err := http.Get(imageUrl) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + logger.Error().Msg("failed to close response body") + } + }(resp.Body) + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch file: %s", resp.Status) + } + + // Create the output file + outFile, err := os.Create(outputPath) + if err != nil { + return nil, err + } + + defer func(outFile *os.File) { + err := outFile.Close() + if err != nil { + logger.Error().Msg("failed to close response body") + } + }(outFile) + + _, err = io.Copy(outFile, resp.Body) + + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/internal/lru_cache/cache.go b/internal/lru_cache/cache.go new file mode 100644 index 0000000..87018f3 --- /dev/null +++ b/internal/lru_cache/cache.go @@ -0,0 +1,89 @@ +package lrucache + +import "sync" + +type Key string + +type Cache interface { + Set(key Key, value interface{}) bool + Get(key Key) (interface{}, bool) + Clear() +} + +type lruCache struct { + capacity int + queue List + items map[Key]*cacheItem +} + +type cacheItem struct { + key Key + value interface{} + item *ListItem +} + +var mutex sync.Mutex + +func (lruCache *lruCache) Set(key Key, value interface{}) bool { + mutex.Lock() + defer mutex.Unlock() + + item, ok := lruCache.items[key] + + if ok { + item.value = value + lruCache.queue.MoveToFront(item.item) + + return true + } + + if lruCache.capacity == lruCache.queue.Len() { + back := lruCache.queue.Back() + valKey, ok := back.Value.(Key) + + if !ok { + return false + } + delete(lruCache.items, valKey) + lruCache.queue.Remove(back) + } + newItem := lruCache.queue.PushFront(key) + + lruCache.items[key] = &cacheItem{ + key: key, + value: value, + item: newItem, + } + + return false +} + +func (lruCache *lruCache) Get(key Key) (interface{}, bool) { + mutex.Lock() + defer mutex.Unlock() + + item, ok := lruCache.items[key] + + if !ok { + return nil, false + } + + lruCache.queue.MoveToFront(item.item) + return item.value, true +} + +func (lruCache *lruCache) Clear() { + mutex.Lock() + defer mutex.Unlock() + + lruCache.queue = new(list) + lruCache.items = make(map[Key]*cacheItem, lruCache.capacity) +} + +func NewCache(capacity int) Cache { + return &lruCache{ + capacity: capacity, + queue: new(list), + items: make(map[Key]*cacheItem, capacity), + } +} diff --git a/internal/lru_cache/cache_test.go b/internal/lru_cache/cache_test.go new file mode 100644 index 0000000..b5d53c1 --- /dev/null +++ b/internal/lru_cache/cache_test.go @@ -0,0 +1,90 @@ +package lrucache + +import ( + "math/rand" + "strconv" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCache(t *testing.T) { + t.Run("empty cache", func(t *testing.T) { + c := NewCache(10) + + _, ok := c.Get("aaa") + require.False(t, ok) + + _, ok = c.Get("bbb") + require.False(t, ok) + }) + + t.Run("simple", func(t *testing.T) { + c := NewCache(5) + + wasInCache := c.Set("aaa", 100) + require.False(t, wasInCache) + + wasInCache = c.Set("bbb", 200) + require.False(t, wasInCache) + + val, ok := c.Get("aaa") + require.True(t, ok) + require.Equal(t, 100, val) + + val, ok = c.Get("bbb") + require.True(t, ok) + require.Equal(t, 200, val) + + wasInCache = c.Set("aaa", 300) + require.True(t, wasInCache) + + val, ok = c.Get("aaa") + require.True(t, ok) + require.Equal(t, 300, val) + + val, ok = c.Get("ccc") + require.False(t, ok) + require.Nil(t, val) + }) + + t.Run("purge logic", func(t *testing.T) { + c := NewCache(1) + + for _, v := range [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9} { + c.Set(Key(strconv.Itoa(v)), v) + } + + x, _ := c.Get(Key(strconv.Itoa(9))) + + require.Equal(t, 9, x) + + x, y := c.Get(Key(strconv.Itoa(1))) + + require.Nil(t, x) + require.False(t, y) + }) +} + +func TestCacheMultithreading(_ *testing.T) { + c := NewCache(10) + wg := &sync.WaitGroup{} + wg.Add(2) + + go func() { + defer wg.Done() + for i := 0; i < 1_000_000; i++ { + c.Set(Key(strconv.Itoa(i)), i) + } + }() + + go func() { + defer wg.Done() + for i := 0; i < 1_000_000; i++ { + c.Get(Key(strconv.Itoa(rand.Intn(1_000_000)))) + } + }() + + wg.Wait() +} diff --git a/internal/lru_cache/list.go b/internal/lru_cache/list.go new file mode 100644 index 0000000..a7dcb6d --- /dev/null +++ b/internal/lru_cache/list.go @@ -0,0 +1,118 @@ +package lrucache + +type List interface { + Len() int + Front() *ListItem + Back() *ListItem + PushFront(v interface{}) *ListItem + PushBack(v interface{}) *ListItem + Remove(i *ListItem) + MoveToFront(i *ListItem) +} + +type ListItem struct { + Value interface{} + Next *ListItem + Prev *ListItem +} + +type list struct { + len int + front *ListItem + back *ListItem +} + +func (l list) Len() int { + return l.len +} + +func (l list) Front() *ListItem { + return l.front +} + +func (l list) Back() *ListItem { + return l.back +} + +func (l *list) PushFront(v interface{}) *ListItem { + item := new(ListItem) + item.Value = v + item.Next = l.front + + if l.len == 0 { + l.back = item + } else { + l.front.Prev = item + } + + l.front = item + l.len++ + + return item +} + +func (l *list) PushBack(v interface{}) *ListItem { + item := new(ListItem) + item.Value = v + + if l.len == 0 { + l.front = item + } else { + l.back.Next = item + } + + item.Prev = l.back + l.back = item + l.len++ + + return item +} + +func (l *list) Remove(item *ListItem) { + if item == nil { + panic("ListItem is nil") + } + + if item.Prev != nil { + item.Prev.Next = item.Next + } else { + l.front = item.Next + } + + if item.Next != nil { + item.Next.Prev = item.Prev + } else { + l.back = item.Prev + } + + item.Next = nil + item.Prev = nil + l.len-- +} + +func (l *list) MoveToFront(item *ListItem) { + exNext := item.Next + exPrev := item.Prev + + if exPrev == nil { + return + } + + if exNext == nil { + exPrev.Next = nil + l.back = exPrev + } else { + exPrev.Next = item.Next + exNext.Prev = item.Prev + } + + exFront := l.Front() + exFront.Prev = item + item.Next = exFront + item.Prev = nil + l.front = item +} + +func NewList() List { + return new(list) +} diff --git a/internal/lru_cache/list_test.go b/internal/lru_cache/list_test.go new file mode 100644 index 0000000..44b7567 --- /dev/null +++ b/internal/lru_cache/list_test.go @@ -0,0 +1,65 @@ +package lrucache + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestList(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + l := NewList() + + require.Equal(t, 0, l.Len()) + require.Nil(t, l.Front()) + require.Nil(t, l.Back()) + }) + + t.Run("complex", func(t *testing.T) { + l := NewList() + + l.PushFront(10) // [10] + l.PushBack(20) // [10, 20] + l.PushBack(30) // [10, 20, 30] + require.Equal(t, 3, l.Len()) + + middle := l.Front().Next // 20 + l.Remove(middle) // [10, 30] + require.Equal(t, 2, l.Len()) + + for i, v := range [...]int{40, 50, 60, 70, 80} { + if i%2 == 0 { + l.PushFront(v) + } else { + l.PushBack(v) + } + } // [80, 60, 40, 10, 30, 50, 70] + + require.Equal(t, 7, l.Len()) + require.Equal(t, 80, l.Front().Value) + require.Equal(t, 70, l.Back().Value) + + l.MoveToFront(l.Front()) // [80, 60, 40, 10, 30, 50, 70] + l.MoveToFront(l.Back()) // [70, 80, 60, 40, 10, 30, 50] + + elems := make([]int, 0, l.Len()) + for i := l.Front(); i != nil; i = i.Next { + elems = append(elems, i.Value.(int)) + } + require.Equal(t, []int{70, 80, 60, 40, 10, 30, 50}, elems) + }) +} + +func TestListMove(t *testing.T) { + t.Run("move", func(t *testing.T) { + l := NewList() + + for _, v := range [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9} { + l.PushBack(v) + l.MoveToFront(l.Back()) + } + + require.Equal(t, 9, l.Front().Value) + require.Equal(t, 1, l.Back().Value) + }) +} diff --git a/internal/server/http/middleware.go b/internal/server/http/middleware.go new file mode 100644 index 0000000..c3ba0e7 --- /dev/null +++ b/internal/server/http/middleware.go @@ -0,0 +1,28 @@ +package internalhttp + +import ( + "fmt" + "github.com/rs/zerolog" + "net/http" + "time" +) + +func LoggingMiddleware(next http.Handler, logg *zerolog.Logger) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + clientIP := r.RemoteAddr + dateTime := time.Now().Format(time.RFC3339) + method := r.Method + path := r.URL.Path + httpVersion := r.Proto + userAgent := r.Header.Get("User-Agent") + + logg.Info().Msg( + fmt.Sprintf( + "Client IP: %s, DateTime: %s, Method: %s, Path: %s, HTTP Version: %s, User Agent: %s", + clientIP, dateTime, method, path, httpVersion, userAgent, + ), + ) + + next.ServeHTTP(w, r) + }) +} diff --git a/internal/server/http/server.go b/internal/server/http/server.go new file mode 100644 index 0000000..038bf28 --- /dev/null +++ b/internal/server/http/server.go @@ -0,0 +1,185 @@ +package internalhttp + +import ( + "context" + "errors" + "fmt" + "github.com/DEMAxx/project_work/internal/file_modifier" + "github.com/DEMAxx/project_work/internal/file_search" + lrucache "github.com/DEMAxx/project_work/internal/lru_cache" + "github.com/DEMAxx/project_work/pkg/config" + "github.com/google/uuid" + "github.com/rs/zerolog" + "io" + "net/http" + "strconv" + "strings" + "time" +) + +type Server struct { + httpServer *http.Server + grpcHostAndPort string + logger *zerolog.Logger + cache lrucache.Cache +} + +func NewServer(logger *zerolog.Logger, hostAndPort string, cache lrucache.Cache, cnf *config.Config) *Server { + mux := http.NewServeMux() + + mux.Handle("/hello", LoggingMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + clientIP := r.RemoteAddr + dateTime := time.Now().Format(time.RFC3339) + method := r.Method + path := r.URL.Path + httpVersion := r.Proto + userAgent := r.Header.Get("User-Agent") + + logger.Info().Msg( + fmt.Sprintf( + "Client IP: %s, DateTime: %s, Method: %s, Path: %s, HTTP Version: %s, User Agent: %s", + clientIP, dateTime, method, path, httpVersion, userAgent, + ), + ) + + write, err := w.Write([]byte("Hello, World!")) + if err != nil { + return + } + logger.Info().Msg(fmt.Sprintf("response: %d", write)) + }), logger)) + + mux.Handle("/fill/", LoggingMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path[len("/fill/"):] + parts := strings.Split(path, "/") + + if len(parts) < 3 { + http.Error(w, "Invalid URL format", 400) + return + } + + height, width, imageUrl := parts[0], parts[1], strings.Join(parts[2:], "/") + + logger.Info().Msg( + fmt.Sprintf( + "Extracted vars - height: %s, width: %s, image url: %s", height, width, imageUrl, + ), + ) + + if !strings.HasSuffix(imageUrl, ".jpg") { + http.Error(w, "Invalid image URL format. Only .jpg files are supported.", http.StatusBadRequest) + return + } + + cacheKey := lrucache.Key(fmt.Sprintf("%s_%s_%s", width, height, imageUrl)) + cachedImage, found := cache.Get(cacheKey) + + if found { + logger.Info().Msg(fmt.Sprintf("Image retrieved from cache: %s", cacheKey)) + + w.Header().Set("Content-Type", "image/jpeg") + w.WriteHeader(http.StatusOK) + _, err := w.Write(cachedImage.([]byte)) + if err != nil { + logger.Error().Msg("Failed to write cached image to response") + } + return + } + + uid := uuid.New() + fetchedFilePath := fmt.Sprintf("%s/%s_%s_%s.jpg", cnf.UploadPath, width, height, uid) + + if resp, err := file_search.FetchFileFromURL(imageUrl, fetchedFilePath, logger); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } else { + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + logger.Error().Msg("failed to close response body") + } + }(resp.Body) + if resp.StatusCode != http.StatusOK { + http.Error(w, "Failed to fetch image.", http.StatusInternalServerError) + return + } + } + + // Resize the image + widthInt, err := strconv.Atoi(width) + if err != nil { + http.Error(w, "Invalid width value", http.StatusBadRequest) + return + } + + heightInt, err := strconv.Atoi(height) + if err != nil { + http.Error(w, "Invalid height value", http.StatusBadRequest) + return + } + + ResizedImage, err := file_modifier.ResizeImage(fetchedFilePath, widthInt, heightInt) + + if err != nil { + http.Error(w, "Failed to modify image.", http.StatusInternalServerError) + return + } + + if err := cache.Set(cacheKey, ResizedImage); err { + http.Error(w, "Failed to store image in cache.", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "image/jpeg") + w.WriteHeader(http.StatusOK) + _, err = w.Write(ResizedImage) + if err != nil { + logger.Error().Msg("Failed to write response body") + return + } + }), logger)) + + return &Server{ + httpServer: &http.Server{ + Addr: hostAndPort, + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, + }, + logger: logger, + cache: cache, + } +} + +func (s *Server) Start(ctx context.Context) error { + s.logger.Info().Msg("Starting HTTP server...") + + // Start HTTP server + go func() { + s.logger.Info().Msg("HTTP server start...") + + if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Error().Msg(fmt.Sprintf("HTTP server ListenAndServe: %s", err.Error())) + } + + s.logger.Info().Msg("HTTP server started") + }() + + <-ctx.Done() + return s.Stop(ctx) +} + +func (s *Server) Stop(ctx context.Context) error { + s.logger.Info().Msg("Stopping HTTP server...") + + // Stop HTTP server + shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + if err := s.httpServer.Shutdown(shutdownCtx); err != nil { + s.logger.Error().Msg(fmt.Sprintf("HTTP server Shutdown: %s", err.Error())) + return err + } + + s.logger.Info().Msg("HTTP server stopped") + return nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..ac278ff --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,46 @@ +package config + +import ( + "github.com/ilyakaznacheev/cleanenv" +) + +const AppName = "previewer" +const AppPort = 8000 +const ProxyHost = "http://localhost:8080" + +type Env struct { + Path string `env:"ENV_PATH" env-default:"./configs/config.toml"` +} + +type Config struct { + Timeout struct { + Read byte `env:"TIMEOUT_READ" env-default:"5"` + Write byte `env:"TIMEOUT_WRITE" env-default:"5"` + Shutdown byte `env:"TIMEOUT_SHUTDOWN" env-default:"3"` + } + Server struct { + Host string `env:"SERVER_HOST" env-default:"localhost"` + Port string `env:"SERVER_PORT" env-default:"8000"` + } + Capability int `env:"CAPABILITY" env-default:"10"` + Debug bool `env:"APP_DEBUG" env-default:"true"` + Env string `env:"APP_ENV" env-default:"local"` + Local bool `env:"LOCAL"` + LogLevel string `env:"LOG_LEVEL" env-default:"info"` + UploadPath string `env:"UPLOAD_PATH" env-default:"/tmp"` +} + +func MustLoad() *Config { + env := Env{} + cfg := Config{} + + if err := cleanenv.ReadEnv(&env); err != nil { + panic("cannot read env: " + err.Error()) + } + + if err := cleanenv.ReadConfig(env.Path, &cfg); err != nil { + panic("cannot read config: " + err.Error()) + } + + return &cfg +} diff --git a/pkg/errors/alert.go b/pkg/errors/alert.go new file mode 100644 index 0000000..9590707 --- /dev/null +++ b/pkg/errors/alert.go @@ -0,0 +1,130 @@ +package app_errors + +import ( + "context" + "fmt" + "github.com/gofiber/fiber/v2" + "golang.org/x/text/message" +) + +type AlertType string + +const ( + AlertCriticalType AlertType = "critical" + AlertDangerType AlertType = "danger" + AlertWarningType AlertType = "warning" + AlertInfoType AlertType = "info" + AlertSuccessType AlertType = "success" +) + +type Alert struct { + Type AlertType + Code string + Message string + Source string + Params map[string]any + Meta map[string]any + Recoverable bool + Status int +} + +func (a *Alert) Error() string { + return fmt.Sprintf("%s : %s", a.Code, a.Message) +} +func (a *Alert) IsRecoverable() bool { + return a.Recoverable +} + +func (a *Alert) FillStatus(status int) { + if a.Status == 0 { + a.Status = status + } +} + +func (a *Alert) FillMessage(p *message.Printer) { + if a.Message == "" { + a.Message = p.Sprintf(fmt.Sprintf("alerts.%s", a.Code)) + } +} + +func (a *Alert) Alert(ctx context.Context) *Alert { + return a +} + +func (a *Alert) HasErrors() bool { + return a.Type != AlertSuccessType +} + +type Alerts []*Alert + +func (a Alerts) Error() (s string) { + for _, alert := range a { + s += alert.Error() + "\n" + } + return +} + +func (a Alerts) IsRecoverable() bool { + for _, alert := range a { + if !alert.IsRecoverable() { + return false + } + } + return true +} + +func (a Alerts) FiberAnswer(p *message.Printer) (int, fiber.Map) { + status := fiber.StatusInternalServerError + + for i := range a { + a[i].FillMessage(p) + if a[i].Status != 0 { + status = a[i].Status + } + } + + return status, fiber.Map{"alerts": a} +} + +func (a Alerts) FillStatus(status int) { + for i := range a { + a[i].FillStatus(status) + } +} + +func (a Alerts) FiberMap() fiber.Map { + return fiber.Map{"alerts": a} +} + +func (a Alerts) HasErrors() bool { + for _, alert := range a { + if alert.HasErrors() { + return true + } + } + + return false +} + +func (a Alerts) FillSource(val string) { + for i := range a { + if a[i].Source == "" { + a[i].Source = val + } + } +} + +type AlertConvertable interface { + Alert(ctx context.Context) *Alert + Error() string +} + +type AlertRecoverable interface { + IsRecoverable() bool + Error() string +} + +type AlertSuccess interface { + HasErrors() bool + Error() string +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..d968fa8 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,36 @@ +package logger + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "os" + "time" +) + +var AppName = "undefined" + +func MustSetupLogger(app, stage string, debug bool, level string) zerolog.Logger { + zerolog.MessageFieldName = "rest" + zerolog.LevelFieldName = "severity" + zerolog.TimestampFieldName = "timestamp" + zerolog.TimeFieldFormat = time.RFC3339Nano + AppName = app + + var logs zerolog.Logger + + if debug { + logs = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + } else { + logs = log.Output(os.Stderr) + } + + parsedLvl, err := zerolog.ParseLevel(level) + + if err != nil { + panic(err) + } + + log.Logger = logs.Level(parsedLvl).With().Str("service", app).Str("stage", stage).Logger() + + return log.Logger +} From 6515d9dde35460b524e914981b4b075465d5131f Mon Sep 17 00:00:00 2001 From: Stanislav Demin Date: Sun, 4 May 2025 14:11:55 +0300 Subject: [PATCH 02/23] add go sum and crop mode --- go.sum | 24 ++++++++++++++++++++++++ internal/file_modifier/file_modifier.go | 1 + 2 files changed, 25 insertions(+) diff --git a/go.sum b/go.sum index eebef92..0166a8c 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,57 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +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/h2non/bimg v1.1.9 h1:WH20Nxko9l/HFm4kZCA3Phbgu2cbHvYzxwxn9YROEGg= github.com/h2non/bimg v1.1.9/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +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.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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= 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/internal/file_modifier/file_modifier.go b/internal/file_modifier/file_modifier.go index cbcffdf..ef5c045 100644 --- a/internal/file_modifier/file_modifier.go +++ b/internal/file_modifier/file_modifier.go @@ -12,6 +12,7 @@ func ResizeImage(inputPath string, width int, height int) ([]byte, error) { resizedImage, err := bimg.NewImage(image).Process(bimg.Options{ Width: width, Height: height, + Crop: true, Type: bimg.JPEG, }) From 401cd17868d6a450a4a093736957cf689481c7dd Mon Sep 17 00:00:00 2001 From: demin Date: Thu, 22 May 2025 21:28:35 +0300 Subject: [PATCH 03/23] fix: file search test and docker build --- .scripts/.sync | 0 build/Dockerfile | 19 ++-- cmd/previewer/main.go | 11 +- internal/file_search/file_search_test.go | 37 +++++++ pkg/config/config.go | 15 +-- pkg/errors/alert.go | 130 ----------------------- 6 files changed, 61 insertions(+), 151 deletions(-) delete mode 100644 .scripts/.sync create mode 100644 internal/file_search/file_search_test.go delete mode 100644 pkg/errors/alert.go diff --git a/.scripts/.sync b/.scripts/.sync deleted file mode 100644 index e69de29..0000000 diff --git a/build/Dockerfile b/build/Dockerfile index 830eef9..bf5fbad 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,8 +1,9 @@ # Собираем в гошке -FROM golang:1.23 AS build +FROM golang:1.24 AS build ENV BIN_FILE=/opt/previewer/previewer-app ENV CODE_DIR=/go/src/ +ENV CC=gcc WORKDIR ${CODE_DIR} @@ -13,15 +14,21 @@ RUN go mod download COPY . ${CODE_DIR} -# Собираем статический бинарник Go (без зависимостей на Си API), -# иначе он не будет работать в alpine образе. +RUN apt-get update && \ + apt-get -qq install -y libvips-dev + +# Собираем статический бинарник Go ARG LDFLAGS -RUN CGO_ENABLED=0 go build \ +RUN CGO_ENABLED=1 PKG_CONFIG_PATH="/usr/lib/pkgconfig" go build \ -ldflags "$LDFLAGS" \ - -o ${BIN_FILE} cmd/calendar/* + -o ${BIN_FILE} cmd/previewer/* # На выходе тонкий образ -FROM alpine:3.9 +FROM build AS base + +ENV CGO_ENABLED=1 +ENV TARGETOS=linux +ENV TARGETARCH=amd64 LABEL ORGANIZATION="OTUS Online Education" LABEL SERVICE="previewer" diff --git a/cmd/previewer/main.go b/cmd/previewer/main.go index 57b9f4d..73e507a 100644 --- a/cmd/previewer/main.go +++ b/cmd/previewer/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "flag" "fmt" lrucache "github.com/DEMAxx/project_work/internal/lru_cache" internalhttp "github.com/DEMAxx/project_work/internal/server/http" @@ -15,10 +16,16 @@ import ( "github.com/DEMAxx/project_work/pkg/logger" ) +var configFile string + +func init() { + flag.StringVar(&configFile, "config", "/etc/calendar/config.toml", "Path to configuration file") +} + func main() { - const op = "cmd.previewer.main" + flag.Parse() - cnf := config.MustLoad() + cnf := config.MustLoad(configFile) logs := logger.MustSetupLogger(config.AppName, cnf.Env, cnf.Debug || cnf.Local, cnf.LogLevel) diff --git a/internal/file_search/file_search_test.go b/internal/file_search/file_search_test.go new file mode 100644 index 0000000..c04554e --- /dev/null +++ b/internal/file_search/file_search_test.go @@ -0,0 +1,37 @@ +package file_search + +import ( + "github.com/DEMAxx/project_work/pkg/logger" + "github.com/stretchr/testify/require" + "net/http" + "os" + "path/filepath" + "testing" +) + +func TestFileSearch(t *testing.T) { + tmpDir := os.TempDir() + outputPath := filepath.Join(tmpDir, "output") + logs := logger.MustSetupLogger("previewer", "Test", true, "info") + + t.Run("success", func(t *testing.T) { + r, err := FetchFileFromURL("https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg", outputPath, &logs) + + require.NoError(t, err) + require.NotNil(t, r) + require.True(t, r.StatusCode == http.StatusOK) + }) + + t.Run("wrong address", func(t *testing.T) { + _, err := FetchFileFromURL("https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/not_gopher_original.jpg", outputPath, &logs) + + require.Error(t, err) + }) + + t.Run("not found", func(t *testing.T) { + _, err := FetchFileFromURL("localhost:9999/image.png", outputPath, &logs) + require.Error(t, err) + require.ErrorContains(t, err, "connection refused") + }) + +} diff --git a/pkg/config/config.go b/pkg/config/config.go index ac278ff..914ea5c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,12 +5,6 @@ import ( ) const AppName = "previewer" -const AppPort = 8000 -const ProxyHost = "http://localhost:8080" - -type Env struct { - Path string `env:"ENV_PATH" env-default:"./configs/config.toml"` -} type Config struct { Timeout struct { @@ -30,15 +24,10 @@ type Config struct { UploadPath string `env:"UPLOAD_PATH" env-default:"/tmp"` } -func MustLoad() *Config { - env := Env{} +func MustLoad(configFile string) *Config { cfg := Config{} - if err := cleanenv.ReadEnv(&env); err != nil { - panic("cannot read env: " + err.Error()) - } - - if err := cleanenv.ReadConfig(env.Path, &cfg); err != nil { + if err := cleanenv.ReadConfig(configFile, &cfg); err != nil { panic("cannot read config: " + err.Error()) } diff --git a/pkg/errors/alert.go b/pkg/errors/alert.go deleted file mode 100644 index 9590707..0000000 --- a/pkg/errors/alert.go +++ /dev/null @@ -1,130 +0,0 @@ -package app_errors - -import ( - "context" - "fmt" - "github.com/gofiber/fiber/v2" - "golang.org/x/text/message" -) - -type AlertType string - -const ( - AlertCriticalType AlertType = "critical" - AlertDangerType AlertType = "danger" - AlertWarningType AlertType = "warning" - AlertInfoType AlertType = "info" - AlertSuccessType AlertType = "success" -) - -type Alert struct { - Type AlertType - Code string - Message string - Source string - Params map[string]any - Meta map[string]any - Recoverable bool - Status int -} - -func (a *Alert) Error() string { - return fmt.Sprintf("%s : %s", a.Code, a.Message) -} -func (a *Alert) IsRecoverable() bool { - return a.Recoverable -} - -func (a *Alert) FillStatus(status int) { - if a.Status == 0 { - a.Status = status - } -} - -func (a *Alert) FillMessage(p *message.Printer) { - if a.Message == "" { - a.Message = p.Sprintf(fmt.Sprintf("alerts.%s", a.Code)) - } -} - -func (a *Alert) Alert(ctx context.Context) *Alert { - return a -} - -func (a *Alert) HasErrors() bool { - return a.Type != AlertSuccessType -} - -type Alerts []*Alert - -func (a Alerts) Error() (s string) { - for _, alert := range a { - s += alert.Error() + "\n" - } - return -} - -func (a Alerts) IsRecoverable() bool { - for _, alert := range a { - if !alert.IsRecoverable() { - return false - } - } - return true -} - -func (a Alerts) FiberAnswer(p *message.Printer) (int, fiber.Map) { - status := fiber.StatusInternalServerError - - for i := range a { - a[i].FillMessage(p) - if a[i].Status != 0 { - status = a[i].Status - } - } - - return status, fiber.Map{"alerts": a} -} - -func (a Alerts) FillStatus(status int) { - for i := range a { - a[i].FillStatus(status) - } -} - -func (a Alerts) FiberMap() fiber.Map { - return fiber.Map{"alerts": a} -} - -func (a Alerts) HasErrors() bool { - for _, alert := range a { - if alert.HasErrors() { - return true - } - } - - return false -} - -func (a Alerts) FillSource(val string) { - for i := range a { - if a[i].Source == "" { - a[i].Source = val - } - } -} - -type AlertConvertable interface { - Alert(ctx context.Context) *Alert - Error() string -} - -type AlertRecoverable interface { - IsRecoverable() bool - Error() string -} - -type AlertSuccess interface { - HasErrors() bool - Error() string -} From 93158e2987e743b8209da1c1ede2dccc67c8d4ad Mon Sep 17 00:00:00 2001 From: demin Date: Sun, 25 May 2025 20:00:31 +0300 Subject: [PATCH 04/23] feat: docker image to test app --- Makefile | 9 + configs/config.toml | 3 + tests/Dockerfile | 12 + tests/features/notification.feature | 13 ++ tests/go.mod | 18 ++ tests/go.sum | 326 ++++++++++++++++++++++++++++ tests/main_test.go | 32 +++ tests/resize_test.go | 55 +++++ 8 files changed, 468 insertions(+) create mode 100755 tests/Dockerfile create mode 100755 tests/features/notification.feature create mode 100644 tests/go.mod create mode 100644 tests/go.sum create mode 100755 tests/main_test.go create mode 100755 tests/resize_test.go diff --git a/Makefile b/Makefile index 1553480..91df932 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ BIN := "./bin/previewer" DOCKER_IMG="previewer:develop" +DOCKER_TEST_IMG="previewer:test" GIT_HASH := $(shell git log --format="%h" -n 1) LDFLAGS := -X main.release="develop" -X main.buildDate=$(shell date -u +%Y-%m-%dT%H:%M:%S) -X main.gitHash=$(GIT_HASH) @@ -25,6 +26,14 @@ version: build test: go test -race ./internal/... +integration-test: + set -e ;\ + docker build -t $(DOCKER_TEST_IMG) -f tests/Dockerfile . + test_status_code=0 ;\ + docker run $(DOCKER_TEST_IMG) go test || test_status_code=$$? ;\ + docker stop $(DOCKER_TEST_IMG) ;\ + exit $$test_status_code ; + install-lint-deps: (which golangci-lint > /dev/null) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.64.6 diff --git a/configs/config.toml b/configs/config.toml index e69de29..b2ebeb1 100644 --- a/configs/config.toml +++ b/configs/config.toml @@ -0,0 +1,3 @@ +SERVER_HOST = localhost +SERVER_PORT = 8000 +LOG_LEVEL = info \ No newline at end of file diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100755 index 0000000..7b02ceb --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.24 + +RUN mkdir -p /opt/integration_tests +WORKDIR /opt/integration_tests + +COPY go.mod . +COPY go.sum . +RUN go mod download + +COPY tests . + +CMD ["go", "test"] diff --git a/tests/features/notification.feature b/tests/features/notification.feature new file mode 100755 index 0000000..5f11a81 --- /dev/null +++ b/tests/features/notification.feature @@ -0,0 +1,13 @@ +# file: features/notification.feature + +# http://localhost:8088/ +# http://reg_service:8088/ + +Feature: Resize image + As API to resize images + + Scenario: Registration service is available + When I am working + + Scenario: Notification event is received + When I am working diff --git a/tests/go.mod b/tests/go.mod new file mode 100644 index 0000000..8fa3626 --- /dev/null +++ b/tests/go.mod @@ -0,0 +1,18 @@ +module godog_example/integration_tests + +go 1.24.0 + +require ( + github.com/cucumber/godog v0.15.0 + github.com/cucumber/messages-go/v16 v16.0.1 +) + +require ( + github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect + github.com/cucumber/messages/go/v21 v21.0.1 // indirect + github.com/gofrs/uuid v4.3.1+incompatible // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-memdb v1.3.4 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/tests/go.sum b/tests/go.sum new file mode 100644 index 0000000..50bc1eb --- /dev/null +++ b/tests/go.sum @@ -0,0 +1,326 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.12.0 h1:xVOc9ML+1joT0CqcdQTpfXiT7G1hOLbCmlUnYOyJ80w= +github.com/cucumber/godog v0.12.0/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6Tm9t5pIc= +github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo= +github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= +github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= +github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY= +github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.0/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g= +github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= +github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/tests/main_test.go b/tests/main_test.go new file mode 100755 index 0000000..2139f12 --- /dev/null +++ b/tests/main_test.go @@ -0,0 +1,32 @@ +package scripts + +import ( + "log" + "os" + "testing" + "time" + + "github.com/cucumber/godog" +) + +const delay = 5 * time.Second + +func TestMain(m *testing.M) { + log.Printf("wait %s for service availability...", delay) + time.Sleep(delay) + + status := godog.TestSuite{ + Name: "integration", + ScenarioInitializer: InitializeScenario, + Options: &godog.Options{ + Format: "progress", // Замените на "pretty" для лучшего вывода + Paths: []string{"features"}, + Randomize: 0, // Последовательный порядок исполнения + }, + }.Run() + + if st := m.Run(); st > status { + status = st + } + os.Exit(status) +} diff --git a/tests/resize_test.go b/tests/resize_test.go new file mode 100755 index 0000000..b54fd9d --- /dev/null +++ b/tests/resize_test.go @@ -0,0 +1,55 @@ +package scripts + +import ( + "fmt" + "os" + "sync" + "time" + + "github.com/cucumber/godog" + //"github.com/cucumber/messages-go/v16" +) + +var amqpDSN = os.Getenv("TESTS_AMQP_DSN") + +func init() { + if amqpDSN == "" { + amqpDSN = "amqp://guest:guest@localhost:5672/" + } +} + +type notifyTest struct { + messages [][]byte + messagesMutex *sync.RWMutex + stopSignal chan struct{} + + responseStatusCode int + responseBody []byte +} + +func panicOnErr(err error) { + if err != nil { + panic(err) + } +} + +func (test *notifyTest) iReceiveEventWithText(text string) error { + time.Sleep(3 * time.Second) // На всякий случай ждём обработки евента + + test.messagesMutex.RLock() + defer test.messagesMutex.RUnlock() + + for _, msg := range test.messages { + if string(msg) == text { + return nil + } + } + return fmt.Errorf("event with text '%s' was not found in %s", text, test.messages) +} + +func InitializeScenario(s *godog.ScenarioContext) { + test := new(notifyTest) + + s.Step(`^I am working "([^"]*)"$`, test.iReceiveEventWithText) + +} From 186ab4c770ed72a0b6f38cbef06d39468e9dd50a Mon Sep 17 00:00:00 2001 From: demin Date: Sun, 25 May 2025 21:06:29 +0300 Subject: [PATCH 05/23] feat: tests for file modifier --- configs/config.toml | 6 +- internal/file_modifier/file_modifier.go | 9 +- internal/file_modifier/file_modifier_test.go | 78 ++++++++++++++++++ .../file_modifier/testdata/valid_image.jpg | Bin 0 -> 45614 bytes internal/server/http/server.go | 4 +- pkg/config/config.go | 22 ++--- 6 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 internal/file_modifier/file_modifier_test.go create mode 100644 internal/file_modifier/testdata/valid_image.jpg diff --git a/configs/config.toml b/configs/config.toml index b2ebeb1..c762a1b 100644 --- a/configs/config.toml +++ b/configs/config.toml @@ -1,3 +1,3 @@ -SERVER_HOST = localhost -SERVER_PORT = 8000 -LOG_LEVEL = info \ No newline at end of file +[Server] +SERVER_HOST = "localhost" +SERVER_PORT = "8001" \ No newline at end of file diff --git a/internal/file_modifier/file_modifier.go b/internal/file_modifier/file_modifier.go index ef5c045..16d45f1 100644 --- a/internal/file_modifier/file_modifier.go +++ b/internal/file_modifier/file_modifier.go @@ -1,9 +1,16 @@ package file_modifier -import "github.com/h2non/bimg" +import ( + "errors" + "github.com/h2non/bimg" +) // ResizeImage resizes an image to the specified width and height. func ResizeImage(inputPath string, width int, height int) ([]byte, error) { + if width <= 0 || height <= 0 { + return nil, errors.New("width or height must be positive") + } + image, err := bimg.Read(inputPath) if err != nil { return nil, err diff --git a/internal/file_modifier/file_modifier_test.go b/internal/file_modifier/file_modifier_test.go new file mode 100644 index 0000000..beef61a --- /dev/null +++ b/internal/file_modifier/file_modifier_test.go @@ -0,0 +1,78 @@ +package file_modifier + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Путь к директории с тестовыми изображениями +const testImagesDir = "testdata" + +func TestResizeImage_Success(t *testing.T) { + fmt.Println(testImagesDir) + inputPath := fmt.Sprintf("%s/valid_image.jpg", testImagesDir) + + // Выполнение + resizedImage, err := ResizeImage(inputPath, 100, 100) + + // Проверка + assert.NoError(t, err) + assert.NotEmpty(t, resizedImage) +} + +func TestResizeImage_InvalidPath(t *testing.T) { + // Выполнение + resizedImage, err := ResizeImage("non_existent_file.jpg", 100, 100) + + // Проверка + assert.Error(t, err) + assert.Nil(t, resizedImage) +} + +func TestResizeImage_ZeroDimensions(t *testing.T) { + inputPath := fmt.Sprintf("%s/valid_image.jpg", testImagesDir) + + resizedImage, err := ResizeImage(inputPath, 0, 0) + + assert.Error(t, err) + assert.Nil(t, resizedImage) +} + +func TestResizeImage_NegativeDimensions(t *testing.T) { + inputPath := fmt.Sprintf("%s/valid_image.jpg", testImagesDir) + + resizedImage, err := ResizeImage(inputPath, -100, -100) + + assert.Error(t, err) + assert.Nil(t, resizedImage) +} + +func TestResizeImage_DifferentDimensions(t *testing.T) { + // Подготовка + inputPath := fmt.Sprintf("%s/valid_image.jpg", testImagesDir) + + // Выполнение + resizedImage100x100, err := ResizeImage(inputPath, 100, 100) + assert.NoError(t, err) + + resizedImage200x200, err := ResizeImage(inputPath, 200, 200) + assert.NoError(t, err) + + // Проверка + assert.NotEqual(t, bytes.Equal(resizedImage100x100, resizedImage200x200), true) +} + +func TestResizeImage_InvalidImageFormat(t *testing.T) { + // Подготовка + inputPath := fmt.Sprintf("%s/invalid_format.txt", testImagesDir) + + // Выполнение + resizedImage, err := ResizeImage(inputPath, 100, 100) + + // Проверка + assert.Error(t, err) + assert.Nil(t, resizedImage) +} diff --git a/internal/file_modifier/testdata/valid_image.jpg b/internal/file_modifier/testdata/valid_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2750e5487b9873f04521cc5251c73c1327e5af66 GIT binary patch literal 45614 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

BWUyuA|86a&%VDlKEc-Z@<78#PeJh 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

XRTdG|Fwu|5UZT=8Out9GEh3JvB6Q$+cHaXm?fC zWp`a??$kw&6?`M{YKZFY*>5W=H7oSZvdZktC@gMjHuEcTXgag4>hn+=f7ghubn<&* z{*m1JQ_vf*m6z=9ohM$O{6`3QKFjD=HtrstVbg=XB5d`}c6&c{$4NUc#}f7N&`a|u zrC5-d3hajj6UMWjTbE4Fm!TAG^w1IVp<5#|g)3>HtlR62nY^N!Iy9Np6N@v2p695E zP=n0LcHZ_usXI2~JmHOK-P){knlazf{M<_YHD9nvZ&^n)WA%gTk8WUy$limhYHI<0 zSt(Dv<6>J0RTkNggO$tOB0g!WsKJ%yZ%WN5agyqVrfRNK4GQdEYWyWCuiD2jk~sz4m_(QYvjB_jN5!xW=IT)pL-d9tosCfPS(sM zw=MZS7^N#wQ)wNcNi(jYc??@D!&!Il%A_4$DZh`FgY8ilvfSQYv1}M(S zEe26hB~&w_b>flmhvL@Ih;b`xcsKBW0rsHRb&gw>-$#A{I+uRRMiKl1XuYVrx8eBF zuA3M8&GH)W7vR>;H6=%)F~PV>|ID~9tNdclX9ZCz*fd7;DIsKgYf zT|1E8n#oJ$Z--9bxC>PbS6|4PhW`RkXutm99fkx%#<)?XX^fIH-TG_ntLC8mqqw`l z=bB61Nyk@&S!*+YR z&iDopQN1b2=MU^?=`KFBkunu<5+h81W~sApul}T({{Zi{>hU$%Hz;OLo-rD4`Uk^w zk~~8H(aO5x+dD#uXu#(ZNM1 zjW<%%Azu`8K;GXMu%5D}EB2WV(_Q^K(h+D7N8nvmIIyBp|EP^J>@L~dJZml>tt~&$4-UfykLf#SkF6mjNhYkQC|SLOd-JBm?Wvx^(WP= z8k-Tt0UhVHVdy)#;I%s$&?BbDGAzdz;(@H^pFN(vRlURLo4#vJm8iVfSf7`IQ6iYu z$B4l$){DtjT>E(lNK3o)BO6qb+q$5KQOhs+jZ{ptTo&L5YI73l!Bujyd@}b{g>G{C z01`QP>Iows^{cM3mh^~qCEo*!a-^>MwD*?4{1hsj%EZ3A&L543XPUu=MSKFXLW4`! zN?(&5N!R|>PReOh@C0AAft1!#Ju9y; zx*6~oI93bOz_wq5mEK8j%2Ab}&0M{dI%28IS&J#X>kmNQ2`fWs&YQ+$Mlczc34)+v9TA3WkHW=|U|Mm66xA z=7P$qwHz%S59ZP~Mm8SKZOOSx9U(pl`a($4(udQz%4KRz$JCcbjMa@fhrS%fUs>Pk zfgV7%d)CqZ-gM?@2EYS%$od@Wv6beHfJ)wBnWej$<_$xw#FNg633m`{Cnh1R%q%B$ z>vrfTi9dqF3VJ4*Nbc7XZ{NCnx6 z+RnZP^=E&E2Vb{<$e~VC3E7&dQI>7DgY>R6Scf*@9rAO**F=U#J{UU4 zW0QO7uNjAT5l_3q&KhJ+U;H?&JF54glr$RuR(w}A9Y7wT)$9=-AND3SJLcZ!Q<`4@ zZbp{Ou;ItF?lv~r{oO5n8Z2s*8|p_Y^)c}yB`ENd5?x(79O$py|-!vp@3 zTrgLw3r}q-V@gY3TQD==zr*evnFxNku{s_@;BN@IXS0C+x^+ zXGTM7h0r@?N;5G%<)r}O_?boRN%2tHeall5F!0cD?QNB*`X@Z3lkDo}`l6j-PR5ky zv3hO+-+7xa?|b*C_2M-uGH_K3nSU|2Ubzu!?K-|!g$9h6jd$<5)iRpf-8QA~b0J%I zCcT^&*BqS;ZP!@t3umWQxaC00{C3hlJZ(Z&+`DRvAmylC8mkIE4O)Qfv3s|dX0m;!oP5Fy?vW22tq~(HL3L2xt0$g^* zA*Vitbx{M!!GPGn@Eh>X5ZQ%9-@FR?`(_~5r;FKA&IB!o_g3JVt) zHO7p#TkcQGMaEC(&&KDbcRqg1E4zqiI)S_!HE@@-;qwxzek<2o{4n6_&gk7v1l{-h znM9o9%lIurG}L*cB=k^jWY^p5ZuRw-6OS_xbZU)I$vlb(SMD2AcTwixR7wQ~YgVH7{4r!sEl(b(B%6@5aD|?z*23t z>ZI&W#>;wluZ|JVrB30eBAb1pkO4ZXr=XKFkZ8HoP*8Yc6lHpy_vKVfpL0y=~B{Q1AV)p zpiM^t&*!zz!0Jl^rlJb&{W9E8VJ-<*DWub6@cr+(`nlu$)A37ofvc4$O7e51>it!q%IIlQT3&kc-8TlcQ zx17!i7;Sv;&|_Q{q4bU-yI%+bUFWU7V%q&wtPv)Y#hgS``kD&9d_Vk5d)-j?7xk%v zXtBfUxv>`g)GgZ5tZtZmqQX;i&RkWT*O*KmfXfvI_I~MdfG5 zLm??)$dGiDzG{52>6ljx*hks!P;TxOl zX^1SD@l2O?$MXs-4@9k!;+>DEFSVi9i^<9M4U=a1lGA( zP#r_m+4Iw#c$6GKJTdI!gksL4=OcC!f84Eah?>6z5Dge>YG8+mOgLz8`Q(nuPR80e zb5@X|Hy)yVwz~_GGyE9XCLVO*-NbmnFI%@C>`9Ok4SaucAHkFyLL{o2FPF%EHb2dz zqMI)?2sseZ%ya7=wxfZ=sVlxqzT;^3FW>Q$i9@5Mx^FTc$x`k3a{<(*83WP`IO&fa zncW$w#PKJyfr-P{F8+tA(@Yxh%XeZ?lj@KpaAeVde;Q^3{`$-MPrPSJLi|Kw1IUiL zpm2$q7GlcJ)4B=tHWqPb&(EbCswRiX^Snvc*Y7*EC|g48VS~J83PH5o&m72}-{2XJ zeQe94gIRZFE+Bq>peXy8wt5hr3=x3S(Xd#hJ52`M&1oMWejc;O9GArRT|?nf7y2`# zTeY??dEZc#L>ktpFd&y7xOXI4{p^I$&Bm0`Zqe=wZSoUVg6Dv>AmqG~sne14)=HG? zONwSZ_|>IJ3^Ju824qj4)R4Mkflk{Gcw;;?$w@1)MpUvFLi)~X7~-nLYmM`Lf@2|F zO2Y4i*Mn2=lwx6e$@0xEEc-X6r+K_f2JWcY4`1dUko$)~nd*q!p^43NZ!Fyg!LH-hQs?^?BP z2P#mB_Z6>wmnfmyM0&o2T94*EP`?m;OqI&s96ee$z@DVwXrMYTwq$*bH%}&?#Ep`K z?0##eXSv5ECF$XZGq4Xyk~8E(Zu?Q8x|-9N3dNS1 zGgbZ&g`k!D$Y;bQwYnlzPx+Oie$8Hi;*bGV-r!#rYOBecgN zk}1{cMTuf$!ccc<=<7=2RZB@bf2qq2Xw-kY56)n1+* z(&^ggu3+^D_x>z|+m?cl+kAqLM{ol`@UOin zlvMa!)STjCs>Ui#&IB|-qX3A1#-EpG+^!X6+>7zf0^N7T>rwlnUw}D!$8WsX^;pp_ z_MX20u0NxPwOj7#p5F7K`5snctQEF7(eh-u`xn6QD8*_9cAJgv8^iSw-^9Hd**7AEW;on?`rhAJjxnE*g;%2INk+c_s<7yMI^7MU zEm8RyM=Ufd^s`_mtLhiK3KM~MBm6aIZ;Ry7m>ckjSrD?3Y_)gpi2txfyyaod zylnURHv!E?E%%C~8b>xJq5Rdj^70K@MYO3oh31d=L*C1AMOVY7Yr+l&+9(ozn2vzf zW?}_xz(I9CD02|AWt33sj$ubrC1GdWtE*Oqea*PI-#xOoNa!@m5eIGnt1IBJ+f<&+ z)FsA_eC`#g;lki-73fW4Cy^HQxafgGSBAre=-34?6#)-V z?c}1U?(rEznUXcB3o>S0ZKt0KDGfS|4GvW6&#tDnDOGqF_qhNeD2;w33~Y%J93sI@#zuCYXJny zG+IMwBb~anKE!%oYew@#0uItMwFt9rjT2WM#nR-iBFT{(AbZA`T7VABHi3D!muSyuCT1caJIN5-1 z{wlg6AGY5nddpUM4+{DrTa9c^nf^i!J$aIn_`v=-tM&N95PmId=;H7Z9W+90`Fu)a`Ll5A z^5=?-LM$9KHuM9eS0SW!HwT{Dnn>&$XcMQ@Xsi}DrR-W=SQ$wo&>Npt1YJRdSQX|3 z&LD@NaeKaU?rnvmFWTln@p-BC#kOGVbWP@%wX2VD@HTwpmf1T@S&4gd{_R%X!|{I1 zi{^G^GE5&0QZaHpZQsk8w?HEniLi@P4qo0`11kaVkOos5;@1 zVauiMSIc->kazu8s!JQVnX;y|u_L~uok$@9cHSL0$cqseg0d8W1)6h-VcxY(x0|^^ znhjM3vMYI#qJ)ilV>t<#7fg3wK*#z!h&_9^lCK4}(WyJKEfSgj?J|tz6??FAEzj~? zI@l}5inKJw3dlqt#@khBD@V#bLQ3jneajKCHvN%{w1_=5~JxSx0nz7b{!pPk*Lu&rZn<;uHer;d#_Zx@hjPK#^qc!Py z3KmETBj6|vYwO6wLm9?lTeqx)zv>a1%S!|PzW^G&O1WTPi-~~L!ijTCQUiTrj)WO- zU0LtB5U4pFG%}O)Bwl%XQ;L?W`C(PC4d@-zC}B_`Rbz_)jcY4A9S9$7 zOlr}I7-lrb`1Q@RT1P&^Xv3kfw|fyZOb@O>ZXfn^LyYEHfbonMC&uaL0n0x?FPKx( ztA2dBJr?E|+gf5h*`WuWUgAotaQK+p3vLe@U;fFUo*3aOxNUbe^R?*^MVXbc`0)4T3>6sMt><@=rYouSV5ie-a#;jN805LEbZ_$92N=2 z(#j%cuIR5rHv;ED)(>aY26fWST%62VeC2JJjBO#7^XCz4mF9&}sTDJ>#wMB3IXH){ z?My`puc%2X)WYaU%#`yb&CEo}q{m@-o>OVql<)Z|ba}l@hpQ_QH0M(A)Xo6;V_)-` z>Cc^!5OJ8kcgyD>l^M#fA%3 z)aIK<@+NY4kEzlvSzFJAFU|L?UCQU}agqXjfg z4Akhv`ZjB}B>y=g#IhR;=0AViz?fTT3K^bIzaCC)pU5TeRqJC(7df06<3Y{CqjLgf zI8LhA4_lz=#%dblpT|MA0kE*o%4R&4Lz$zInuJ3Ypo(t~)n-|`?dd&J9VXt-{Ae62 zeF#5v$K}r)4|;wDgG}t6^>VAHBI9#xx`VhdX=zm+%P4-Z{J-saC<~B<;HQ3y>gu>H=4qfMccC9frBN*`HA} z&ICzY;l46UWK_u^lNspkWyiRiQ!~>OMl#o>GcJw#aHba;!7vK3&`(KDd=|G6(9*j$ zE>@d?T1=SJk*F~xcMd8|iM4Oh%din|2{NaY4=pFsTb;_$^WT=Afe&xV+(xFkH{rHc z(f|w7m6oUTQoxBV-27@kUI!^+NU07R=tnk{X)IkL5z#<>-&Q7sjjD=D=m=i;ky ztg9Knd+Ss`d0{r?O0Ci2Lq%zsai4YG+t#*Z9uz;u1hxx2?x(C~pHptx#cEryS^a0&1 z6gsDHbX*{qayR~VOWDVjXi1~ou=&>1n&hM{Ps$-Q1RX!!mk=i|pkor}5|}H{I)yy! z?rzJB7Pxd3&~wMMT=GavR|VfP+MpwcB2p45zP3@Nt|WaHeKjbB7&JkHj*a1=(#A=j z+mhz3GILxIX;8*IbZff+9Jcg;wt`;+yqP+kQ;ub(?W)K~{LsoQpyRFQzllXtP8aLB zYhg-AY3(FMSj#s`(l-aIJCz1gQ)w^oJ5FPMX%=-i1Z$Oqg)RpX3uK63j zr8J-H?~KTADO*`LKmbzl8BvE9+09*EGNHy7v`-(J7cWzaLO)T8eTl7nlvRAHICb@JzUdUE)C zp5S|An|#Z7!DGVHkp@CZ$n5Ero@c%KYXB9}Ry9h62!MkfPc^a}w9I~Dn|M2JTo-LA z1A{#ZXD{e^nV9Q-Qgx2n&g-_&&iQUTrB_|=Iv3Y|r!e0;4y;NoE_ps-vD-jwdT*T2G-So_o2i6ytgS*AcTYfXqK(V`;>UnXU{D(G27{~Q5Za58TPv2S&rj&O zCU)$5San#Urs4teKO8M%XxVAJc3_JYS!G?A3wY3`v~Xq41d(PKLbpySi>JiJf!x8q>`(DR!&GOT# zF2y9;ZdM1zAFT7&I-Ro%y*X8R8Jqj05AUFvnEeRP`D=HU2fS_%04)R`t(hk-ZZ-6^ z9-4jIpdsB#5qka+)7K*@5U885MyKY`WqHI_@q9Q}zh?T?y(2E$jqkT#qRa0Q*PDK+{RJ>TM60GU^nfqBV!+7)ba%EwEo@Fgg5A`!S|ndwfy#9u>K}5; zwxX=rh+mdz+WSUu+$!jxUOoO!hD4eNj|FIM$JLCnwf8c61)DoR+?Z`*$ILH0E2*=O z=p%_dm<;IifJ%7%tAv8EJ7l+0y>WUS!Vk`411%wHCHeNHm9eZ2d@vKeTYszU1ogRy9PtItXm4?RT<&Z@ShnKzlLJ3jYN#J_=9@)E&JtYpu6t zWWxlxNe{M%Rcu3QGd!w+J8Cc6}`! zckd7tLo0Q=zZiHCbT6)_t>Z#&;pao5AD3!%nflK%n%-tPWid9JdyRGQ+4VC`$cPhB z^lHb3>T9fiyLF)@_cESVhQJ7=!C;Ly z&0PUH(i0{xxa~D*o8EpW0et}~RJ2Evc#?aNCOyi?zG=Eh#cyv$(Uc-7D==bbQ&5ApJrhAe4`(g$|ymiD>p=Xr z8?~mpZ&D!L$1~EN4CG?zA|KN`XE_+^wwS$Bda<@2^_^ltN@c;aSoFsJyWye)hew#G zIrBrG9}o`avGfskNm&G54p0(&Y6cFE@}Bm)SZXP45?=oQ=3D_wrdw@jDS`We_| z9WNL2S}pI%7##{8_{u#M_*T*lPR-V!$jO8s8dNUlzpf^BjgFHbNI=~w85B|AGW!Wy ze;N#H|3VYW+PMENs8zyM?d9X{@5xB!?u(&qSsC0RLVGL(3O6V&X?|t! z6U$&XG}94;Y^kX%Bw1F(9@@(cEu?|)t##^(@YbU_vvMEZU61L zn!McQor(i{db`mVuY2}lH*>?7rDafFV|O0wzt@CXphQWKf>QE|WTNL#Q`t-A zz9gKnI}drRF6>TyL*@4Qip%#(D=Y1GU|84$acjhgUv1>J0 zz82Q6!ZOSb6ED8eQU`AGl=8GN10!e#6sX@saXpt2;nzy@^|8@bOy=n!)1|8VszBME zPW&P9Wr+uExoxIKmV6;Ljg>u&E6ZNhS5~g=wftL}Gk&dE;u(KK_Mo100WJ@obBWi8 z8;#IKjlN{ui)-dUpzN`&7I#V*w{=ZyP-o@{2S4G_EH!7Wd`9|V-XN4e<<(p$#d)6E zR_wj|5Aziu28By}+p0H#*(jqh>3w-eEo5)jc}tW?Bg1B#;b)1augFjEZP83noIT@* ztko+SYDZhIC|*j9&Y9;NpXu#PY}3O;1}0-i!1){5IlGJN7F%UZ*FbqMzij7|_uHCd z@m1$N(Q+HQyKB3MbW1|zOl6^806win-QsqOSy_dE_pwY{+rt^?jBclv+gcl?ej^Hj z*t>mHRp;3^&exvco>>JcaX)4K`@nru_`PcnK*c4lYE0?O84!aJ%WRn$*f>%7_d_b4 zGMB=m;k)%_b&dmpJFhpQiNg$ckUA&S<-hmfJwbEis+@2PICxF!d^UNJszlC3@S5hz zYsW#zGUZAc$U*ZJN3`)IUIj;tREfQr0IAZ+72edz-9B@?xp@^W_2H}76+4b*v(5z9 za#Ck>I&AiYG?&_}&WKkh`NP+K#xSj~_wHW_Z>zC$>^@;Qdl7?gR-4JIlbDiB&F(0L z>zeW+{TcsgoPzSyF92H@t{1NEW?Zr`_QPkBXO)I@9M?cfl54U9S(}hMAuc zIDC_5ODSLNdhSUKVbMFMcmMioxTDZ-V)ygbm0`2Wl|R9bF}x@GHt$`gdo1~<#4w#?YBOT$^1E4-`SiC3(4w0^gl?iw z!H)4^;tykl%xbAIAIf-Qgoo~m4(WV$P*)I7{&0HNPN5|+@g??avz?Nvkhtziw|KZ^ zOf0byfSnrOO#6@KiK%!sKlB9tsH_b`oB%aHU=w4ay}VUUl#KZf3=X~ei{m0sCueGjg@B4Y~`?s6UrVfY1>>~%I?w^O#3sa}mXYF3s zi!j(2K3Fd$)OI*tnGu^*lwX6yY#!!M%tNe`n=*5MN>M%F$m#N^BQ@ny~jy>)#li3I6($#J1|-NFdSoicL^(};Pr z)b7B;u1(|!OPZ9{Qlrn= zxJh|bp}b4kx#gcFoZy}?srIn{;okkBDi1S$7{C?&@Njyupk5q~Ak0-gVO)xRKM2?^ z{g^1~q1|SLr=<<7Q$Y#GOnto*qOs4jl|j{Ak4TG6Jz_Rmx+sKf2}0fi^ACL$lZ5d? zIT*iP0iH;>e*0}tlEg`g;iSr;WXuaEAF*MBC!KOIh(|fdBJf(^iXWbPAIpnG=seeY zA;fZe(jN2L&T;PDqSfo_ktP&r3zJ(77Y+ zD;3*8asRN+2w#HEi4z)eTi3q&(h^8pIYZUE-Go8aLa=4be1}O%4y6#JTbT~@Rm^ro zSy*c8T4t|YgOVFh=gKLNu%C*DiBSe)28`AUF&t#;R#onU4F0I|Do)$*CTMf{peI(& z*ld~O@R(3K)BFeC9_tpv*?}=)Nb)wJSk&ta-5jNh29CraSs-tf9gQ*(S765~EkVF( z-k4dXgJiOhmfC!KJB(HTclgt*05)#e1_uSr$3PBx9}smP!x|C2AgsLP$B?Rcw2S+f zX~Ihq`n%wF&k5Cut){h3{|*|JtWbYWdqVQJf41o0{v&x`msA=`|AoQt>bnPii$sV` zww*|QoHh~PQ+^6DTN!quj@ZcdY|taosr^|&@vDy=o@_&&DyRuO6(5)vk#UqsrglqD zNUj{)``39kAeV;|zN>nwAxZ$;9|&Xus1a%Qks2{#tt~MMk+v@yB{{_;0<^+QUGcY( zOdcv=yzhMbj-=U?cL4+|A+q<}P9Cby84LJo1XXr%s=Ye2oM7?4lFE22QJ)O!FfRpSiHf{<-@u;Kpz7JZl>mLw=R~VN|Rl}d0V6A6P+^HsQBndhB zzZFZ&n*n1K@@)r<)`%O%%x58N5Ov$<&Cm9)>B=AK1;N3k?tsxP9*1uEe^~8C8j?zE z$a5AY%8#Tay+m@RIjaZ(o za1~t(`koc9VMyTb!Lz7Pv9tyGsgCX#XZw&j;sB`&njuX9Z}NwCU|P zIjQ)x6B?EpwHNI%Zz@SXNprEoauwDkgm7xx-(5lw`e_cEsM<$~Qpw~vi{o^2y|sC7 z9qML3%D+5^pa_p%v9N^Elk+Uh2%3DUZ=XAi-*Txg>v&GSz}--*?t;AIGuWeWuGPH8 zrOkVt$lF2@3MiQGkqwSyW{(y4fr1$K7#ya3K+9H>Q*3AZ;D>J!b|()jfTN;A4A{iK z^jqIHWPVs5$Y}+;&vRE|RjmBgRORQ|;NM_|J0dS>acT(#Ssaj9HV(c2us~A_SO%!K zu=$?q@%LqK!W!{#KJ0nC-YsARe}v?1g-2?PzombQ<;4Ul?5B6(iNSic-rH`bUJJk$p0fI>2{VZDpkX6mreB=~DAd2So#(GJ@|A9d`cf=5a0rB5@A zW+|Gs7~CZ=Y0qn5$&4uKHbyN|>hA%@-w6{%HRLL7FA$;BK zuMFyN(L@@L>tAuze2zGA_ly|G1yxf*<|13TTdRP>D>iy^PNc^=dY$b?~0B{zGZALfS1}iX%-mr_PXeR>V>8ZvuO> zqg(YS(rUt{Eiu-kr>T;fa>(M2MCa^%^a}!M!(S(HTb$@eviV`PNJeD(?FYf45x2k@ zt8x7J6g&PoPeu5zT)MrkCchATXzj>`6EMD^%(QaU8fIagmv4v~qbd0+`qDJ9%L>h- zK^d)xcaT1glkikl{cG-{A7XjNQ?Dhe(k#_aH{J@;(QQi|3Tnh@HkQ&OzG>YYvnNBn zV2OXC!T+#mLsXuHBvkVSKZH^UA|&6?x5zw|8!*XXS#un9=%G9({9|yxKx<|$v&`fY z%mw8e{W{s#}*Bu%0j*Y$*z5@z!nR#>0Z)``natr*z!*sGr0t{S->lrFs|Vg8Y`8Ej6{f=2YcG zF<~Q;D-cPPVgVLjZ0Ha9~z3l}T!Cf%xOeI0U%s(s<{9H}Q#``s9h@=_%2EWOu z^AX)E22*yN`gzoPbrglnRl*O(^?)j?@1iE@C)UK;E6zrn@2BPG6v7hn-uSwP>HYnR zRpt?rJpX+#`un86>XX7Ple$J3Rx?t2{UA60<)qqXZ7M1GFiFv8=JTzYV{xu|??t~x z{IcJpAtQ6Q+lmF)EhOnivN-*Ea6OTob8<#|kXC2BgUAn!859dQ41Zavxo)#hDrkeP z+xr<1c$6LQ{x!f7Zc!P}Z<@#_0}!=D3~q_M5bV(+ydrX>ufc?A!E!eu-wXX6am9bN z0rq)|y2E><#YDXPqi4d6s!<`wkK3qLqR7+U()|0p1h7&1+?*XiGw*(4G0?Rq-_S0J zv)A(wifhiZrSf=>=#TK3v`Kx253jtE!S8*@O9D z@BayR$2Qw98GZ`hsF3K5kd)>9S-~PqqTAaheo&Qaz{Wl2v19Z4AC}T&Uc9s6eWlQA z7rC6wq4lG@_^i=-qi>yyxnysw&YKuG4VK)DhK27ye<#Q*nqmxrTmV1#J)qfBm=hZFEU^DT@b}vyP z8AQ)}1mknmq_uukrEJwq=*OugSsR4u*GQhUETUTX%SSeHCM($eYzDPf!Zx9b?MoiR zO~!-fd>O+i@8uJmAP!Bw0?$mMFIVWKkISYs{*YnW;jQav+?SBlD1w>LM_Q(NQSFp_5$My5vqBK2D@8)E|NfVQ=NS?lG!!rx!xy`)o;`}XYz(e;B zi?}wDVoBWu(P%TlCqbNlZdIW1=Fp&$r-@La&DHjO!^hI$?&hv1!gkb8NN=dM%=}X9 zQH!EdjW4Ip5E{>env-0#BMy!hDRsGP24sky@q;5uYDgr6BjN z^x4oSL`|5qAZR<>ZDoPIfArISoX8q2G8<3%FOwX@Cq~Ad6ujR%H9}6f$=e>qm_x0- z|4jb+O)v0d+{CLEQgGVpsUwm9o;~z2myId|0z__aT5YzcP`|~MX{(3t9HTj;<)4^h zcoa*tNbF2^WcS@ih>|X(eU;Qk-@b@hfWLOFLC5NHc*1k{6ZE*uKqCZ~V+rW>!%e za(~{r7S{B9%WkzH`-&Lsda_H zW@v!8Jil$JLd$5d^>MGO7};hHV{;{bulg7Md8nQK3>J!hDB-VwL+^}-R`dK{Pnrh& zQCT`)CV(6g|7_(1kW}eLH2Yf`W2gQcOQj>XI*_PF>^e~{?rf?bb;2fYCoez|_bczc zNM6*7n^cq5+Pac6VUBZ}y;@Y2z95>eq3M_aKyvRyY@yz#GfiJR+=NRXZM+HonB(m; zKA&duEy-HHA}lfm!5S9z3Q?gcwJSJI`ls)NVG)KJ)V78a+1c$M&XDo8(lb00!0W9b zuvEy&!s?hlP)I$OghQ=9cq!nvSD4g_k+z|k39R+di??pX6r%HfcVi^!RMGK4T#;(B zh#mal3H(0^s64Dc=i)^olzs&_`mujdOrnkxtHYMCv3{(S6M<<_<#bp@&oEf)9Cu^T zAAsB3&ewLoY&o_8+ZVJ|uc-CWm&nO;Xi@0Zk7$6}T%ZCb%_N#LS2TVXa>Gb&PEnjT|SL@~ySExn<@+w!v z9vCm9S5an1ZZ0`L+rtb*|6$>46zibnjQ;GYeBVzb!gCFY_P47j8A^&b&41ql(F!c~ zi0=Z#F$DP!iFoY_4YiY_3nW*+P(gg+9XmG!mjT%28v$A7qjQ&d2sIP0Duc!HZkJa&e z(g*HDUeCTkhLOB@sNEj6*eZ*SXdTs)g5YSRkA7PLs@EuoAh#3nhy%d|55T|3`>4Ei zgXgSbJPhOtIF=CI$?*f%vV=g3OM$gqgZjz#kUvr!m$KIQU>i7{n0GG4yX8vTSGB=; zvxn#ktbCwjDdIDI7X`cI%N54}fd=>&cJ%HH;;_JURlo=;TFGDh>HT+{+W+}R+#gSlL% zutg!YOie$G!z0yMZi_EX3j8z4TdvO~#US0!LQVFt3UQ?tY=Xp<%yn)yYtx&ehMV}0$(zZ<9!(Ft74bZQR1ilpglV{a zP4gidW+cT2GBqW~k{yZ3NlQ+C0SFnR;O>5oi6eztdNmVv(ln+LwqHcQHgRv0 zMNP+3;r*9ia3s9_WTc{AsK2e;Rp9{QR^oId8=#5PT+jJmcQ^IuU6fh8%Lej3o}-Zg zOY^c&SLQR~d}0r9d7;^zIEZm)y(>#wdZ&G%%Dq9QW#>dil3v>0?DPJ4Oj5gGJc}g9 z2VmLPh6-(9^$WAH=Jgn6QRfEOpvm`kirPq=_CG}PGO7JNq7AFf!}L$h^wXTQnB)Po zr{b07%alx}o-bBC_LA;&ua4(nwWC z9x+Q_rq@zOmhaodvgS73H*!lo9JKVP`DPo9%w&hvK2=jseBAYXEv2mU4-@-wP zNzbc1d&fJ9F2D@7*!R);J#A3LX4>~0^j%fs=DRk6h!He5`xhR&Za3wFLq+A3p1+L&&d@rAdwT&mx6zJyG?O+ zE!pPkge{ev4_l0N`NXB=vqv4x3wOOL9Cv7RY!x=meVh9p_%<-dm1$V+u`lx{a;aaj zB`w1u1Qj#6nFDzlSq^cel`*E&GQ{|(icHHu0AV24_;mcl*7x`%Z!5C|B2b4S+Lq_7C(c4WbAsD!>q6(L;RrdpBQaC z7pvj&Ydup6z5S#&y-u_fBm@;Y`-zNb`Y#_6>Z!Sv+hj@H-po0_K%J*`=Fwete+=I9Zqw54(jPsbtk;Wykxgr zdqt>OtLG~>qP3oWOGG`dOQpNIQ1?g+4+*L9T%6Y!E0px1lPjc(KtO#J$e(@FB0n5{ zFN=@>77^?EYhdLunc>lV_6XW0d6`xk8^H2NTESz!{c+U?n6YcF8x?%01DokL>4*}P z`)ATt$yrojyh$8r6pJTk6I_Fv2qi!?Sb0i&T%0xxlQN4DuDr5Q>sKA(?$!#DiZ5Tv z7hGF9qIutA^U|VXzM{%Fv3y<b<+Ldg=ti3>r=1I;duo&C($ADa<~B#JpQxPbr1C*T~avt1J~3iwcX!(BRpGhqVfW{yx-T-U{y)& zfkgQF8yOs}xom742x(xkrFtgt6s}=*GO;l-g@-h^%~JrMlc{E@Ky&}Z))))$ zoRN{YfQrvZn^<}kK^@~LA!!8NV}%%_xuSAqB;E-KZq1i_cDz!|tP#fkNK&0>mi2Xz zmPb;qnNhalW>)h?|M6Q1y>uilFtj{I_xh(1p;QN-Rr|T=Qv}m6ydjB^$Y@?PFlPQM zYQv3)jGIEI-#=v-VZJfL>R3h-&2IWowvei6@G-m9aV=d+8y;5ZvLMx1`}SfXz&xPgvv*qi@PLyx#4VlG2rgjXKl|wj026a(xUKn5i@zo zZ}Y$?3YvlM?_JyNeWr|LgsmPAQ@+s*)5>hxjV;^~nUR2Z*;L!HlV=R^;y|66i>Vln zC0xt~>_vBmu~L9eMgX=Hs}g$;4KnO4d9I#L2sV;7Oyi9E*f;GC;`w```*@I3>mI48h6 zrzYng`%yce586L`S2$qH=t$m=8Nsbs)$*8aIT?62Rs44)O6s5>*K~-9=zXH_q-b6V z6_LbU!=&kqI9@V;_KV}P^7lIGQX0STU>){#JB7|CImVNaBA2BA7|hyMM+=N+M;EjI zlF;W2pe(6%lg037pgk{z3SIVys)TK|prGl(ej8tj!-s9pWK^8jW089ErH|7)NYKjq zf)B(v<@{jCPcY@d;lIG-)K%YUNdb7MRh$8Lzng`VDIz!I@ih~%>zB}R*Bwudxa#RA zYbdbaz1AWpv(-j55gii99&VTQX!u*iK9W6lR68mg8=M05*x;PO3tA00WTQfke^hat zYFHWWbpMUcm17C}qni2dAJ!p1tMR9sx)WRl@G}CU8$9%%pJc?NaG|8hH>*riypNTK zXynTCB0Z8% z^n}VR1%D+|zt!SkI+XUTa4FnctGJ2n-0|U3{MqwZ!An8T!1M{~Q^~G_deO#Y20E~C zN4Vx){@$K_%@Do^9tmQ@PpG==Pr#d{!)HUg2uoOh9w~1VBH-dvJ)u+29w$Gg8~@vT z)4CA)nbPMSXpZQ!Sjikqho&00pWC>vO1zJDoOj`tn)yL5Xx+-)uk zz2%L(4qfh%UY~gMWZ(v-^AL9c^U_y3V9h*nX%CO@DvVZ3{1E@T!=fmKz;W=jMEM->zLZ~OAgvhNur`nh=V6ZBBelho(%@^UmS9&(t zD?lo9dg;0%_n1YPa9tug`THla1D|@LKv&Ay*roc%!O@&S+TvzC<(A#lz|npzB1u4Y zL-ixww0~GX_>8T`LOGD^3zXygQtSt&xJVYBasy;DY1Hw%pyD!}v6@zIt*IB~(5bh-VXh;MTAW&2fp=8I6r#+E*35iBMgBj7}6-ZjXEK0qoLn?!*5= zq#S*l5{)f|*O`F+urP6u7{3Fqi!?P(cZ5jlq~K2nlu4&GWOp;fQGHh99bCQm`O-?w z!<_Q!0UJ=lUc@K*Vx|_dalDgZNj)KzP*?b zEbssce)se(ZjL}@Vx*HIXKZBdEcbf_i%;< z){&Io8(h47OD|dcF2&7-F?^-0nv_$S>In@2fQtx{|9l!GJJ_}bxL2dDd5b4JH7;zn zJ~nqAA}b@$W{5ALynIs5_KqTU(y)Z%xslKqUsZ&b;e)$`m-@d1b=?1ftp5k5{vUS< zm82TrA#&CvQbw^+h7Vefc2iD|cwb&vhP#>8SkOe^{_kaBF|>=Yic~(OydsG$?cz zYH$z3EPe@2+@p0>ihH~VVtPMiOTj7|@M6EJQ} z&V!b_n0xQtVoc)=Pdaq>18Grke8LXNrjyiYmpy9D1k_tv=F ztLpd-MerI;uzB81Q${m^&ye6d9Js)rUw7JD;g*U~j1iv8=e;BS9dZLvPEQ{)pr5wW zW3Hg^D>A9+!nSd|Y-K~rUn(!cHCcA#|3(||TDae8tFELy^X3T;L{`QqEem90K+3Yq z5$uW4I=b7yyuX(21u?(JA67jaFHH7E&VVnK?;n=e6%p-{5JS&e>K$cdlfH_^RnZf_ z4a_wt^b{IMlwvr`@UVk{2FZYT0elIb-llGA*BRi>7KdIYZ{#c;D$vX>DSoiNSp!(ugDXG~HIq08#ckSynKnsj>Oq zcFf>YTaZGm)t<&8s*RrvPWCi}sSw&kZ@cv9+zn!<%LL#AxkLJqq$ZOMq}JCOaOwnP z?^-wKIEb=}S;l0)%aW6rEm@XGY?%jw1oO2nM>KaU1+I? z*cjwhIWCJkd7U3h4s~f?isxPh}5RNy<4jNBi6a7U( zv~3*2z7;?xcYYp!IqB~mKWlXa9LH02S}A&cP!~BM5%`Cd!PB~}n_SC?$T_DXSY>c? zoo*$~7)UsG;MkXTPW3$g4E^LCyL-^;n2os#^dvRqlqYFw}bp$3Lv2EgvbzB@3-gA z<)kF1aW0)bS+T)UO0cVyTXui8AAhZK#MPCFmtmGCz(I-!oTy`eZ>hE@!II~fS4*p` z>|AFHve!zw%A-x3+D*>2-!;O<3sbkbe50CBo>Dx2z$4^~FMQAg=;1S+3qQCz_ww@7 zaT`@K0d|K`%SEu)wExH(8@86CFP{ zl=nq4yd5#an=o zG|7%g+2TBeudYYGc1@jUH$egDaub|2ns_hu8`j2`d>VrKs=$SK!!^ITBwoGq$)Po=*c39}c0Beq}j`HB=@_APreZN~17 zYs`xW7@D-+*w3=oshzis8~8c7tg(h0__TVhWp!j0=D`~~4n2Fv8P%OLV|w>r;``&h ziF;@@Un~aNO}AA+of|YJUH{0Pt=>dCTvLi>I}kni)8*R2b?@?r-b7}tTBm*qnYBGS z-2pXvY%;;Tr0t3WoLA>y9pM(AxpeRPs&V}}lPHqXJ@J>bn_-m`Y2?2Ws`iQ6@<6iT zwR21$(o>oTV9_q)K`&@XuR#L_ap~v^&aI9wStn7 zycB~z8SxjmC#EAq^JSv0kw=?oBcCAOa3+{=eN6HrNyeD$xsnk_^qSI>Z!|^mp@> z%T^1Pn315^qa)8l_73zsdQ&`c#!p%M)-#lO7xqa#vO$4T+(cZBCBc$fN%|bLBO&f0 zHS)V(_FEDW(vBzi32D~&5~q747xs~rzPFdPcdK`6j4vVXVL1VIYhft)v<*>o+HKLPIMe)+lIUyKR*Om4 zKP)1@lrq$}hmCIp+iSe`Vw|vJ#l&l_D`dNdTo>ujxkR)f9rm*u5dv z{rD4xxK9@PQaJvW?B`)wY+o~W3VZloHzjm@CmKjC9G8qe$2t&fJDOA!J7bIrw#G9q zq7`+dC66gT(dHFho+o{IN#M_+-jIg_coU}}JB!}v<#dQ-b=drL%U9l^vK+E*!lGm| zmLF!_lX9ti+$9(Ulw$Yxt7HY7NBl%`RH zMmM}HlbfGMm_J2p$`mQ%dvQ^WCn5kYlWs#97mxc=KR4`%TbmLYw3K9aJMX+BxG*HE zZC&Qd4~ALv+8wF2aqO-!q^;{JWT%^xbZnCF#yCuskpCJPX`F&vA$Kz_^m^|?)A|Jj z2E_tElzjRicceAxd^GjTTb0Q>|gSnBF;8{G@!oPmJJ1~F>(;z%tLrrq!t z$X0OFkv*a#WtTb5z2gg0`lijyCH4foSPulXPBx3`3|qH(BoZhRNX@b?>_{xAeK1_X zkI2AFpbcw;n6D=A>gX|D6$FOG8l;o%CS@*cwiVG*5`-0~v1cfZ3(rIf2ZZl*ggzm8?F+cYOvQ?MEYoD?wFtAEuu zf7|Z(RyJ^4+<0mFENrDgp%8lXOhekYc?NR0HN(p6c`ueFxx>L&p@BqFAbFYx34%^! zMsOQEX$+Y+LR}AFYkKTi`k|s@r_iT`J+^JdiI=#WC!`=w4(O=P3&!#)_oYzRzd~)O(r+l$z0pD8=%D2qG?t-svt8W|#3Lfc1J-05jWe zVPoHe;uQEF#ra>365jt%i2tiN0cyq)4~jD?bHJ#y5%XWs`5(r`gXp9L?IZ5F0ZoP& zo#w%Z7+*@bOOQ+P9=9^H`;ZF8@yH=@GOG(45Uni zx#Ls>_sU&UMx9QJ$6QIl1vai#Qm+MKb{|H1m_h6Gbhu{dbUgA@AJ`O{xp`GQ(CfEx z*92GA#&F_Z`-gH4WCb!MU31D!A-yqFmM+jx>8n=9t6xLPz~MmfS&%YmL=5nry=}XQ zh-c8?TAA`&!iMZsD8pUbMqp@-vMyfGH03C;dFw&`0g4HuTGxeKLZ`{J6E^Pr*Nc#{ zR~F#Bbm$!=a5O%2haqsvJ01k?m0eG}f`d3X2~Usv_?N<&x5HmQ2*|v2Vr5GFX60Vd zUglQLC=6&n^|Y9F5V?UdxaQ;zlqo4w<3TFJp%xJtAkS;(k#9{`?gN{} z*bTwfR0;YKS!=G-0gzDFVeIAOB2Ix3-3<&PVCz9}2DwimcR;nRcT29VoaDVs0L=3C zNp?Pk<^?6NEOQQc)6vpo$V}^+W%;lKP@e4**DG-21_n+R7>b`2$YDsCnh3-gjQIhP zz`rfOI}}b#MuNoOw97ga6K_Q80-J|6dP|$maTjBjgFHjtFN62F!&x!OUQ4dIC+(H+=6JShQ=xR+CRI##X?oUQ1!k9LrLa?fzqDXq>(- z-t&IdTohB?7i#i|zV<*5`Jr%Xt>m;bFhQ-Pf2hf-`1Eyj9pG#cA@xonIhz$HzSlCA=7FYp4Ko zu#5Chv?D6Z9;#iL-ILpy<5U!VNJe+G%f=j%Vc9hHHt`-Z*FL?_&E&C;E0)K^&V(; zK3O_%{fHA>cpE2-(2d1C%l%TRfnMN=C(b&w|*|4%q8X@mJj0Mu6F>3 z2*R?0OU5^7SSf@Sv^nlc9}c0M-?R4!`G;kWralQDJG8ujn77-%$cUw`jm2h~h4C-L zs$g6f*(ZPYi+yJUm&?)Y$E%^#zcPrMJg|25v#CC$i~4cR-SiA9*YXfr^02G8R%D&T zjiy)||5ZJlMR?@Q5xUSCx}xy#>sslKOVbKHSrpFw39V-dYGcf!AOFMh2ysMn>Z_}m ze&*ws14!$`R5KSs*xI@_G?Z)H0;T-nuJIa^4z&j~JB`hGx7ZI<#E>12zv|u3th4v5 z_8q6JZPVfR(t|+A6WI&T-_Tms*H=s#0Xok0Ab|^N7+gPJ@cBrk4T& zKAe>y1k+enn%kdm{aNcHZd!wj*MUJ7)Qe#%Vh^X&nrCzom6~{GKI0v2jrK)q|3G$0 zY`&e9OH$m7+%0a}hBe6^xZERsi*cHDeJe-5#+yDZZ(wM+>kM$`*e^dvrZFhlkTDXa z45Cnox7N&;XcAI%=&wPSX-L@LMsr>4Of)!v5QS0cxE!i22p{WI{Sk{rfjH+y?EG>p zeNbSLBSL?~u+h{pmt1cfa{gg)wyj=1R7*bU?W2?o?o0oGi=8-^XO8%%TbVl&UoOd{ zIN?!(f{qTGXO%3S0wKtZYM-8S?lUrGu8i%m0cdc&|4rl}dbDQ0JccGDnxeuFx?La1 z$neN-eh;2@ertB`gq}-r;+Z1-xwP-bDg*5e?@&2U68I zzjo>}47fhG%b|`a1UO9$FU29g%fmm?tC$;a^*Cz90hpKqErXeMtyJp&_WYZ#k6f}oQ_e<@jjIS^AjgwZR!{Bqmf3m)IO0g?asVA*J zF*h#dQ$SnHn$19g#@ygViixax7s^X_H*diDn4FTV53hc{{KnXwi`h-*9$5o8;$s8@ zB_Bmk1IKzLBd?asty&DuZh~&#&jMTE9rUeBk}B6-jGIR++x_QP9aj;kH=FCrPYJy^ ztB|#^qiE+D*Qjyl)#4Y7RM{CSe#4w!`!ekPM)g06K`3ZOEWU&!FwT#pWr>zA+co*# zWbi&yio7Mb&xe{H-2>%^4jm|N8#`X3&(A~B{WlKerO~`P_MggDlwOOeu2T#r2W0GU z)ZrD8al|sisB}`}+1g^z19;I40h!qKo|w9dRo|#~Jz36|K}!K2;j7~i?BlK%=O=7S z2F0b1GpkQJA!^FyJSa(NhlMS1Qu1#xuFb9f{pSk42dykcpYouV43>iaiVK&FNlXXa zw7M6anVAwPhmbVGKu=$1|T!32YQZ{7wjo5KaEUxx4iq+OUr^&p_NE05gdI#EP6=%dffz;rL2LnyOQ$Rc>J4ng z-oiG)VrX@a5A0R%*;&-|l&$f@M8pf)28t#5C*zjzHU}{cCBqnL1;-yQBZNle;BRT$ zAVbGASRWzD^0B{K>OdQs_c(Lsf@~KR2eq=giIHDn=kfP({B&NP?xiS(lt~;4vyflC z$;2j$RHSqubxaw7521z0^~I5b9KTGxP)%>q%wA`~IYAjT($vL{8qRpWaDAmqqD!}t zkU;is^_T+(c%hy+*R($u1?)%03ez~^Lo3<6xW8w{{IlHc24am*?{;?U#=}NE`^TB# zvwnl}g;ep-dn9%S^Y&2xA(HMqsi&~jkH=lyy(hs+UG~RfKfZ4EA}@t4AUh8LnS#*B zRfULCy@o!sLnwqLV)qOz3AjpexKQ``F^VZ!$a4LRe|G3>kgLHC6Zg^wDfr@?K-k`9 zb|-K!K^Pc;ZfCZUwutM3(^@JqT-8*MnPn=ip_j=E|; z2H>%8mSC%6Lob1KCI{XS9oi;MHm{_Z3~*{res;(T=ml*F)yLaqb*PBx=<7|uZ%!*bTQg7=3&ygt$R5P!=kt43pIsP#W6$RP3 zg?TbChX~%QE_WCFy>idYKqNHnn+WtCkK?Blx3~;4clXwM9{oaL!}9^aldOQu6LJBX zBin3f+?TZsxZ8kuJ!N&rFnQ?t=k!G&|C&bUT5=_BgOuXzb8&0$>h-4h@Zm6~`Ecm= zwYp8wl#+(`i^=kG3?ClrGG*nv$2<4h;{{n4$R@h@eol5Of9&JM>SbQJ zfftXG+Olg{T$nU(Iy*!OdGNw>ime zgqT@2H-2UI*0PC*43dgh73u|Q@TXZKqfyA8u17_~2|wl*awG&$ppt_9$n{HY|6>Q2qJ)fTbQ03*d($xf|Q$V?n?T6o@R>u!4f z>Ox}o!-XYIaQhP?iYsZa5*rJuh3Eef_Wuhi_)jn&&nnNRyDNL9VOQoevbH`gbF&(FulxRPwMn-f^ zKy2j1bR`ADI18IG?&$FiY={qV2btAqo`n)!5m7}(o5x}}o{;rn#nQ~0>0E`H5gVTo z1Ky?RW2E9AK8J7xbvGk+hXW;_N;*-2o)2&WEH?&~4RnoSL6kCV-4B_6>UT`8#&}Mn zL%5UL131(lCH*H(jfUIRNBc1no0}i0`g1nlNen$$q?mS7wLzrzj5P(-xGW}#$6)Cn2wI%bj>hx813shpjcss(}Kae-hO-cDnY zheZ%3a8)L~Qrl@I!c?X^<+#PuPse$;LtgL5g_+!YR*mX4R`4 z9ZuTNp&quJj>1vKV?JZ@OQ~?MSux|9Z4sC=MPU9OJaHW#j5RU`Jpq_R!L-ejpk24}~)j`-mw!<8?qb zHv;K*93B5*VOffiF-iInT~+o6(hy|~h)`+f5ayrR*y9lOu>`s{dQmyDp3%9gC*!feb{m!C8Y4F)9d{ zoWoI*7{_ww1iv+t%`PDea1iw>GUA4F-QwB~ zVE8^FEb$KmFEDX8D=;B0D6sKz;6eygN|;N`P=sUVycS}=15s&;JBd9?sKXc{N}6TE zaf!HK(L3c5-eD_b)5Td5qs(&gEK7qA1K9lvly zi!$L-65$r&VhkxUmx-h?AmR)c;L;m`6Ehgs!KUTWgiJuD47pSywpDmKhAWwpR6;U% zRI|Zk&xwx^so((yWfd~RP!o>~IZC;#rT+kc5_ZZ|LZSwnp33j@5@8B3?-)s>LEyLG z@V+K-@WLsiwZV!-9Kond18_4KNvTY?!qzs0V-Jg#W$`^BrV^$Q3>%4#acMS-nSMG# zo+0sH0+PHARl)%RpyDKmqgkrI21Km*v}Q~ghAM)D65_>(mPRTCcrz6oc#%*g=B$in69WA7LhO2A9+;eqq4?oIW}TLay+7!gX_Y8iR>nJNyLfFre~DZM*w9=F z`iR&8{y%Od4KUuSw8RHVNf-zjj!08J0gs0-7Jb2(9u`4}sdTNB9D&ka0Qy&nk)26HML;$!iDwuz>EJn3@ zj|hGD*mqZdQug^^5WYxy60ykz7`O&b4`I!HaR3Vy1m~L7{{XixRYm~e;r?Pspkqb% z0WnKBwg~DDW<(NI0J?t%xT1j*2=NlGmJyT2>6%NXV2a(w7G>bpVd6YO8H5D7m2sM2 z#HwOQIfST`OV5Fd47j<0Y{v)@YBYkxTui(TCDx5)3LtrgrPLU1U?pB5xT4i44p?2L z8cv?^s4e#?)OBFCV@A}d9wZT(H2rtJBBAcolkL<|bs;9dfB6}^91-rRy=(Cqsa}XL z=|1@RVA9D8NrbbSa_{pRmtKP&zf!&S?te1jb5W!H{=_ck6caDHj$%~QQF9h6!7s*$ zkY-U(T*0WX9u#=1hEU430aBp0VvJo1GO^)tD%o)o9v28L#A-1rjZ3C2ksF#v69!}R zn`eA0s9I;@#TI9%t+0T-R5Qw|#`9jGxU&|Vn>aQ1exXDH!lL9pzu&f&JXLOQ7f&6; zjxxN1nx^0N%Ee&A%W1=3UCj;qKrG#=Z$kUHwu%6)2L+aW=kiO0%v*>u(e;ge!^R{s z4E3AhH(jxlZfQT*hIl~P9+MweEc;Hhf3Ym0!A>++^8jpLy#E09ThZAXOflT7!K7@< zF!BPK#9T`gPh=d-h~ipQ*7%C$JWRzPEnUq#YRKjP000=dfQpX%a9{}zCXt!TSwV=Z zTo){1sg4}Mt--xZ(o+SQhL*fW4H~HK7zMmKi&*DE+WARJ$|EC@&GP&esO6wQrE0yc zwebm%icio@Q`?y0EEu3lrW`)qk$|;6BZpEc3Rp>u3xVxw;_=qwdoRKF7z9E-K$ti#C zV3?jZLk>-)_!YB z3qTmx^B85+bCpE3gMLRZs=el9mRElN0AFwa0Buwmwhh(7wZ^UW6)=@*1-4$(s(>ksQQEFg>$yt3E(Zqr zqCXkQ^Dqsp0=ipDerxJwwxPrS0HhFX4H=JF1h4nlQulbj>K#;3wOH}aKlUwZ9W zGTHYNZihR)2lw#KCV5rUgPI$v2DuEfH%HJH<@A1+ScLyG=xcm4O4N*X73J zlVZbbYgzd{{jmW`1xxzH{56fl)@-F47brFF^~7j+FkuIKTZ72e&W2iVpWf%88X$M~ z^Qm@K8npRS{QB0$lFT8_{{Rh2aCNKDKdvA{L%qL(>KrBF9 z)lA-CS9X{PYsV2F*kZXLgHkkX9o$t0i*Ealo?6BA94igq%(VvDZ?ZCrr4SR5WguDr z<#j5_lZUwFG(9*mGJrkBSvy1nVyn{_MoZjUA4u`UbYW^*E;LN|cTomG;#{I&3YtO2 zF5>JE!S*vQ0{W0=$+wBTWa_c9B_DSrcR@evWqsYBCzpY<|} zERY6ng1&Q~xbi8`2h#=oj6Kqp+X3#Gzi`@-Rco%5e}nTh-JlCc-PTWt0?cVNK1+Us z)T{caYykJleMeO^$06W-7q^HBV(MGo*WNkz287VOg=#I1wA$7271cX8uaCdtFYy*) zRm>S-mcqE8>bbrW)lgjJ1FC=-6vBp0ikWt;!6ld`<7B4Uux2X6O~7A)6=qxw3EYBr zIfKc>ug$v@f8=!lEcteNV5DBGbMqFIrZfQLWeOC8Go;iqXeH*doJ)l$^*rAA4zzDX9P@Tr~(Hb#M7 zR)e@7aw<@g%;*ArMPueDgc?~tCgRH-^Trz<7&||5fSIWfap&`VN)*V3exAuptN|55 z9|^GCx2oQ*6NT2vLHx#07AyN5+@esK09oBiPFrgL3|G6Ibb(@>=MjmZplVjZMLDeC zxlt^jEEl@evNDJN07;2$tjqK5e~58pNdEx3BW%C4C5RLPpKvVG#TSEPX^=03HzQCg5*T*c$k-9{k> z^Bvut4rN>+vM3rY+_6odp2J`zL~EJ@aycak^riyqbQXtlu(+X0A4_NZiBJyev-ZII zh{U5|ao+xG=5FHcw*$_9`zxX#+mrQ0#U03}*8c$cE(>?9PgaNOD@mOdit4fYi?`j< zE9&3oAkvS-?@#0N6Xq{z(*FRtd%&2wGJjIQXmiU2u!SvD0@va$5E5Mw-7CaY3uMlS zp@xD*vJv2ho`b29*j|S)O`DZ~Bu<}EO5pMa5E&X^h$h%CN>t0qzswy#8mkF>Ocs{N zs@h>W5RH{@Pf#d;@)a;F3rpD7<{;G->UwMWhsiWb;Yiwos$HF}eGDiK>uHh6&p-Dw z=io6rVEXPfrie#*dej9+Y1@CZKqrFcR~`vx0DrqEcoe_?~pZ0gG@tA(p!#rtvR=Ao_j~c=70t5sM0T+f@sU zn5-2BvIk7Y*;>^_ZY3MutZl>vi=PB?fDfXKP?@PCq3|3t2eC`P^XmrWq}%Q=1mH0Zg=*_8Hb9`DtC`xFQVs4ZJAJ}ZqOcD0 z1M*Hbqfj?ce=@G+OM-AV6UxIO>8vb0sWIa zn$u=1!>XtEC}-)IY0|jgxI1iwV5Xt<^31^IvAn;jgU$dN>iGC2#IBePqQ2tt!Zq!8 zDVFSDit+D{QpM=Z+dm)gQqurh_F?E+l)$Wz{MFBM$RDgZHhvvwT2CA zmChwCCg6bi_XehVyFOw#n4`JCrh0$1Kq*fB9pD@Hh3Bb_iJN=}tE^j&HxD2WtL^Jq z{L1B_fPe9K_I_-sn55M&Kd;O>bty{;$6LO7loCsBf!&M!5Gz4#gP)X`u}kCg+-jza zkFa6;{Q8S%8Agt-Su@#6MHv2NoeQd%jB=>XO9Y`+nRN2sq5x0PmOvIl91M9TCnM9T zc3)xyL?-W0BC`us!4p_jUqJ{=Et|f1eNTWC&uN4X26Xr%XxwR22AV)s6X4<~E;f37 z4ByOc(T&#oGMc{N(4~Ma?w78hS_6}aG!Am*VB>TtsLpt{&Sj7v`7+r}>41^xG|Y^z0l3uw z>}D=j1;i#Dkyl^TcTJcE)(5MA;U205^BIyDSg19!ti{}bwxZXJb${{hE%@9kdl!9T z1)3F_X^kTD`;@I2u%E=gsK{wyD0^w51Z1~JzxHF)xJAxTUl_?>VqY-|q@`6#&+9(@ zB%`ecU~{?+4>%$P^-YC57|OjsMXODt#CFeRnPUxWVHYU7*J z{{Rx?UF8Kl@P9D`xT7$Y#oP)ZC(+OM0J>-f#AwzpbrqkGsB)vVneJre zS%UOZmBdSlH9{!@@Ur(n139Qh=+mIM7&snPtTr}BUaVxM;OVS9ToKZR@?S7af{Km+ z(G?BnE=7K~IsG;(4!HcnTI^L_b&lP}K@yavOC98`{6dkDXqh>)_#WZoQA(h`oVt!o zSy9cl)0f`|)Df{}05PC&{8r#z3mm~hxW~qJcQP|9P4{Z1%>Q*U(t05FE=t4Z%XTh~lw`r=FqW-Y8K>KQ$$pbgUuK;XBc zh8;?3>??tOcELok91%zOBbYc&Pfot0dfRqz55WH7j56Lzg-D{TQ05I(gmfG>T)2Up z6I{G|J#HGk3fbxEUtTvg1=)1>Dzp~Y%)-QsNfGq{{Rx%VX6b=(30gy3|OaDf$t6R z6*mrfS6RmUxrIrb!G44DR}^hoLFhGy?=p$bBkg<@kBMASnX-?-jr~MQ%3HlFU3Rc1 zxX^WI!aR@aGf*hpRn=L1U-CkJD5H0W4w}AZewL^Pf-}vSxb5rQpQ@$WxGHhOv3J$$ z^EP1ki(4sX@XA~bQoR6M2Hy!PMbOv1%TiuJXrK?E{lyWtP~Rbc#HtwLRw#;`DAa0& zsIu}^`H}4EJwm`N(S{nPG*Yi%@9w1t8UWq5EfdL+0Y7QHjSDj_}d&#z`uovz7fI%Pud#-MOQle^82miA^iVb-YS|i)|Yz)o#fuxDWtd z%y4F(xS}SMZg*zsgar$Q(%}P5D_G5IS4O*Yz%GY^!&`tS-EP9bybCmS%o6rOiLM>& zG#zmi-B9l@Et|1gR#vJ~H6t6rlNVe##Kn1AH~=w!`wKM+m%6jvW0`FLCX-6G7)uv$fj5VhAUvhvVn_t>Gy?|S2GJ(p# zco3Wr+v{^;k|hTtU#VR9fhS(DK=SB0QW>tF?o%6e9TNA~23M9X%r=-!kFkUfD-OFs zdb>K*$gmWqSb|wS3 z(iuNxFcrEUUCT{W21N%O%2D}cQ;`fJMu#4*P*pL;fZgBwQx(BeBI9fl_UaiS?#dmD zNNe>MEjj2i$2G?SvUc99aZLmVP6D~s3V>u>g>JsOmDw^*PQE&noB=}1?MOyW!c(@& zmO-Q-#>K9C$58>9Rz%$n2D1Cbb7?nOXrit#0dJgfx`I@KZviRqGVntukgII9xT@I6 z2D!0upHil#2MRk=pZbmy5^fFvJ&TwB08J64Q+0C$#Z=pYt1-Wa1O`wgnKgfleaq89 zw-;M2#qOnA&~O!Z-ai8rQL2H8Jt)RHKI)?Y@1&nx(D9(3aue-^6pU-~ioGZ!xI!hMR zTU<3&1RxZSogfrlxAOsNi*<_K@IR6r57&T}urPfFAa#PK6$pL1=d!92vR(&FJNeDA zVseGFFudi@^EL9(O9g^-PpzB8TpTCZxo50(S(%8!(QUV0+4K+CM8lM!yp6A|W%-;k z3nEr-+7LZ`a|5}UI1STS@myC^>)mYUg{M?;R{>{lb|sKvLgrH_8*jzfc}jBwTh_fZnu<`wfd_2V@^ zQ6Qmcx(pSncpmCujknUFRTt=(nxv~dXLy6U95GDKAnc*x4)Wq=E;#i931VeLbYNs< zkSI!kiF*nT;x%3be&a@^jXjZ2PHf=6`;;e7oy67Tp>y6MLipiPN1ZJKHed>ggUVF0 zmvvVxU|$5m7e$3VdWr=ARyRqQmvV>$Hq<&mi!})DHmy}n`h&;-p?GgNhQtK+ox^cS&Hh&n@Nt;e7SVTnee2U7r;Jl%mb6AT2A0~u2P0GNMSiv=ya`rcrwJ(BEYE*i6` zTYCP3qT`Rcp3k-ZAw5F1$lx5;d^ea+;84Lf_wvKy4rtFr{{U)Q*?CJxz+KXzoE^)3 zMJ(JcUo;lrj`A9uh42^a`Gwz&4Lw}eF|)aPw?bb)&Rty3L$Phkfo5{;4pbwox?9^M z!o7SHN5F990W-~J;$_cVteC7njZ9_mv>l)cf9xbel$Drk4BfV3zXrl-040%(J^cMd zOoL>zSQK)fIU67yN2t%Lr1S&2m(d}0OY|E);e3)c{M!P}s9$jCg|T?4WVRFzSy!#* z&&%ow6x5=q_>UECJ-vD%6b7@C(bP#xb<9~THZ=<{7!kD94}f2n+*Rqc`iCNjF5m$O zwKa0F(Kl>04P=M`7AwIDo(4U!hQ)^3CNL})hp6a9fDTB&a0(Q%xSE*0 zwo=F>qi?u|Mwc)5b6D=L$W%I}mA2(ztPYvWPR;8=md`j%q(F3kJR_>EGfQUET+EKZq)eu)l%P(Jq}O zpIj2an9YT}HT(QT7#1`Y>>@vLHPu1^L2z4z1v(hN`}9M35T$7Dq9aRa3jIDNVsi$7 ztz4^k*;Digy!$^nFmjptIQ8lrPOA3Vj&pUIiC-nP4ua`dciRV5yfO}J_*gB_i`CuU zU&z0x@SwB^t_y|xxca@ZP&a%N-{zwo90tJh71?{ma~skb6i0f|gI`jf2L|t^6dO1d zYk|fURDmmz#N|+JH(D;0>N_}S%)<;E%D=hNv>BHwv{wj9nXk@Zy6jG&YFh=k@05#x zr(-0<7A(zzuuW39Osl=Xe1mXV+J;751;84V{oZu5s2FzT9v={y`B;}Aq;4XX(7eOW zy#QsL@+yba1p!3iWTO`*hAt@8t4vD=Q$r~?L_Cnt6QjSVjK0hR<}B7+EAB(-!lplP zSC!@EEmIL|`>6K8cr|RWPzzlh6P~~t`I@lS(eP9j#e{lZ6uMVfm>?Bo>bRHyn-n)v z)XOPFBHOu+hfFm5r?(=M?)jFH}0ZkM1j2t>&4k zkQmZfVJ!X`(K-2{%is_eM=HhoxkprK+BWLRY#x0|AD~o_j`&woL=|p1&9(Ku+JXuB;<-D^t&C_ zpb(&4$2#Na`-8`BK%Lpk{{V3HYq>@jj%yn_iIpONwV>3ph%nwS=vU$}*7INpy&LfapsLf6^UZOfza*_VL+ua@pBl_D2=PNYhweF%T^i9?jbBHwk?J* zry<LrV~;O|J|+uucRjBzYpm#L$nEX}8i|mKS5{A4NBAp&3bF6k3I^xpO0J zvQ=geq*`$6mRq%3RWWgP#9)XTa5m<1A*(o=mW^^s>#YnhT(Pl&1E8XE+!bc=dnJ06 zrLUNgfwdNWz>J* zp~Vrl6&IneaS^boFu@Z>4THH#p}R(N1+?Ux+VRkoH69GFuW&_R1JiH>(&Z(X%QhdC zMM)bWDf!}5m~z15mKI@{UgEUd@YJzYOJbGE)n#SQhg2c>Ou$!x0Z(YJG5lB>EOf+1 zeIUxh$&cj}uu*Z+G3enCV7hT7wP#_qlw341*(mj!4pBq#Y|M5cT^9-xOC}W&RaGO1 z6;Zo46$5}+pj-+>#(C1_AyGC;1yoX;%2+LuGXa``wPE>#VXG{Wx&>ej5K&;l)NBp{ zq)TA98X1{NcNR?>TXNWcgE1&tESN>Ixg+rjs7*FmPcF9f!l^JgU))PT0NeYN1uSR~ zvT+o`GcApv&M#)KxK9+h8HvgTG{7-|VP9py?S1XWCjiuolO&>FnRsvCiU#zm>$o5Q~=u<1E1;Beb zh7#I35bRx5smv3GjzilBA$X*gFkY*ds7%7qoj{Xb7%2#=h0Fq@P&!X>v8?8`VC)5v z#d;vBaH6?=z~b68<%KCa9$=K{Ij)|uzqp%5QVPNS-exfa9hcN@0-!h*N-`%iDeYxd z#{Emvc8r0ecWPL+ak7vKiDCGN7=2PD6VUu6;zog$_byqjIzS@i&LXvNqUGaLF9HfS z->uBOYB&gv@J);pUj(DKzU8F>*Qj)HTs0l#t|}fDa6v52iiT=ih6K5#x8u^Q12UtJ&iC(O{unHV<%3dKs^^wt#v7_O{887MJg)U#9frM zLnst`HpEpMJ*AK_;<$;d1~CPuZBp!*nj68U7zE(*F%Z;V;b5TA&BIYk4541+d=m)K zTk23(EPb#x4{#w=^8kM^L04H?rHRp?2n6j>(FTBpuQG`>99*cS6c;LStwmykUfAsr z;CPh@Chyv1f|XFH*nZ*atX)N3T;<{eEpiT6TEKa(B{5b(FOj0{Ev*LZ<|vs~>|QD% zXr`T2pV2X4p>N_K5MvKLz5GO==%Tm|p)yBdgHK|a5O0-4+u_o|#m^=ZAaZ3l?tVaA z=r|BTL!wf^D;CTY*zwFNGVDr74w{5#mr+eE5xiM*DSeDza~7yka8|bqo5KF41TUBw z<8gdJW^&yS=|fVfN5lh)I7>h_!Bz%kCDfGyd$({QLXFbZ>K1{%C9FesgpD*?g~b)j zE~3A4J3~@CfLAE0Q5kl^jHv3E&tmSbJtqiCN;Fpp#8@OuZJD`Hv_Lr2nQ&7~XVy51 zwvA#a$W@1NUqr5$>$@!u;+6^3OvS6%47*^q`-<8k6pTSE6mqyUPXh4*t#-%(U@5%A zQ$UH_X7VHEy{8hymccYf5XnlHj<-XI@GV255Q?3Wgz#w)jlzou#824=FDh11g1+UZ z^|LT?Dx*zRqZ-D@Dy{bo4cAu$YPgy{NcCtiKuDHsVoF*O+?K{ag9WQIVs zK8V5CO~kji+)jY8xIrzv$|j+>)V$#$8-lk5g?>n4b37waZj#_XlI2QJjN^#%{$c@u z%8J`kf+v^-E=vsF zG{CJ(MO~HRGBI`3q@mIpmLinyCXy_6hL+dyHK1t+TH}cDIooh4nX7P`o0kJE$`?IL zqVQ(j#b^PRD_LfVVywL8D@P-!n=o8J+$~kWyE{O}Y?ThF+t)Iw-cjLLN#rHic5anSRd zm5o^undl>;$72BnL6w3zHJp*y10^HOuWrY!n>4G`C?TQj3O6A zdyGiv<}_O3Qsq3N(v2*8USd@nGf+Wc<~29sTjvGR8VXCu_!^v+Q(|g1`Jr2P_^&l5m@UH(5|CwcQAnlsQ6N?t?^Mz zh9QX;rKTkCuq-z*cx-n7#}SaMX^j0`T=~O^ha`0>f{1QZF`BkkfcTVv-Tq?&PDT&y z3T`J#Sgb&CL4};yN1=x@%}8KH^D#2HSxVzGHvMEVwF5X@P5DazP>IMs3Ga#8DKwEK!7>e6A!IwxRfv()eK}}4!qceGzD^SvTdP?5F z$XJL9sa*yNWzpMP%L17`IFdK#eU0e{bH8(1~L~Z#+95n}FFPEr$LYT{k zL=$V$S4N7KMt!jNRlyJC>W)hziBuCK)D#T>n?@W)sG4IF2%(}=Pd!V@gtnqE35mc; z5q+k%5Ha;CbSe#bfvS&k*LQ|6x1kNw9^euY_S_D%&{ zSY!TqffWr@z^FNd17{TxAxzw1in-|;G`XQkhf^eB!dOfX5Kt|!gc!3v_YU+12q2^f za_k8ZYuO1o9Pt(Q&Pbr?vjoE6Ovaq2$+sl3vlg{oOIm`- zmM|tdITlyQN{3&3gDf@?+_46fvEm~_+Bl7=n2H;WYG=P035L6r%_>xD+-mz`G$6+n zHy+CvQmc1(h-y^FO!p8M4y%7;zY8TXto$)wV}idOUV4na068WSXvQiq`W(>B`{*6Q!glO6q1Ntw-^EOE(VZ* zw?i&Y9ZNZL1?-7&Vp!nBxyI@c3U4xpCRN(2+&fGp`1qq}4yNvR+z6N2COj9g3m~){D0+s1|u37&0=z zT*1W0!$zh;S*S*UZU7@L<&`N*$!_`DJH%Ot6x#yBGR9>9*Md7JqneHv2#IpTkPHzS zFie{ViSR@=1DjJ-xvpz~VcslHfADoleN&8{0=ImSPOcp-e)AgtGmi zhJvnSEK*>W9wdr4UbP8I-}N;LAi$LBWLgL8vdY5UJw%F0#Ih|^N;`3Rlm)bwsuIn0 z98ORYpt{^jmZh3vu_!20#2Rb8!W2{{F7Chwquk?y!@0=M61>UC%1#g&_%Mzl^9lo9 zrVluPhU@r(0sx{(f6;KULEbV~|o?TX!6Bv^MFR<#naz%dYsRRB;JDD7qLbF|{* z2Am533y-;a90XF)W?ZqW%qb{hT^o3UES-|<+i;qevwAZ0i4nxdrZ~ekj4 Date: Sun, 25 May 2025 21:15:49 +0300 Subject: [PATCH 06/23] fix: some fixes --- pkg/config/config.go | 5 +++-- pkg/logger/logger.go | 16 +++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 6578695..d1663b6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,6 +2,7 @@ package config import ( "github.com/ilyakaznacheev/cleanenv" + "log" ) const AppName = "previewer" @@ -14,7 +15,7 @@ type Config struct { } Server struct { Host string `toml:"SERVER_HOST" env:"SERVER_HOST" env-default:"localhost"` - Port string `toml:"SERVER_PORT" env:"SERVER_PORT" env-default:"8000"` + Port string `toml:"port" env:"SERVER_PORT" env-default:"8000"` } Capability int `toml:"CAPABILITY" env:"CAPABILITY" env-default:"10"` Debug bool `toml:"APP_DEBUG" env:"APP_DEBUG" env-default:"true"` @@ -28,7 +29,7 @@ func MustLoad(configFile string) *Config { cfg := Config{} if err := cleanenv.ReadConfig(configFile, &cfg); err != nil { - panic("cannot read config: " + err.Error()) + log.Fatalf("Error loading config: %v", err) } return &cfg diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index d968fa8..a814958 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -2,35 +2,33 @@ package logger import ( "github.com/rs/zerolog" - "github.com/rs/zerolog/log" + zlog "github.com/rs/zerolog/log" + "log" "os" "time" ) -var AppName = "undefined" - func MustSetupLogger(app, stage string, debug bool, level string) zerolog.Logger { zerolog.MessageFieldName = "rest" zerolog.LevelFieldName = "severity" zerolog.TimestampFieldName = "timestamp" zerolog.TimeFieldFormat = time.RFC3339Nano - AppName = app var logs zerolog.Logger if debug { - logs = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + logs = zlog.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } else { - logs = log.Output(os.Stderr) + logs = zlog.Output(os.Stderr) } parsedLvl, err := zerolog.ParseLevel(level) if err != nil { - panic(err) + log.Fatalf("Error loading config: %v", err) } - log.Logger = logs.Level(parsedLvl).With().Str("service", app).Str("stage", stage).Logger() + zlog.Logger = logs.Level(parsedLvl).With().Str("service", app).Str("stage", stage).Logger() - return log.Logger + return zlog.Logger } From f19154a58d3195d447ffabd21e81b7e6618a7979 Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 08:14:39 +0300 Subject: [PATCH 07/23] fix: ci lints and tests --- .github/workflows/tests.yml | 51 +++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100755 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100755 index 0000000..55e51a9 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,51 @@ +name: Demin final project tests + +on: + push: + branches: + - v* + +env: + GO111MODULE: "on" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Extract branch name + run: echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ~1.24 + + - name: Check out code + uses: actions/checkout@v3 + + - name: Linters + uses: golangci/golangci-lint-action@v3 + with: + version: v1.64.6 + working-directory: ${{ env.BRANCH }} + + tests_by_makefile: + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ^1.24 + + - name: Check out code + uses: actions/checkout@v3 + + - name: make lint + run: make lint + + - name: make build + run: make build + + - name: make test + run: make test + working-directory: hw12_13_14_15_calendar From b0e0d7e4b999d2fa39d3627dfadaf1db69c0cfd4 Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 08:18:11 +0300 Subject: [PATCH 08/23] fix: ci --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 55e51a9..bce9e3e 100755 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,6 @@ jobs: uses: golangci/golangci-lint-action@v3 with: version: v1.64.6 - working-directory: ${{ env.BRANCH }} tests_by_makefile: runs-on: ubuntu-latest From c31580082afef991a2b758e01cb23fc34f28d892 Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 08:18:26 +0300 Subject: [PATCH 09/23] fix: ci --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bce9e3e..024305a 100755 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,4 +47,3 @@ jobs: - name: make test run: make test - working-directory: hw12_13_14_15_calendar From 3d65a068b732451232199701f1fbc6614c8ec419 Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 11:42:00 +0300 Subject: [PATCH 10/23] fix: after linters --- .golangci.yml | 71 +++++++++++------- cmd/previewer/main.go | 9 ++- .../filemodifier.go} | 4 +- .../filemodifier_test.go} | 4 +- .../testdata/valid_image.jpg | Bin .../filesearch.go} | 14 ++-- .../filesearch_test.go} | 29 +++++-- internal/{lru_cache => lrucache}/cache.go | 0 .../{lru_cache => lrucache}/cache_test.go | 0 internal/{lru_cache => lrucache}/list.go | 0 internal/{lru_cache => lrucache}/list_test.go | 0 internal/server/http/middleware.go | 3 +- internal/server/http/server.go | 51 ++++++------- pkg/config/config.go | 3 +- pkg/logger/logger.go | 6 +- 15 files changed, 111 insertions(+), 83 deletions(-) rename internal/{file_modifier/file_modifier.go => filemodifier/filemodifier.go} (96%) rename internal/{file_modifier/file_modifier_test.go => filemodifier/filemodifier_test.go} (97%) rename internal/{file_modifier => filemodifier}/testdata/valid_image.jpg (100%) rename internal/{file_search/file_search.go => filesearch/filesearch.go} (74%) rename internal/{file_search/file_search_test.go => filesearch/filesearch_test.go} (52%) rename internal/{lru_cache => lrucache}/cache.go (100%) rename internal/{lru_cache => lrucache}/cache_test.go (100%) rename internal/{lru_cache => lrucache}/list.go (100%) rename internal/{lru_cache => lrucache}/list_test.go (100%) diff --git a/.golangci.yml b/.golangci.yml index 8552d18..6c25252 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,51 +1,29 @@ +version: "2" run: - tests: true build-tags: - bench - - !bench - -linters-settings: - funlen: - lines: 150 - statements: 80 - -issues: - exclude-rules: - - path: _test\.go - linters: - - errcheck - - dupl - - gocyclo - - gosec - - depguard - - path: .*\.go - linters: - - depguard - + - "" + tests: true linters: - disable-all: true + default: none enable: - asciicheck + - bodyclose - depguard - dogsled - dupl - - bodyclose - durationcheck - errorlint - exhaustive - funlen - - gci - gocognit - goconst - gocritic - gocyclo - godot - - gofmt - - gofumpt - goheader - goprintffuncname - gosec - - gosimple - govet - importas - ineffassign @@ -60,11 +38,46 @@ linters: - predeclared - revive - staticcheck - - stylecheck - tagliatelle - thelper - - typecheck - unconvert - unparam - unused - whitespace + settings: + funlen: + lines: 150 + statements: 80 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - depguard + - dupl + - errcheck + - gocyclo + - gosec + path: _test\.go + - linters: + - depguard + path: .*\.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - gofumpt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/cmd/previewer/main.go b/cmd/previewer/main.go index 73e507a..ce2a8f5 100644 --- a/cmd/previewer/main.go +++ b/cmd/previewer/main.go @@ -4,14 +4,14 @@ import ( "context" "flag" "fmt" - lrucache "github.com/DEMAxx/project_work/internal/lru_cache" - internalhttp "github.com/DEMAxx/project_work/internal/server/http" "net" "os" "os/signal" "syscall" "time" + lrucache "github.com/DEMAxx/project_work/internal/lrucache" + internalhttp "github.com/DEMAxx/project_work/internal/server/http" "github.com/DEMAxx/project_work/pkg/config" "github.com/DEMAxx/project_work/pkg/logger" ) @@ -25,6 +25,11 @@ func init() { func main() { flag.Parse() + if flag.Arg(0) == "version" { + printVersion() + return + } + cnf := config.MustLoad(configFile) logs := logger.MustSetupLogger(config.AppName, cnf.Env, cnf.Debug || cnf.Local, cnf.LogLevel) diff --git a/internal/file_modifier/file_modifier.go b/internal/filemodifier/filemodifier.go similarity index 96% rename from internal/file_modifier/file_modifier.go rename to internal/filemodifier/filemodifier.go index 16d45f1..dbbb49b 100644 --- a/internal/file_modifier/file_modifier.go +++ b/internal/filemodifier/filemodifier.go @@ -1,7 +1,8 @@ -package file_modifier +package filemodifier import ( "errors" + "github.com/h2non/bimg" ) @@ -22,7 +23,6 @@ func ResizeImage(inputPath string, width int, height int) ([]byte, error) { Crop: true, Type: bimg.JPEG, }) - if err != nil { return nil, err } diff --git a/internal/file_modifier/file_modifier_test.go b/internal/filemodifier/filemodifier_test.go similarity index 97% rename from internal/file_modifier/file_modifier_test.go rename to internal/filemodifier/filemodifier_test.go index beef61a..a25815a 100644 --- a/internal/file_modifier/file_modifier_test.go +++ b/internal/filemodifier/filemodifier_test.go @@ -1,4 +1,4 @@ -package file_modifier +package filemodifier import ( "bytes" @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -// Путь к директории с тестовыми изображениями +// Путь к директории с тестовыми изображениями. const testImagesDir = "testdata" func TestResizeImage_Success(t *testing.T) { diff --git a/internal/file_modifier/testdata/valid_image.jpg b/internal/filemodifier/testdata/valid_image.jpg similarity index 100% rename from internal/file_modifier/testdata/valid_image.jpg rename to internal/filemodifier/testdata/valid_image.jpg diff --git a/internal/file_search/file_search.go b/internal/filesearch/filesearch.go similarity index 74% rename from internal/file_search/file_search.go rename to internal/filesearch/filesearch.go index cb26787..223923f 100644 --- a/internal/file_search/file_search.go +++ b/internal/filesearch/filesearch.go @@ -1,20 +1,21 @@ -package file_search +package filesearch import ( "fmt" - "github.com/rs/zerolog" "io" "net/http" "os" "strings" + + "github.com/rs/zerolog" ) -func FetchFileFromURL(imageUrl, outputPath string, logger *zerolog.Logger) (*http.Response, error) { - if !strings.HasPrefix(imageUrl, "http://") && !strings.HasPrefix(imageUrl, "https://") { - imageUrl = fmt.Sprintf("https://%s", imageUrl) +func FetchFileFromURL(imageURL, outputPath string, logger *zerolog.Logger) (*http.Response, error) { + if !strings.HasPrefix(imageURL, "http://") && !strings.HasPrefix(imageURL, "https://") { + imageURL = fmt.Sprintf("https://%s", imageURL) } - resp, err := http.Get(imageUrl) + resp, err := http.Get(imageURL) //nolint if err != nil { return nil, err } @@ -43,7 +44,6 @@ func FetchFileFromURL(imageUrl, outputPath string, logger *zerolog.Logger) (*htt }(outFile) _, err = io.Copy(outFile, resp.Body) - if err != nil { return nil, err } diff --git a/internal/file_search/file_search_test.go b/internal/filesearch/filesearch_test.go similarity index 52% rename from internal/file_search/file_search_test.go rename to internal/filesearch/filesearch_test.go index c04554e..786de89 100644 --- a/internal/file_search/file_search_test.go +++ b/internal/filesearch/filesearch_test.go @@ -1,12 +1,13 @@ -package file_search +package filesearch import ( - "github.com/DEMAxx/project_work/pkg/logger" - "github.com/stretchr/testify/require" "net/http" "os" "path/filepath" "testing" + + "github.com/DEMAxx/project_work/pkg/logger" + "github.com/stretchr/testify/require" ) func TestFileSearch(t *testing.T) { @@ -15,23 +16,37 @@ func TestFileSearch(t *testing.T) { logs := logger.MustSetupLogger("previewer", "Test", true, "info") t.Run("success", func(t *testing.T) { - r, err := FetchFileFromURL("https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg", outputPath, &logs) + r, err := FetchFileFromURL( + "https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg", //nolint + outputPath, + &logs, + ) require.NoError(t, err) require.NotNil(t, r) require.True(t, r.StatusCode == http.StatusOK) + err = r.Body.Close() + require.NoError(t, err) }) t.Run("wrong address", func(t *testing.T) { - _, err := FetchFileFromURL("https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/not_gopher_original.jpg", outputPath, &logs) + r, err := FetchFileFromURL( + "https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/not_gopher_original.jpg", + outputPath, + &logs, + ) require.Error(t, err) + err = r.Body.Close() + require.NoError(t, err) }) t.Run("not found", func(t *testing.T) { - _, err := FetchFileFromURL("localhost:9999/image.png", outputPath, &logs) + r, err := FetchFileFromURL("localhost:9999/image.png", outputPath, &logs) require.Error(t, err) require.ErrorContains(t, err, "connection refused") - }) + err = r.Body.Close() + require.NoError(t, err) + }) } diff --git a/internal/lru_cache/cache.go b/internal/lrucache/cache.go similarity index 100% rename from internal/lru_cache/cache.go rename to internal/lrucache/cache.go diff --git a/internal/lru_cache/cache_test.go b/internal/lrucache/cache_test.go similarity index 100% rename from internal/lru_cache/cache_test.go rename to internal/lrucache/cache_test.go diff --git a/internal/lru_cache/list.go b/internal/lrucache/list.go similarity index 100% rename from internal/lru_cache/list.go rename to internal/lrucache/list.go diff --git a/internal/lru_cache/list_test.go b/internal/lrucache/list_test.go similarity index 100% rename from internal/lru_cache/list_test.go rename to internal/lrucache/list_test.go diff --git a/internal/server/http/middleware.go b/internal/server/http/middleware.go index c3ba0e7..a972c41 100644 --- a/internal/server/http/middleware.go +++ b/internal/server/http/middleware.go @@ -2,9 +2,10 @@ package internalhttp import ( "fmt" - "github.com/rs/zerolog" "net/http" "time" + + "github.com/rs/zerolog" ) func LoggingMiddleware(next http.Handler, logg *zerolog.Logger) http.Handler { diff --git a/internal/server/http/server.go b/internal/server/http/server.go index feaae3e..2350c33 100644 --- a/internal/server/http/server.go +++ b/internal/server/http/server.go @@ -4,24 +4,23 @@ import ( "context" "errors" "fmt" - "github.com/DEMAxx/project_work/internal/file_modifier" - "github.com/DEMAxx/project_work/internal/file_search" - lrucache "github.com/DEMAxx/project_work/internal/lru_cache" - "github.com/DEMAxx/project_work/pkg/config" - "github.com/google/uuid" - "github.com/rs/zerolog" - "io" "net/http" "strconv" "strings" "time" + + "github.com/DEMAxx/project_work/internal/filemodifier" + "github.com/DEMAxx/project_work/internal/filesearch" + lrucache "github.com/DEMAxx/project_work/internal/lrucache" + "github.com/DEMAxx/project_work/pkg/config" + "github.com/google/uuid" + "github.com/rs/zerolog" ) type Server struct { - httpServer *http.Server - grpcHostAndPort string - logger *zerolog.Logger - cache lrucache.Cache + httpServer *http.Server + logger *zerolog.Logger + cache lrucache.Cache } func NewServer(logger *zerolog.Logger, hostAndPort string, cache lrucache.Cache, cnf *config.Config) *Server { @@ -58,20 +57,20 @@ func NewServer(logger *zerolog.Logger, hostAndPort string, cache lrucache.Cache, return } - height, width, imageUrl := parts[0], parts[1], strings.Join(parts[2:], "/") + height, width, imageURL := parts[0], parts[1], strings.Join(parts[2:], "/") logger.Info().Msg( fmt.Sprintf( - "Extracted vars - height: %s, width: %s, image url: %s", height, width, imageUrl, + "Extracted vars - height: %s, width: %s, image url: %s", height, width, imageURL, ), ) - if !strings.HasSuffix(imageUrl, ".jpg") { + if !strings.HasSuffix(imageURL, ".jpg") { http.Error(w, "Invalid image URL format. Only .jpg files are supported.", http.StatusBadRequest) return } - cacheKey := lrucache.Key(fmt.Sprintf("%s_%s_%s", width, height, imageUrl)) + cacheKey := lrucache.Key(fmt.Sprintf("%s_%s_%s", width, height, imageURL)) cachedImage, found := cache.Get(cacheKey) if found { @@ -89,20 +88,15 @@ func NewServer(logger *zerolog.Logger, hostAndPort string, cache lrucache.Cache, uid := uuid.New() fetchedFilePath := fmt.Sprintf("%s/%s_%s_%s.jpg", cnf.UploadPath, width, height, uid) - if resp, err := file_search.FetchFileFromURL(imageUrl, fetchedFilePath, logger); err != nil { + resp, err := filesearch.FetchFileFromURL(imageURL, fetchedFilePath, logger) //nolint + if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return - } else { - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - logger.Error().Msg("failed to close response body") - } - }(resp.Body) - if resp.StatusCode != http.StatusOK { - http.Error(w, "Failed to fetch image.", http.StatusInternalServerError) - return - } + } + + if resp.StatusCode != http.StatusOK { + http.Error(w, "Failed to fetch image.", http.StatusInternalServerError) + return } // Resize the image @@ -118,8 +112,7 @@ func NewServer(logger *zerolog.Logger, hostAndPort string, cache lrucache.Cache, return } - ResizedImage, err := file_modifier.ResizeImage(fetchedFilePath, widthInt, heightInt) - + ResizedImage, err := filemodifier.ResizeImage(fetchedFilePath, widthInt, heightInt) if err != nil { http.Error(w, "Failed to modify image.", http.StatusInternalServerError) return diff --git a/pkg/config/config.go b/pkg/config/config.go index d1663b6..5c981af 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,8 +1,9 @@ package config import ( - "github.com/ilyakaznacheev/cleanenv" "log" + + "github.com/ilyakaznacheev/cleanenv" ) const AppName = "previewer" diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index a814958..b1a89f5 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -1,11 +1,12 @@ package logger import ( - "github.com/rs/zerolog" - zlog "github.com/rs/zerolog/log" "log" "os" "time" + + "github.com/rs/zerolog" + zlog "github.com/rs/zerolog/log" ) func MustSetupLogger(app, stage string, debug bool, level string) zerolog.Logger { @@ -23,7 +24,6 @@ func MustSetupLogger(app, stage string, debug bool, level string) zerolog.Logger } parsedLvl, err := zerolog.ParseLevel(level) - if err != nil { log.Fatalf("Error loading config: %v", err) } From 4b87cde42802cf875aeb16f2624f323dd88a2295 Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 12:01:29 +0300 Subject: [PATCH 11/23] fix: install libvips in github tests --- .github/workflows/tests.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 024305a..1db864d 100755 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,5 +1,3 @@ -name: Demin final project tests - on: push: branches: @@ -23,6 +21,11 @@ jobs: - name: Check out code uses: actions/checkout@v3 + - name: Install libvips-dev + run: | + sudo apt-get update + sudo apt-get install -y libvips-dev + - name: Linters uses: golangci/golangci-lint-action@v3 with: @@ -39,6 +42,11 @@ jobs: - name: Check out code uses: actions/checkout@v3 + - name: Install libvips-dev + run: | + sudo apt-get update + sudo apt-get install -y libvips-dev + - name: make lint run: make lint @@ -46,4 +54,4 @@ jobs: run: make build - name: make test - run: make test + run: make test \ No newline at end of file From c31c1f57b2c072b2570862d88973612da7352f0d Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 12:06:49 +0300 Subject: [PATCH 12/23] fix: tests --- .github/workflows/tests.yml | 24 ------------------------ internal/server/http/server.go | 7 ++++++- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1db864d..4117f0a 100755 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,30 +7,6 @@ env: GO111MODULE: "on" jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Extract branch name - run: echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: ~1.24 - - - name: Check out code - uses: actions/checkout@v3 - - - name: Install libvips-dev - run: | - sudo apt-get update - sudo apt-get install -y libvips-dev - - - name: Linters - uses: golangci/golangci-lint-action@v3 - with: - version: v1.64.6 - tests_by_makefile: runs-on: ubuntu-latest steps: diff --git a/internal/server/http/server.go b/internal/server/http/server.go index 2350c33..da06f60 100644 --- a/internal/server/http/server.go +++ b/internal/server/http/server.go @@ -23,7 +23,12 @@ type Server struct { cache lrucache.Cache } -func NewServer(logger *zerolog.Logger, hostAndPort string, cache lrucache.Cache, cnf *config.Config) *Server { +func NewServer( + logger *zerolog.Logger, + hostAndPort string, + cache lrucache.Cache, + cnf *config.Config, +) *Server { mux := http.NewServeMux() mux.Handle("/hello", LoggingMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From 4471e506e237e2380fd57804452f0f5a6a893cbf Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 12:10:11 +0300 Subject: [PATCH 13/23] fix: bck add --- .golangci.bck.yml | 83 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 .golangci.bck.yml diff --git a/.golangci.bck.yml b/.golangci.bck.yml new file mode 100644 index 0000000..6c25252 --- /dev/null +++ b/.golangci.bck.yml @@ -0,0 +1,83 @@ +version: "2" +run: + build-tags: + - bench + - "" + tests: true +linters: + default: none + enable: + - asciicheck + - bodyclose + - depguard + - dogsled + - dupl + - durationcheck + - errorlint + - exhaustive + - funlen + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - goheader + - goprintffuncname + - gosec + - govet + - importas + - ineffassign + - lll + - makezero + - misspell + - nestif + - nilerr + - noctx + - nolintlint + - prealloc + - predeclared + - revive + - staticcheck + - tagliatelle + - thelper + - unconvert + - unparam + - unused + - whitespace + settings: + funlen: + lines: 150 + statements: 80 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - depguard + - dupl + - errcheck + - gocyclo + - gosec + path: _test\.go + - linters: + - depguard + path: .*\.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - gofumpt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ From 777163674fe92907146e9e66ddea929000b6552d Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 12:21:59 +0300 Subject: [PATCH 14/23] fix: makefile lint --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 91df932..496bb8b 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ integration-test: install-lint-deps: (which golangci-lint > /dev/null) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.64.6 -lint: install-lint-deps +lint: golangci-lint run ./... .PHONY: build run build-img run-img version test lint From 01a358d4eeec0374a6c0d6e2fc0425fcd91bb12b Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 12:31:26 +0300 Subject: [PATCH 15/23] fix: makefile lint --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 496bb8b..3153fa4 100644 --- a/Makefile +++ b/Makefile @@ -35,9 +35,9 @@ integration-test: exit $$test_status_code ; install-lint-deps: - (which golangci-lint > /dev/null) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.64.6 + (which golangci-lint > /dev/null) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v2.1.6 -lint: +lint: install-lint-deps golangci-lint run ./... .PHONY: build run build-img run-img version test lint From 9e3655e867f3010daa4b29d6ef1aa9c55b0115ac Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 12:38:37 +0300 Subject: [PATCH 16/23] fix: test file search --- internal/filesearch/filesearch_test.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/filesearch/filesearch_test.go b/internal/filesearch/filesearch_test.go index 786de89..ffe2b1e 100644 --- a/internal/filesearch/filesearch_test.go +++ b/internal/filesearch/filesearch_test.go @@ -37,8 +37,11 @@ func TestFileSearch(t *testing.T) { ) require.Error(t, err) - err = r.Body.Close() - require.NoError(t, err) + + if err == nil { + err = r.Body.Close() + require.NoError(t, err) + } }) t.Run("not found", func(t *testing.T) { @@ -46,7 +49,9 @@ func TestFileSearch(t *testing.T) { require.Error(t, err) require.ErrorContains(t, err, "connection refused") - err = r.Body.Close() - require.NoError(t, err) + if err == nil { + err = r.Body.Close() + require.NoError(t, err) + } }) } From 2d2445db2ac32d8e5de2819889fd691eb75e91ed Mon Sep 17 00:00:00 2001 From: demin Date: Mon, 26 May 2025 13:57:07 +0300 Subject: [PATCH 17/23] fix: remove file on cache purge --- cmd/previewer/main.go | 2 +- internal/lrucache/cache.go | 21 +++++++++++++++++++-- internal/lrucache/cache_test.go | 15 +++++++++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/cmd/previewer/main.go b/cmd/previewer/main.go index ce2a8f5..d98ea73 100644 --- a/cmd/previewer/main.go +++ b/cmd/previewer/main.go @@ -39,7 +39,7 @@ func main() { ctx = logs.WithContext(ctx) - cache := lrucache.NewCache(cnf.Capability) + cache := lrucache.NewCache(cnf.Capability, cnf.UploadPath, logs) server := internalhttp.NewServer( &logs, diff --git a/internal/lrucache/cache.go b/internal/lrucache/cache.go index 87018f3..73faec7 100644 --- a/internal/lrucache/cache.go +++ b/internal/lrucache/cache.go @@ -1,6 +1,12 @@ package lrucache -import "sync" +import ( + "fmt" + "os" + "sync" + + "github.com/rs/zerolog" +) type Key string @@ -12,6 +18,8 @@ type Cache interface { type lruCache struct { capacity int + upload string + logger zerolog.Logger queue List items map[Key]*cacheItem } @@ -44,8 +52,15 @@ func (lruCache *lruCache) Set(key Key, value interface{}) bool { if !ok { return false } + delete(lruCache.items, valKey) lruCache.queue.Remove(back) + + err := os.Remove(fmt.Sprintf("%s/%s", lruCache.upload, valKey)) + if err != nil { + lruCache.logger.Error().Err(err) + return false + } } newItem := lruCache.queue.PushFront(key) @@ -80,9 +95,11 @@ func (lruCache *lruCache) Clear() { lruCache.items = make(map[Key]*cacheItem, lruCache.capacity) } -func NewCache(capacity int) Cache { +func NewCache(capacity int, upload string, logger zerolog.Logger) Cache { return &lruCache{ capacity: capacity, + upload: upload, + logger: logger, queue: new(list), items: make(map[Key]*cacheItem, capacity), } diff --git a/internal/lrucache/cache_test.go b/internal/lrucache/cache_test.go index b5d53c1..95c4e1b 100644 --- a/internal/lrucache/cache_test.go +++ b/internal/lrucache/cache_test.go @@ -6,12 +6,16 @@ import ( "sync" "testing" + "github.com/rs/zerolog" "github.com/stretchr/testify/require" ) func TestCache(t *testing.T) { + upload := "/tmp" + logger := zerolog.Logger{} + t.Run("empty cache", func(t *testing.T) { - c := NewCache(10) + c := NewCache(10, upload, logger) _, ok := c.Get("aaa") require.False(t, ok) @@ -21,7 +25,7 @@ func TestCache(t *testing.T) { }) t.Run("simple", func(t *testing.T) { - c := NewCache(5) + c := NewCache(5, upload, logger) wasInCache := c.Set("aaa", 100) require.False(t, wasInCache) @@ -50,7 +54,7 @@ func TestCache(t *testing.T) { }) t.Run("purge logic", func(t *testing.T) { - c := NewCache(1) + c := NewCache(1, upload, logger) for _, v := range [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9} { c.Set(Key(strconv.Itoa(v)), v) @@ -68,7 +72,10 @@ func TestCache(t *testing.T) { } func TestCacheMultithreading(_ *testing.T) { - c := NewCache(10) + upload := "/tmp" + logger := zerolog.Logger{} + + c := NewCache(10, upload, logger) wg := &sync.WaitGroup{} wg.Add(2) From c246601ce2df9f25f620200ca9393124f82bd3bc Mon Sep 17 00:00:00 2001 From: demin Date: Tue, 27 May 2025 08:19:11 +0300 Subject: [PATCH 18/23] fix: ignore bck --- .gitignore | 2 ++ .golangci.bck.yml | 83 ----------------------------------------------- 2 files changed, 2 insertions(+), 83 deletions(-) delete mode 100644 .golangci.bck.yml diff --git a/.gitignore b/.gitignore index 6f72f89..7faf2e2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ go.work.sum # env file .env + +.golangci.bck.yml diff --git a/.golangci.bck.yml b/.golangci.bck.yml deleted file mode 100644 index 6c25252..0000000 --- a/.golangci.bck.yml +++ /dev/null @@ -1,83 +0,0 @@ -version: "2" -run: - build-tags: - - bench - - "" - tests: true -linters: - default: none - enable: - - asciicheck - - bodyclose - - depguard - - dogsled - - dupl - - durationcheck - - errorlint - - exhaustive - - funlen - - gocognit - - goconst - - gocritic - - gocyclo - - godot - - goheader - - goprintffuncname - - gosec - - govet - - importas - - ineffassign - - lll - - makezero - - misspell - - nestif - - nilerr - - noctx - - nolintlint - - prealloc - - predeclared - - revive - - staticcheck - - tagliatelle - - thelper - - unconvert - - unparam - - unused - - whitespace - settings: - funlen: - lines: 150 - statements: 80 - exclusions: - generated: lax - presets: - - comments - - common-false-positives - - legacy - - std-error-handling - rules: - - linters: - - depguard - - dupl - - errcheck - - gocyclo - - gosec - path: _test\.go - - linters: - - depguard - path: .*\.go - paths: - - third_party$ - - builtin$ - - examples$ -formatters: - enable: - - gci - - gofmt - - gofumpt - exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ From f56e6e87f7985bd800eaf4edbe2165724bf987fb Mon Sep 17 00:00:00 2001 From: demin Date: Wed, 28 May 2025 20:14:36 +0300 Subject: [PATCH 19/23] fix: tests --- .scripts/lint.sh | 2 +- .scripts/update.sh | 12 -- internal/filemodifier/filemodifier.go | 122 ++++++++++- internal/filemodifier/filemodifier_test.go | 226 ++++++++++++++++----- internal/filesearch/filesearch.go | 12 +- internal/lrucache/cache.go | 6 + internal/server/http/server.go | 69 ++----- internal/server/http/server_test.go | 37 ++++ 8 files changed, 364 insertions(+), 122 deletions(-) delete mode 100755 .scripts/update.sh create mode 100644 internal/server/http/server_test.go diff --git a/.scripts/lint.sh b/.scripts/lint.sh index dba0737..4d6aa42 100755 --- a/.scripts/lint.sh +++ b/.scripts/lint.sh @@ -6,7 +6,7 @@ sed -i '' '/- structcheck/d' .golangci.yml for d in $(ls) do - if [[ $d == hw* ]]; then + if [[ $d == internal ]]; then cd $d echo "Lint ${d}..." golangci-lint run ./... diff --git a/.scripts/update.sh b/.scripts/update.sh deleted file mode 100755 index 69c6e78..0000000 --- a/.scripts/update.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/zsh - -for d in $(ls) -do - if [[ $d == hw* ]]; then - cd $d - echo "Update deps in ${d}..." - go mod tidy - go get -t -u ./... - cd .. - fi -done diff --git a/internal/filemodifier/filemodifier.go b/internal/filemodifier/filemodifier.go index dbbb49b..0912747 100644 --- a/internal/filemodifier/filemodifier.go +++ b/internal/filemodifier/filemodifier.go @@ -2,24 +2,60 @@ package filemodifier import ( "errors" + "fmt" + "github.com/DEMAxx/project_work/internal/filesearch" + "github.com/DEMAxx/project_work/internal/lrucache" + "github.com/DEMAxx/project_work/pkg/config" + "github.com/rs/zerolog" + "net/http" + "strconv" + "strings" "github.com/h2non/bimg" ) -// ResizeImage resizes an image to the specified width and height. -func ResizeImage(inputPath string, width int, height int) ([]byte, error) { - if width <= 0 || height <= 0 { - return nil, errors.New("width or height must be positive") +type Modifier interface { + ResizeImage() ([]byte, error) + GetFromCache() (interface{}, bool) +} + +type fileModifier struct { + height int + width int + imageURL string + UploadPath string + fetchedFilePath string + cacheKey lrucache.Key + cache lrucache.Cache + logger *zerolog.Logger +} + +func (fileModifier *fileModifier) ResizeImage() ([]byte, error) { + fetchedFilePath := fmt.Sprintf( + "%s/%d_%d.jpg", + fileModifier.UploadPath, + fileModifier.width, + fileModifier.height, + ) + + resp, err := filesearch.FetchFileFromURL(fileModifier.imageURL, fetchedFilePath, fileModifier.logger) //nolint + + if err != nil { + return nil, err } - image, err := bimg.Read(inputPath) + if resp.StatusCode != http.StatusOK { + return nil, errors.New("failed to fetch image") + } + + image, err := bimg.Read(fileModifier.fetchedFilePath) if err != nil { return nil, err } resizedImage, err := bimg.NewImage(image).Process(bimg.Options{ - Width: width, - Height: height, + Width: fileModifier.width, + Height: fileModifier.height, Crop: true, Type: bimg.JPEG, }) @@ -27,5 +63,77 @@ func ResizeImage(inputPath string, width int, height int) ([]byte, error) { return nil, err } + if err := fileModifier.cache.Set(fileModifier.cacheKey, resizedImage); err { + return nil, errors.New("failed to store image in cache") + } + return resizedImage, nil } + +func (fileModifier *fileModifier) GetFromCache() (cachedImage interface{}, found bool) { + cacheKey := lrucache.Key( + fmt.Sprintf( + "%d_%d_%s", + fileModifier.width, + fileModifier.height, + fileModifier.imageURL, + ), + ) + + return fileModifier.cache.Get(cacheKey) +} + +func New(parts []string, logger *zerolog.Logger, cnf *config.Config, cache lrucache.Cache) (Modifier, error) { + if len(parts) < 3 { + return nil, errors.New("not enough parts") + } + + height, width, imageURL := parts[0], parts[1], strings.Join(parts[2:], "/") + + logger.Info().Msg( + fmt.Sprintf( + "Extracted vars - height: %s, width: %s, image url: %s", height, width, imageURL, + ), + ) + + if !strings.HasSuffix(imageURL, ".jpg") { + return nil, errors.New("invalid image URL format. Only .jpg files are supported") + } + + // Resize the image + widthInt, err := strconv.Atoi(width) + if err != nil { + return nil, errors.New("invalid width value") + } + + heightInt, err := strconv.Atoi(height) + if err != nil { + return nil, errors.New("invalid height value") + } + + if widthInt <= 0 || heightInt <= 0 { + return nil, errors.New("width or height must be positive") + } + + fetchedFilePath := fmt.Sprintf("%s/%s_%s.jpg", cnf.UploadPath, width, height) + + cacheKey := lrucache.Key( + fmt.Sprintf( + "%d_%d_%s", + widthInt, + heightInt, + imageURL, + ), + ) + + return &fileModifier{ + height: heightInt, + width: widthInt, + imageURL: imageURL, + UploadPath: cnf.UploadPath, + fetchedFilePath: fetchedFilePath, + cacheKey: cacheKey, + cache: cache, + logger: logger, + }, nil +} diff --git a/internal/filemodifier/filemodifier_test.go b/internal/filemodifier/filemodifier_test.go index a25815a..80127e1 100644 --- a/internal/filemodifier/filemodifier_test.go +++ b/internal/filemodifier/filemodifier_test.go @@ -1,8 +1,11 @@ package filemodifier import ( - "bytes" "fmt" + "github.com/DEMAxx/project_work/internal/lrucache" + "github.com/DEMAxx/project_work/pkg/config" + "github.com/DEMAxx/project_work/pkg/logger" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -11,68 +14,195 @@ import ( // Путь к директории с тестовыми изображениями. const testImagesDir = "testdata" -func TestResizeImage_Success(t *testing.T) { - fmt.Println(testImagesDir) - inputPath := fmt.Sprintf("%s/valid_image.jpg", testImagesDir) +func TestResizeImage(t *testing.T) { + fileUrl := "https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg" + log := logger.MustSetupLogger("previewer", "Test", true, "info") + cnf := config.Config{} + cnf.UploadPath = testImagesDir + cnf.Capability = 1 + cache := lrucache.NewCache(cnf.Capability, cnf.UploadPath, log) - // Выполнение - resizedImage, err := ResizeImage(inputPath, 100, 100) + t.Run("success", func(t *testing.T) { + path := fmt.Sprintf( + "%d/%d/%s", + 100, + 100, + fileUrl, + ) - // Проверка - assert.NoError(t, err) - assert.NotEmpty(t, resizedImage) -} + modifier, err := New( + strings.Split(path, "/"), + &log, + &cnf, + cache, + ) -func TestResizeImage_InvalidPath(t *testing.T) { - // Выполнение - resizedImage, err := ResizeImage("non_existent_file.jpg", 100, 100) + assert.NoError(t, err) - // Проверка - assert.Error(t, err) - assert.Nil(t, resizedImage) -} + cachedImage, found := modifier.GetFromCache() -func TestResizeImage_ZeroDimensions(t *testing.T) { - inputPath := fmt.Sprintf("%s/valid_image.jpg", testImagesDir) + assert.False(t, found) + assert.Nil(t, cachedImage) - resizedImage, err := ResizeImage(inputPath, 0, 0) + resizedImage, err := modifier.ResizeImage() - assert.Error(t, err) - assert.Nil(t, resizedImage) -} + assert.NoError(t, err) + assert.NotNil(t, resizedImage) -func TestResizeImage_NegativeDimensions(t *testing.T) { - inputPath := fmt.Sprintf("%s/valid_image.jpg", testImagesDir) + cache.Clear() + }) - resizedImage, err := ResizeImage(inputPath, -100, -100) + t.Run("success different dimensions", func(t *testing.T) { + path := fmt.Sprintf( + "%d/%d/%s", + 200, + 200, + fileUrl, + ) - assert.Error(t, err) - assert.Nil(t, resizedImage) -} + modifier, err := New( + strings.Split(path, "/"), + &log, + &cnf, + cache, + ) -func TestResizeImage_DifferentDimensions(t *testing.T) { - // Подготовка - inputPath := fmt.Sprintf("%s/valid_image.jpg", testImagesDir) + assert.NoError(t, err) - // Выполнение - resizedImage100x100, err := ResizeImage(inputPath, 100, 100) - assert.NoError(t, err) + cachedImage, found := modifier.GetFromCache() - resizedImage200x200, err := ResizeImage(inputPath, 200, 200) - assert.NoError(t, err) + assert.False(t, found) + assert.Nil(t, cachedImage) - // Проверка - assert.NotEqual(t, bytes.Equal(resizedImage100x100, resizedImage200x200), true) -} + resizedImage, err := modifier.ResizeImage() + + assert.NoError(t, err) + assert.NotNil(t, resizedImage) + + cache.Clear() + + path = fmt.Sprintf( + "%d/%d/%s", + 200, + 200, + fileUrl, + ) -func TestResizeImage_InvalidImageFormat(t *testing.T) { - // Подготовка - inputPath := fmt.Sprintf("%s/invalid_format.txt", testImagesDir) + modifier, err = New( + strings.Split(path, "/"), + &log, + &cnf, + cache, + ) + + assert.NoError(t, err) + + cachedImage, found = modifier.GetFromCache() + + assert.False(t, found) + assert.Nil(t, cachedImage) + + resizedImage, err = modifier.ResizeImage() + + assert.NoError(t, err) + assert.NotNil(t, resizedImage) + + cache.Clear() + }) + + t.Run("success from cache", func(t *testing.T) { + path := fmt.Sprintf( + "%d/%d/%s", + 200, + 200, + fileUrl, + ) + + modifier, err := New( + strings.Split(path, "/"), + &log, + &cnf, + cache, + ) + + assert.NoError(t, err) + + cachedImage, found := modifier.GetFromCache() + + assert.False(t, found) + assert.Nil(t, cachedImage) + + resizedImage, err := modifier.ResizeImage() + + assert.NoError(t, err) + assert.NotNil(t, resizedImage) - // Выполнение - resizedImage, err := ResizeImage(inputPath, 100, 100) + modifier, err = New( + strings.Split(path, "/"), + &log, + &cnf, + cache, + ) + + assert.NoError(t, err) + + cachedImage, found = modifier.GetFromCache() + + assert.True(t, found) + assert.NotNil(t, cachedImage) + }) + + t.Run("zero dimensions", func(t *testing.T) { + path := fmt.Sprintf( + "%d/%d/%s", + 0, + 0, + fileUrl, + ) + + _, err := New( + strings.Split(path, "/"), + &log, + &cnf, + cache, + ) - // Проверка - assert.Error(t, err) - assert.Nil(t, resizedImage) + assert.Error(t, err) + }) + + t.Run("invalid path", func(t *testing.T) { + path := fmt.Sprintf( + "%d/%d/%s", + 100, + 100, + "test", + ) + + _, err := New( + strings.Split(path, "/"), + &log, + &cnf, + cache, + ) + + assert.Error(t, err) + }) + + t.Run("negative dimensions", func(t *testing.T) { + path := fmt.Sprintf( + "%d/%d/%s", + -100, + -100, + fileUrl, + ) + + _, err := New( + strings.Split(path, "/"), + &log, + &cnf, + cache, + ) + + assert.Error(t, err) + }) } diff --git a/internal/filesearch/filesearch.go b/internal/filesearch/filesearch.go index 223923f..f4f9dd6 100644 --- a/internal/filesearch/filesearch.go +++ b/internal/filesearch/filesearch.go @@ -6,19 +6,28 @@ import ( "net/http" "os" "strings" + "time" "github.com/rs/zerolog" ) +const TIMEOUT = 5 * time.Second + func FetchFileFromURL(imageURL, outputPath string, logger *zerolog.Logger) (*http.Response, error) { if !strings.HasPrefix(imageURL, "http://") && !strings.HasPrefix(imageURL, "https://") { imageURL = fmt.Sprintf("https://%s", imageURL) } - resp, err := http.Get(imageURL) //nolint + client := &http.Client{ + Timeout: TIMEOUT, + } + + resp, err := client.Get(imageURL) //nolint + if err != nil { return nil, err } + defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { @@ -30,7 +39,6 @@ func FetchFileFromURL(imageURL, outputPath string, logger *zerolog.Logger) (*htt return nil, fmt.Errorf("failed to fetch file: %s", resp.Status) } - // Create the output file outFile, err := os.Create(outputPath) if err != nil { return nil, err diff --git a/internal/lrucache/cache.go b/internal/lrucache/cache.go index 73faec7..9b33f0f 100644 --- a/internal/lrucache/cache.go +++ b/internal/lrucache/cache.go @@ -93,6 +93,12 @@ func (lruCache *lruCache) Clear() { lruCache.queue = new(list) lruCache.items = make(map[Key]*cacheItem, lruCache.capacity) + + err := os.RemoveAll(lruCache.upload) + + if err != nil { + lruCache.logger.Error().Err(err) + } } func NewCache(capacity int, upload string, logger zerolog.Logger) Cache { diff --git a/internal/server/http/server.go b/internal/server/http/server.go index da06f60..fc473ef 100644 --- a/internal/server/http/server.go +++ b/internal/server/http/server.go @@ -5,18 +5,17 @@ import ( "errors" "fmt" "net/http" - "strconv" "strings" "time" "github.com/DEMAxx/project_work/internal/filemodifier" - "github.com/DEMAxx/project_work/internal/filesearch" - lrucache "github.com/DEMAxx/project_work/internal/lrucache" + "github.com/DEMAxx/project_work/internal/lrucache" "github.com/DEMAxx/project_work/pkg/config" - "github.com/google/uuid" "github.com/rs/zerolog" ) +const TIMEOUT = 5 * time.Second + type Server struct { httpServer *http.Server logger *zerolog.Logger @@ -55,31 +54,28 @@ func NewServer( mux.Handle("/fill/", LoggingMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path[len("/fill/"):] - parts := strings.Split(path, "/") - if len(parts) < 3 { - http.Error(w, "Invalid URL format", 400) + if path == "" { + http.Error(w, "Missing URL parameter", http.StatusBadRequest) return } - height, width, imageURL := parts[0], parts[1], strings.Join(parts[2:], "/") - - logger.Info().Msg( - fmt.Sprintf( - "Extracted vars - height: %s, width: %s, image url: %s", height, width, imageURL, - ), + modifier, err := filemodifier.New( + strings.Split(path, "/"), + logger, + cnf, + cache, ) - if !strings.HasSuffix(imageURL, ".jpg") { - http.Error(w, "Invalid image URL format. Only .jpg files are supported.", http.StatusBadRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) return } - cacheKey := lrucache.Key(fmt.Sprintf("%s_%s_%s", width, height, imageURL)) - cachedImage, found := cache.Get(cacheKey) + cachedImage, found := modifier.GetFromCache() if found { - logger.Info().Msg(fmt.Sprintf("Image retrieved from cache: %s", cacheKey)) + logger.Info().Msg(fmt.Sprintf("Image retrieved from cache")) w.Header().Set("Content-Type", "image/jpeg") w.WriteHeader(http.StatusOK) @@ -90,47 +86,16 @@ func NewServer( return } - uid := uuid.New() - fetchedFilePath := fmt.Sprintf("%s/%s_%s_%s.jpg", cnf.UploadPath, width, height, uid) - - resp, err := filesearch.FetchFileFromURL(imageURL, fetchedFilePath, logger) //nolint - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if resp.StatusCode != http.StatusOK { - http.Error(w, "Failed to fetch image.", http.StatusInternalServerError) - return - } + resizedImage, err := modifier.ResizeImage() - // Resize the image - widthInt, err := strconv.Atoi(width) - if err != nil { - http.Error(w, "Invalid width value", http.StatusBadRequest) - return - } - - heightInt, err := strconv.Atoi(height) - if err != nil { - http.Error(w, "Invalid height value", http.StatusBadRequest) - return - } - - ResizedImage, err := filemodifier.ResizeImage(fetchedFilePath, widthInt, heightInt) if err != nil { http.Error(w, "Failed to modify image.", http.StatusInternalServerError) return } - if err := cache.Set(cacheKey, ResizedImage); err { - http.Error(w, "Failed to store image in cache.", http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "image/jpeg") w.WriteHeader(http.StatusOK) - _, err = w.Write(ResizedImage) + _, err = w.Write(resizedImage) if err != nil { logger.Error().Msg("Failed to write response body") return @@ -141,7 +106,7 @@ func NewServer( httpServer: &http.Server{ Addr: hostAndPort, Handler: mux, - ReadHeaderTimeout: 5 * time.Second, + ReadHeaderTimeout: TIMEOUT, }, logger: logger, cache: cache, diff --git a/internal/server/http/server_test.go b/internal/server/http/server_test.go new file mode 100644 index 0000000..6549886 --- /dev/null +++ b/internal/server/http/server_test.go @@ -0,0 +1,37 @@ +package internalhttp + +import ( + "github.com/DEMAxx/project_work/internal/lrucache" + "github.com/DEMAxx/project_work/pkg/config" + "github.com/DEMAxx/project_work/pkg/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net/http" + "net/http/httptest" + "testing" +) + +const testImagesDir = "testdata" + +func TestServer(t *testing.T) { + logs := logger.MustSetupLogger(config.AppName, "test", true, "INFO") + cnf := config.Config{ + Capability: 10, + UploadPath: testImagesDir, + } + + cache := lrucache.NewCache(cnf.Capability, cnf.UploadPath, logs) + server := NewServer(&logs, "localhost:8080", cache, &cnf) + + t.Run("hello", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/hello", nil) + require.NoError(t, err) + + rec := httptest.NewRecorder() + + server.httpServer.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "Hello, World!", rec.Body.String()) + }) +} From 39e86fc84ad7da4c4d3fbb96697e7462223cfc7e Mon Sep 17 00:00:00 2001 From: demin Date: Wed, 28 May 2025 20:38:48 +0300 Subject: [PATCH 20/23] fix: server tests --- internal/filesearch/filesearch.go | 10 ++++++++-- internal/server/http/server.go | 2 +- internal/server/http/server_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/internal/filesearch/filesearch.go b/internal/filesearch/filesearch.go index f4f9dd6..6a10a9a 100644 --- a/internal/filesearch/filesearch.go +++ b/internal/filesearch/filesearch.go @@ -14,10 +14,16 @@ import ( const TIMEOUT = 5 * time.Second func FetchFileFromURL(imageURL, outputPath string, logger *zerolog.Logger) (*http.Response, error) { - if !strings.HasPrefix(imageURL, "http://") && !strings.HasPrefix(imageURL, "https://") { - imageURL = fmt.Sprintf("https://%s", imageURL) + if strings.HasPrefix(imageURL, "http:/") { + imageURL = strings.Trim(strings.Replace(imageURL, "http:/", "", 1), "/") } + if strings.HasPrefix(imageURL, "https:/") { + imageURL = strings.Trim(strings.Replace(imageURL, "https:/", "", 1), "/") + } + + imageURL = fmt.Sprintf("https://%s", imageURL) + client := &http.Client{ Timeout: TIMEOUT, } diff --git a/internal/server/http/server.go b/internal/server/http/server.go index fc473ef..6c27735 100644 --- a/internal/server/http/server.go +++ b/internal/server/http/server.go @@ -89,7 +89,7 @@ func NewServer( resizedImage, err := modifier.ResizeImage() if err != nil { - http.Error(w, "Failed to modify image.", http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("Failed to modify image: %s", err), http.StatusInternalServerError) return } diff --git a/internal/server/http/server_test.go b/internal/server/http/server_test.go index 6549886..32493b2 100644 --- a/internal/server/http/server_test.go +++ b/internal/server/http/server_test.go @@ -1,6 +1,7 @@ package internalhttp import ( + "fmt" "github.com/DEMAxx/project_work/internal/lrucache" "github.com/DEMAxx/project_work/pkg/config" "github.com/DEMAxx/project_work/pkg/logger" @@ -8,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "net/http" "net/http/httptest" + "os" "testing" ) @@ -34,4 +36,31 @@ func TestServer(t *testing.T) { assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "Hello, World!", rec.Body.String()) }) + + t.Run("fill success", func(t *testing.T) { + err := os.MkdirAll(testImagesDir, 0755) + + require.NoError(t, err) + + fileUrl := "raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg" + path := fmt.Sprintf( + "/fill/%d/%d/%s", + 100, + 100, + fileUrl, + ) + + req, err := http.NewRequest(http.MethodGet, path, nil) + require.NoError(t, err) + + rec := httptest.NewRecorder() + + server.httpServer.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "image/jpeg", rec.Header().Get("Content-Type")) + + err = os.RemoveAll(testImagesDir) + require.NoError(t, err) + }) } From 534379e36413b382085eea407c05eb83dd09debb Mon Sep 17 00:00:00 2001 From: demin Date: Thu, 29 May 2025 20:47:30 +0300 Subject: [PATCH 21/23] fix: linters --- internal/filemodifier/filemodifier.go | 9 +++--- internal/filemodifier/filemodifier_test.go | 29 ++++++++++++------ .../filemodifier/testdata/valid_image.jpg | Bin 45614 -> 0 bytes internal/filesearch/filesearch.go | 1 - internal/lrucache/cache.go | 13 ++++++-- internal/server/http/server.go | 4 +-- internal/server/http/server_test.go | 21 +++++++------ 7 files changed, 47 insertions(+), 30 deletions(-) delete mode 100644 internal/filemodifier/testdata/valid_image.jpg diff --git a/internal/filemodifier/filemodifier.go b/internal/filemodifier/filemodifier.go index 0912747..96e3bcd 100644 --- a/internal/filemodifier/filemodifier.go +++ b/internal/filemodifier/filemodifier.go @@ -3,15 +3,15 @@ package filemodifier import ( "errors" "fmt" - "github.com/DEMAxx/project_work/internal/filesearch" - "github.com/DEMAxx/project_work/internal/lrucache" - "github.com/DEMAxx/project_work/pkg/config" - "github.com/rs/zerolog" "net/http" "strconv" "strings" + "github.com/DEMAxx/project_work/internal/filesearch" + "github.com/DEMAxx/project_work/internal/lrucache" + "github.com/DEMAxx/project_work/pkg/config" "github.com/h2non/bimg" + "github.com/rs/zerolog" ) type Modifier interface { @@ -39,7 +39,6 @@ func (fileModifier *fileModifier) ResizeImage() ([]byte, error) { ) resp, err := filesearch.FetchFileFromURL(fileModifier.imageURL, fetchedFilePath, fileModifier.logger) //nolint - if err != nil { return nil, err } diff --git a/internal/filemodifier/filemodifier_test.go b/internal/filemodifier/filemodifier_test.go index 80127e1..7325937 100644 --- a/internal/filemodifier/filemodifier_test.go +++ b/internal/filemodifier/filemodifier_test.go @@ -2,12 +2,12 @@ package filemodifier import ( "fmt" - "github.com/DEMAxx/project_work/internal/lrucache" - "github.com/DEMAxx/project_work/pkg/config" - "github.com/DEMAxx/project_work/pkg/logger" "strings" "testing" + "github.com/DEMAxx/project_work/internal/lrucache" + "github.com/DEMAxx/project_work/pkg/config" + "github.com/DEMAxx/project_work/pkg/logger" "github.com/stretchr/testify/assert" ) @@ -15,7 +15,7 @@ import ( const testImagesDir = "testdata" func TestResizeImage(t *testing.T) { - fileUrl := "https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg" + fileURL := "raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg" //nolint log := logger.MustSetupLogger("previewer", "Test", true, "info") cnf := config.Config{} cnf.UploadPath = testImagesDir @@ -27,7 +27,7 @@ func TestResizeImage(t *testing.T) { "%d/%d/%s", 100, 100, - fileUrl, + fileURL, ) modifier, err := New( @@ -57,7 +57,7 @@ func TestResizeImage(t *testing.T) { "%d/%d/%s", 200, 200, - fileUrl, + fileURL, ) modifier, err := New( @@ -85,7 +85,7 @@ func TestResizeImage(t *testing.T) { "%d/%d/%s", 200, 200, - fileUrl, + fileURL, ) modifier, err = New( @@ -115,7 +115,7 @@ func TestResizeImage(t *testing.T) { "%d/%d/%s", 200, 200, - fileUrl, + fileURL, ) modifier, err := New( @@ -151,13 +151,22 @@ func TestResizeImage(t *testing.T) { assert.True(t, found) assert.NotNil(t, cachedImage) }) +} + +func TestFailResizeImage(t *testing.T) { + fileURL := "raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg" //nolint + log := logger.MustSetupLogger("previewer", "Test", true, "info") + cnf := config.Config{} + cnf.UploadPath = testImagesDir + cnf.Capability = 1 + cache := lrucache.NewCache(cnf.Capability, cnf.UploadPath, log) t.Run("zero dimensions", func(t *testing.T) { path := fmt.Sprintf( "%d/%d/%s", 0, 0, - fileUrl, + fileURL, ) _, err := New( @@ -193,7 +202,7 @@ func TestResizeImage(t *testing.T) { "%d/%d/%s", -100, -100, - fileUrl, + fileURL, ) _, err := New( diff --git a/internal/filemodifier/testdata/valid_image.jpg b/internal/filemodifier/testdata/valid_image.jpg deleted file mode 100644 index 2750e5487b9873f04521cc5251c73c1327e5af66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45614 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

BWUyuA|86a&%VDlKEc-Z@<78#PeJh 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

XRTdG|Fwu|5UZT=8Out9GEh3JvB6Q$+cHaXm?fC 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 Date: Thu, 29 May 2025 20:58:30 +0300 Subject: [PATCH 22/23] fix: main file --- cmd/previewer/main.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/previewer/main.go b/cmd/previewer/main.go index d98ea73..9c4404b 100644 --- a/cmd/previewer/main.go +++ b/cmd/previewer/main.go @@ -10,12 +10,14 @@ import ( "syscall" "time" - lrucache "github.com/DEMAxx/project_work/internal/lrucache" + "github.com/DEMAxx/project_work/internal/lrucache" internalhttp "github.com/DEMAxx/project_work/internal/server/http" "github.com/DEMAxx/project_work/pkg/config" "github.com/DEMAxx/project_work/pkg/logger" ) +const timeout = time.Second * 3 + var configFile string func init() { @@ -60,14 +62,12 @@ func main() { logs.Info().Msg("calendar is running...") - go func() { - <-ctx.Done() + <-ctx.Done() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) - defer cancel() + ctx, cancel = context.WithTimeout(context.Background(), timeout) + defer cancel() - if err := server.Stop(ctx); err != nil { - logs.Error().Msg(fmt.Sprintf("failed to stop http server: %s", err.Error())) - } - }() + if err := server.Stop(ctx); err != nil { + logs.Error().Msg(fmt.Sprintf("failed to stop http server: %s", err.Error())) + } } From 46d9731256eedc881e3d39f904d327b49ac4a337 Mon Sep 17 00:00:00 2001 From: demin Date: Sat, 31 May 2025 10:20:01 +0300 Subject: [PATCH 23/23] feat: proxy to file modifier --- cmd/previewer/main.go | 15 +++-- internal/filemodifier/filemodifier.go | 18 +++++- internal/filemodifier/filemodifier_test.go | 75 +++++++++++----------- internal/filesearch/filesearch.go | 46 +++++++++++-- internal/filesearch/filesearch_test.go | 24 +++++-- internal/server/http/server.go | 15 +++-- internal/server/http/server_test.go | 2 +- 7 files changed, 132 insertions(+), 63 deletions(-) diff --git a/cmd/previewer/main.go b/cmd/previewer/main.go index 9c4404b..fb7a7d3 100644 --- a/cmd/previewer/main.go +++ b/cmd/previewer/main.go @@ -43,18 +43,19 @@ func main() { cache := lrucache.NewCache(cnf.Capability, cnf.UploadPath, logs) + ctx, cancel = signal.NotifyContext(ctx, + syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + defer cancel() + server := internalhttp.NewServer( + ctx, &logs, net.JoinHostPort(cnf.Server.Host, cnf.Server.Port), cache, cnf, ) - ctx, cancel = signal.NotifyContext(ctx, - syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - defer cancel() - - if err := server.Start(ctx); err != nil { + if err := server.Start(); err != nil { logs.Error().Msg(fmt.Sprintf("failed to start http server: %s", err.Error())) cancel() os.Exit(1) //nolint:gocritic @@ -64,10 +65,10 @@ func main() { <-ctx.Done() - ctx, cancel = context.WithTimeout(context.Background(), timeout) + _, cancel = context.WithTimeout(context.Background(), timeout) defer cancel() - if err := server.Stop(ctx); err != nil { + if err := server.Stop(); err != nil { logs.Error().Msg(fmt.Sprintf("failed to stop http server: %s", err.Error())) } } diff --git a/internal/filemodifier/filemodifier.go b/internal/filemodifier/filemodifier.go index 96e3bcd..2f7f0bf 100644 --- a/internal/filemodifier/filemodifier.go +++ b/internal/filemodifier/filemodifier.go @@ -1,6 +1,7 @@ package filemodifier import ( + "context" "errors" "fmt" "net/http" @@ -28,6 +29,8 @@ type fileModifier struct { cacheKey lrucache.Key cache lrucache.Cache logger *zerolog.Logger + ctx context.Context + r *http.Request } func (fileModifier *fileModifier) ResizeImage() ([]byte, error) { @@ -38,7 +41,9 @@ func (fileModifier *fileModifier) ResizeImage() ([]byte, error) { fileModifier.height, ) - resp, err := filesearch.FetchFileFromURL(fileModifier.imageURL, fetchedFilePath, fileModifier.logger) //nolint + client := filesearch.NewClient(fileModifier.ctx, fileModifier.r) + + resp, err := client.FetchFileFromURL(fileModifier.imageURL, fetchedFilePath, fileModifier.logger) //nolint if err != nil { return nil, err } @@ -82,7 +87,14 @@ func (fileModifier *fileModifier) GetFromCache() (cachedImage interface{}, found return fileModifier.cache.Get(cacheKey) } -func New(parts []string, logger *zerolog.Logger, cnf *config.Config, cache lrucache.Cache) (Modifier, error) { +func New( + ctx context.Context, + parts []string, + logger *zerolog.Logger, + cnf *config.Config, + cache lrucache.Cache, + r *http.Request, +) (Modifier, error) { if len(parts) < 3 { return nil, errors.New("not enough parts") } @@ -134,5 +146,7 @@ func New(parts []string, logger *zerolog.Logger, cnf *config.Config, cache lruca cacheKey: cacheKey, cache: cache, logger: logger, + ctx: ctx, + r: r, }, nil } diff --git a/internal/filemodifier/filemodifier_test.go b/internal/filemodifier/filemodifier_test.go index 7325937..79e668b 100644 --- a/internal/filemodifier/filemodifier_test.go +++ b/internal/filemodifier/filemodifier_test.go @@ -1,7 +1,10 @@ package filemodifier import ( + "context" "fmt" + "net/http" + "net/url" "strings" "testing" @@ -14,28 +17,29 @@ import ( // Путь к директории с тестовыми изображениями. const testImagesDir = "testdata" +var cnf = config.Config{ + UploadPath: testImagesDir, + Capability: 1, +} + func TestResizeImage(t *testing.T) { fileURL := "raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg" //nolint log := logger.MustSetupLogger("previewer", "Test", true, "info") - cnf := config.Config{} - cnf.UploadPath = testImagesDir - cnf.Capability = 1 cache := lrucache.NewCache(cnf.Capability, cnf.UploadPath, log) + ctx := context.Background() + r := &http.Request{ + Method: "GET", + URL: &url.URL{ + Scheme: "http", + Host: "localhost", + Path: cnf.UploadPath, + }, + } t.Run("success", func(t *testing.T) { - path := fmt.Sprintf( - "%d/%d/%s", - 100, - 100, - fileURL, - ) + path := fmt.Sprintf("%d/%d/%s", 100, 100, fileURL) - modifier, err := New( - strings.Split(path, "/"), - &log, - &cnf, - cache, - ) + modifier, err := New(ctx, strings.Split(path, "/"), &log, &cnf, cache, r) assert.NoError(t, err) @@ -60,12 +64,7 @@ func TestResizeImage(t *testing.T) { fileURL, ) - modifier, err := New( - strings.Split(path, "/"), - &log, - &cnf, - cache, - ) + modifier, err := New(ctx, strings.Split(path, "/"), &log, &cnf, cache, r) assert.NoError(t, err) @@ -89,10 +88,12 @@ func TestResizeImage(t *testing.T) { ) modifier, err = New( + ctx, strings.Split(path, "/"), &log, &cnf, cache, + r, ) assert.NoError(t, err) @@ -118,12 +119,7 @@ func TestResizeImage(t *testing.T) { fileURL, ) - modifier, err := New( - strings.Split(path, "/"), - &log, - &cnf, - cache, - ) + modifier, err := New(ctx, strings.Split(path, "/"), &log, &cnf, cache, r) assert.NoError(t, err) @@ -137,12 +133,7 @@ func TestResizeImage(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, resizedImage) - modifier, err = New( - strings.Split(path, "/"), - &log, - &cnf, - cache, - ) + modifier, err = New(ctx, strings.Split(path, "/"), &log, &cnf, cache, r) assert.NoError(t, err) @@ -156,10 +147,16 @@ func TestResizeImage(t *testing.T) { func TestFailResizeImage(t *testing.T) { fileURL := "raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg" //nolint log := logger.MustSetupLogger("previewer", "Test", true, "info") - cnf := config.Config{} - cnf.UploadPath = testImagesDir - cnf.Capability = 1 cache := lrucache.NewCache(cnf.Capability, cnf.UploadPath, log) + ctx := context.Background() + r := &http.Request{ + Method: "GET", + URL: &url.URL{ + Scheme: "http", + Host: "localhost", + Path: cnf.UploadPath, + }, + } t.Run("zero dimensions", func(t *testing.T) { path := fmt.Sprintf( @@ -170,10 +167,12 @@ func TestFailResizeImage(t *testing.T) { ) _, err := New( + ctx, strings.Split(path, "/"), &log, &cnf, cache, + r, ) assert.Error(t, err) @@ -188,10 +187,12 @@ func TestFailResizeImage(t *testing.T) { ) _, err := New( + ctx, strings.Split(path, "/"), &log, &cnf, cache, + r, ) assert.Error(t, err) @@ -206,10 +207,12 @@ func TestFailResizeImage(t *testing.T) { ) _, err := New( + ctx, strings.Split(path, "/"), &log, &cnf, cache, + r, ) assert.Error(t, err) diff --git a/internal/filesearch/filesearch.go b/internal/filesearch/filesearch.go index 8514b58..0816500 100644 --- a/internal/filesearch/filesearch.go +++ b/internal/filesearch/filesearch.go @@ -1,19 +1,51 @@ package filesearch import ( + "context" "fmt" "io" + "log/slog" "net/http" "os" "strings" - "time" "github.com/rs/zerolog" ) -const TIMEOUT = 5 * time.Second +func NewClient(ctx context.Context, r *http.Request) *Client { + return &Client{ + ctx: ctx, + r: r, + } +} + +type Client struct { + ctx context.Context + r *http.Request +} + +func (p *Client) newHTTPRequest(url string) *http.Request { + prxReq, _ := http.NewRequestWithContext(p.ctx, p.r.Method, url, p.r.Body) + prxQuery := prxReq.URL.Query() + + for key, values := range p.r.URL.Query() { + for _, value := range values { + prxQuery.Add(key, value) + } + } -func FetchFileFromURL(imageURL, outputPath string, logger *zerolog.Logger) (*http.Response, error) { + for key, values := range p.r.Header { + for _, value := range values { + prxReq.Header.Set(key, value) + } + } + + prxReq.URL.RawQuery = prxQuery.Encode() + + return prxReq +} + +func (p *Client) FetchFileFromURL(imageURL, outputPath string, logger *zerolog.Logger) (*http.Response, error) { if strings.HasPrefix(imageURL, "http:/") { imageURL = strings.Trim(strings.Replace(imageURL, "http:/", "", 1), "/") } @@ -24,11 +56,11 @@ func FetchFileFromURL(imageURL, outputPath string, logger *zerolog.Logger) (*htt imageURL = fmt.Sprintf("https://%s", imageURL) - client := &http.Client{ - Timeout: TIMEOUT, - } + req := p.newHTTPRequest(imageURL) + + slog.Debug(fmt.Sprintf("Proxy: IN='%s %s' -> OUT='%s %s'", p.r.Method, p.r.URL.String(), req.Method, req.URL.String())) //nolint - resp, err := client.Get(imageURL) //nolint + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } diff --git a/internal/filesearch/filesearch_test.go b/internal/filesearch/filesearch_test.go index ffe2b1e..6e2ecee 100644 --- a/internal/filesearch/filesearch_test.go +++ b/internal/filesearch/filesearch_test.go @@ -1,7 +1,9 @@ package filesearch import ( + "context" "net/http" + "net/url" "os" "path/filepath" "testing" @@ -11,12 +13,24 @@ import ( ) func TestFileSearch(t *testing.T) { - tmpDir := os.TempDir() - outputPath := filepath.Join(tmpDir, "output") + outputPath := filepath.Join(os.TempDir(), "output") logs := logger.MustSetupLogger("previewer", "Test", true, "info") + ctx := context.Background() + + client := NewClient( + ctx, + &http.Request{ + Method: "GET", + URL: &url.URL{ + Scheme: "http", + Host: "localhost", + Path: outputPath, + }, + }, + ) t.Run("success", func(t *testing.T) { - r, err := FetchFileFromURL( + r, err := client.FetchFileFromURL( "https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/_gopher_original_1024x504.jpg", //nolint outputPath, &logs, @@ -30,7 +44,7 @@ func TestFileSearch(t *testing.T) { }) t.Run("wrong address", func(t *testing.T) { - r, err := FetchFileFromURL( + r, err := client.FetchFileFromURL( "https://raw.githubusercontent.com/OtusGolang/final_project/master/examples/image-previewer/not_gopher_original.jpg", outputPath, &logs, @@ -45,7 +59,7 @@ func TestFileSearch(t *testing.T) { }) t.Run("not found", func(t *testing.T) { - r, err := FetchFileFromURL("localhost:9999/image.png", outputPath, &logs) + r, err := client.FetchFileFromURL("localhost:9999/image.png", outputPath, &logs) require.Error(t, err) require.ErrorContains(t, err, "connection refused") diff --git a/internal/server/http/server.go b/internal/server/http/server.go index fea8c1b..4dad324 100644 --- a/internal/server/http/server.go +++ b/internal/server/http/server.go @@ -17,12 +17,14 @@ import ( const TIMEOUT = 5 * time.Second type Server struct { + ctx context.Context httpServer *http.Server logger *zerolog.Logger cache lrucache.Cache } func NewServer( + ctx context.Context, logger *zerolog.Logger, hostAndPort string, cache lrucache.Cache, @@ -61,10 +63,12 @@ func NewServer( } modifier, err := filemodifier.New( + ctx, strings.Split(path, "/"), logger, cnf, cache, + r, ) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -101,6 +105,7 @@ func NewServer( }), logger)) return &Server{ + ctx: ctx, httpServer: &http.Server{ Addr: hostAndPort, Handler: mux, @@ -111,7 +116,7 @@ func NewServer( } } -func (s *Server) Start(ctx context.Context) error { +func (s *Server) Start() error { s.logger.Info().Msg(fmt.Sprintf("Starting HTTP server on %s...", s.httpServer.Addr)) // Start HTTP server @@ -123,15 +128,15 @@ func (s *Server) Start(ctx context.Context) error { } }() - <-ctx.Done() - return s.Stop(ctx) + <-s.ctx.Done() + return s.Stop() } -func (s *Server) Stop(ctx context.Context) error { +func (s *Server) Stop() error { s.logger.Info().Msg("Stopping HTTP server...") // Stop HTTP server - shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + shutdownCtx, cancel := context.WithTimeout(s.ctx, 5*time.Second) defer cancel() if err := s.httpServer.Shutdown(shutdownCtx); err != nil { diff --git a/internal/server/http/server_test.go b/internal/server/http/server_test.go index 5fa7e83..51c9797 100644 --- a/internal/server/http/server_test.go +++ b/internal/server/http/server_test.go @@ -26,7 +26,7 @@ func TestServer(t *testing.T) { } cache := lrucache.NewCache(cnf.Capability, cnf.UploadPath, logs) - server := NewServer(&logs, "localhost:8080", cache, &cnf) + server := NewServer(ctx, &logs, "localhost:8080", cache, &cnf) t.Run("hello", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/hello", nil)