refactor: migrate the project to golang

This commit is contained in:
Muhammed Efe Cetin
2025-03-15 15:03:34 +03:00
committed by M. Efe Çetin
parent cf112b8eb7
commit b283b1568f
25 changed files with 2270 additions and 0 deletions

132
cmd/main.go Normal file
View File

@@ -0,0 +1,132 @@
package main
import (
"flag"
"fmt"
"io"
"net/http"
"os"
"strconv"
"github.com/armbian/ansi-hastebin/config"
"github.com/armbian/ansi-hastebin/handler"
"github.com/armbian/ansi-hastebin/keygenerator"
"github.com/armbian/ansi-hastebin/storage"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
)
func main() {
// Creater router instance
r := chi.NewRouter()
// Add several middlewares
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// Check if config argument sent
var configLocation string
flag.StringVar(&configLocation, "config", "", "Pass config yaml")
flag.Parse()
// Parse config fields
cfg := config.NewConfig(configLocation)
var pasteStorage storage.Storage
switch cfg.Storage.Type {
case "file":
pasteStorage = storage.NewFileStorage(cfg.Storage.FilePath, cfg.Expiration)
case "redis":
pasteStorage = storage.NewRedisStorage(cfg.Storage.Host, cfg.Storage.Port, cfg.Storage.Username, cfg.Storage.Password, cfg.Expiration)
case "memcached":
pasteStorage = storage.NewMemcachedStorage(cfg.Storage.Host, cfg.Storage.Port, int(cfg.Expiration))
case "mongodb":
pasteStorage = storage.NewMongoDBStorage(cfg.Storage.Host, cfg.Storage.Port, cfg.Storage.Username, cfg.Storage.Password, cfg.Storage.Database, cfg.Expiration)
case "postgres":
pasteStorage = storage.NewPostgresStorage(cfg.Storage.Host, cfg.Storage.Port, cfg.Storage.Username, cfg.Storage.Password, cfg.Storage.Database, int(cfg.Expiration))
case "s3":
pasteStorage = storage.NewS3Storage(cfg.Storage.Host, cfg.Storage.Port, cfg.Storage.Username, cfg.Storage.Password, cfg.Storage.AWSRegion, cfg.Storage.Bucket)
default:
logrus.Fatalf("Unknown storage type: %s", cfg.Storage.Type)
return
}
// Set static documents from config
for _, doc := range cfg.Documents {
file, err := os.OpenFile(doc.Path, os.O_RDONLY, 0644)
if err != nil {
logrus.WithError(err).WithField("path", doc.Path).Fatal("Failed to open document")
}
content, err := io.ReadAll(file)
if err != nil {
logrus.WithError(err).WithField("path", doc.Path).Fatal("Failed to read document")
}
file.Close()
pasteStorage.Set(doc.Key, string(content), false)
}
var keyGenerator keygenerator.KeyGenerator
switch cfg.KeyGenerator {
case "random":
keyGenerator = keygenerator.NewRandomKeyGenerator(cfg.KeySpace)
case "phonetic":
keyGenerator = keygenerator.NewPhoneticKeyGenerator()
default:
logrus.Fatalf("Unknown key generator: %s", cfg.KeyGenerator)
return
}
// Add document handler
document_handler := handler.NewDocumentHandler(cfg.KeyLength, cfg.MaxLength, pasteStorage, keyGenerator)
// Add prometheus metrics
r.Get("/metrics", promhttp.Handler().ServeHTTP)
// Add document routes
r.Get("/raw/{id}", document_handler.HandleRawGet)
r.Head("/raw/{id}", document_handler.HandleRawGet)
r.Post("/log", document_handler.HandlePutLog)
r.Put("/log", document_handler.HandlePutLog)
r.Post("/documents", document_handler.HandlePost)
r.Get("/documents/{id}", document_handler.HandleGet)
r.Head("/documents/{id}", document_handler.HandleGet)
static := os.DirFS("static")
r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if file, err := static.Open(id); err == nil {
defer file.Close()
io.Copy(w, file)
return
}
index, err := static.Open("index.html")
if err != nil {
http.Error(w, "Not found", http.StatusNotFound)
return
}
defer index.Close()
io.Copy(w, index)
})
fileServer := http.StripPrefix("/", http.FileServer(http.FS(static)))
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
fileServer.ServeHTTP(w, r)
})
if err := http.ListenAndServe(cfg.Host+":"+strconv.Itoa(cfg.Port), r); err != nil {
fmt.Printf("Failed to start server: %v\n", err)
return
}
}

21
config.yaml Normal file
View File

@@ -0,0 +1,21 @@
host: "0.0.0.0"
port: 7777
key_length: 10
max_length: 4000000
static_max_age: 3
expiration: 0
recompress_static_assets: false
key_generator: "phonetic"
storage:
type: "file"
file_path: "./test"
documents:
- key: "about.md"
path: "./about.md"
logging:
level: "info"
type: "text"
colorize: true

301
config/config.go Normal file
View File

@@ -0,0 +1,301 @@
package config
import (
"os"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
type LoggingConfig struct {
Level string `yaml:"level"`
Type string `yaml:"type"`
Colorize bool `yaml:"colorize"`
}
type StorageConfig struct {
// Type is the storage backend to use
// Available storage backends are: "redis", "file", "memcached", "mongodb", "s3", "postgres"
Type string `yaml:"type"`
// Host is the hostname or IP address of the storage backend
Host string `yaml:"host"`
// Port is the port of the storage backend
Port int `yaml:"port"`
// Username is the username to use for the storage backend
Username string `yaml:"username"`
// Password is the password to use for the storage backend
Password string `yaml:"password"`
// Database is the database to use for the storage backend
Database string `yaml:"database"`
// Bucket is the bucket to use for the storage backend
Bucket string `yaml:"bucket"`
// AWSRegion is the AWS region to use for the storage backend
// This property is only used for the "s3" storage backend
AWSRegion string `yaml:"aws_region"`
// FilePath is the file path to use for the "file" storage backend
// This property is only used for the "file" storage backend
FilePath string `yaml:"file_path"`
}
type DocumentConfig struct {
// Key is the key of the document
Key string `yaml:"key"`
// Path is the path of the document which is going to be read
Path string `yaml:"path"`
}
type Config struct {
// Host is the hostname or IP address to bind to
Host string `yaml:"host"`
// Port is the port to bind to
Port int `yaml:"port"`
// KeyLength is the length of the key to generate which is used for storage key
KeyLength int `yaml:"key_length"`
// KeySpace is the key space to use for the key generator
// This property is only used for the "random" key generator
KeySpace string `yaml:"key_space"`
// MaxLength is the maximum length of the paste
MaxLength int `yaml:"max_length"`
// StaticMaxAge is the maximum age of static assets
StaticMaxAge int `yaml:"static_max_age"`
// Expiration is the maximum lifetime of paste entry
// 0 means there will be no expiration.
// "file" and "s3" storages don't support expiration control.
Expiration time.Duration `yaml:"expiration"`
// RecompressStaticAssets is a flag to recompress static assets by default
RecompressStaticAssets bool `yaml:"recompress_static_assets"`
// KeyGenerator is the key generator to use
// Available key generators are: "random", "phonetic"
KeyGenerator string `yaml:"key_generator"`
// Storage is the storage backend to use
// Available storage backends are: "redis", "file", "memcached", "mongodb", "s3", "postgres"
Storage StorageConfig `yaml:"storage"`
// Logging is the logging configuration
Logging LoggingConfig `yaml:"logging"`
// Documents is the list of documents to load statically
Documents []DocumentConfig `yaml:"documents"`
}
var DefaultConfig = &Config{
Host: "0.0.0.0",
Port: 7777,
KeyLength: 10,
MaxLength: 4000000,
StaticMaxAge: 3,
RecompressStaticAssets: false,
KeyGenerator: "phonetic",
Storage: StorageConfig{
Type: "file",
FilePath: "data",
},
Logging: LoggingConfig{
Level: "info",
Type: "text",
},
Documents: []DocumentConfig{
{
Key: "about",
Path: "about.md",
},
},
}
// NewConfig creates a new Config instance
func NewConfig(configFile string) *Config {
cfg := &Config{}
// Read the configuration file
data, err := os.ReadFile(configFile)
if err != nil && !os.IsNotExist(err) {
logrus.WithError(err).Fatal("Failed to read configuration file")
}
// Unmarshal the configuration file
if err := yaml.Unmarshal(data, cfg); err != nil {
logrus.WithError(err).Fatal("Failed to unmarshal configuration file")
}
// Override with environment variables
if host := os.Getenv("HOST"); host != "" {
cfg.Host = host
}
if port := os.Getenv("PORT"); port != "" {
portInt, err := strconv.Atoi(port)
if err != nil {
logrus.WithError(err).Fatal("Failed to parse PORT environment variable")
}
cfg.Port = portInt
}
if keyLength := os.Getenv("KEY_LENGTH"); keyLength != "" {
keyLengthInt, err := strconv.Atoi(keyLength)
if err != nil {
logrus.WithError(err).Fatal("Failed to parse KEY_LENGTH environment variable")
}
cfg.KeyLength = keyLengthInt
}
if maxLength := os.Getenv("MAX_LENGTH"); maxLength != "" {
maxLengthInt, err := strconv.Atoi(maxLength)
if err != nil {
logrus.WithError(err).Fatal("Failed to parse MAX_LENGTH environment variable")
}
cfg.MaxLength = maxLengthInt
}
if staticMaxAge := os.Getenv("STATIC_MAX_AGE"); staticMaxAge != "" {
staticMaxAgeInt, err := strconv.Atoi(staticMaxAge)
if err != nil {
logrus.WithError(err).Fatal("Failed to parse STATIC_MAX_AGE environment variable")
}
cfg.StaticMaxAge = staticMaxAgeInt
}
if recompressStaticAssets := os.Getenv("RECOMPRESS_STATIC_ASSETS"); recompressStaticAssets != "" {
recompressStaticAssetsBool, err := strconv.ParseBool(recompressStaticAssets)
if err != nil {
logrus.WithError(err).Fatal("Failed to parse RECOMPRESS_STATIC_ASSETS environment variable")
}
cfg.RecompressStaticAssets = recompressStaticAssetsBool
}
if keyGenerator := os.Getenv("KEY_GENERATOR"); keyGenerator != "" {
cfg.KeyGenerator = keyGenerator
}
if storageType := os.Getenv("STORAGE_TYPE"); storageType != "" {
cfg.Storage.Type = storageType
}
if storageHost := os.Getenv("STORAGE_HOST"); storageHost != "" {
cfg.Storage.Host = storageHost
}
if storagePort := os.Getenv("STORAGE_PORT"); storagePort != "" {
storagePortInt, err := strconv.Atoi(storagePort)
if err != nil {
logrus.WithError(err).Fatal("Failed to parse STORAGE_PORT environment variable")
}
cfg.Storage.Port = storagePortInt
}
if storageUsername := os.Getenv("STORAGE_USERNAME"); storageUsername != "" {
cfg.Storage.Username = storageUsername
}
if storagePassword := os.Getenv("STORAGE_PASSWORD"); storagePassword != "" {
cfg.Storage.Password = storagePassword
}
if storageDatabase := os.Getenv("STORAGE_DATABASE"); storageDatabase != "" {
cfg.Storage.Database = storageDatabase
}
if storageBucket := os.Getenv("STORAGE_BUCKET"); storageBucket != "" {
cfg.Storage.Bucket = storageBucket
}
if storageAWSRegion := os.Getenv("STORAGE_AWS_REGION"); storageAWSRegion != "" {
cfg.Storage.AWSRegion = storageAWSRegion
}
if storageFilePath := os.Getenv("STORAGE_FILE_PATH"); storageFilePath != "" {
cfg.Storage.FilePath = storageFilePath
}
if loggingLevel := os.Getenv("LOGGING_LEVEL"); loggingLevel != "" {
cfg.Logging.Level = loggingLevel
}
if loggingType := os.Getenv("LOGGING_TYPE"); loggingType != "" {
cfg.Logging.Type = loggingType
}
if loggingColorize := os.Getenv("LOGGING_COLORIZE"); loggingColorize != "" {
loggingColorizeBool, err := strconv.ParseBool(loggingColorize)
if err != nil {
logrus.WithError(err).Fatal("Failed to parse LOGGING_COLORIZE environment variable")
}
cfg.Logging.Colorize = loggingColorizeBool
}
// Walk environment variables for documents
for _, env := range os.Environ() {
if len(env) > 10 && env[:10] == "DOCUMENTS_" {
parts := strings.Split(env[10:], "=")
if len(parts) == 2 {
cfg.Documents = append(cfg.Documents, DocumentConfig{
Key: parts[0],
Path: parts[1],
})
}
}
}
// Apply default values to the configuration
if cfg.Host == "" {
cfg.Host = DefaultConfig.Host
}
if cfg.Port == 0 {
cfg.Port = DefaultConfig.Port
}
if cfg.KeyLength == 0 {
cfg.KeyLength = DefaultConfig.KeyLength
}
if cfg.MaxLength == 0 {
cfg.MaxLength = DefaultConfig.MaxLength
}
if cfg.StaticMaxAge == 0 {
cfg.StaticMaxAge = DefaultConfig.StaticMaxAge
}
if cfg.KeyGenerator == "" {
cfg.KeyGenerator = DefaultConfig.KeyGenerator
}
if cfg.Storage.Type == "" {
cfg.Storage.Type = DefaultConfig.Storage.Type
}
if cfg.Storage.FilePath == "" {
cfg.Storage.FilePath = DefaultConfig.Storage.FilePath
}
if cfg.Logging.Level == "" {
cfg.Logging.Level = DefaultConfig.Logging.Level
}
if cfg.Logging.Type == "" {
cfg.Logging.Type = DefaultConfig.Logging.Type
}
return cfg
}

80
config/config_test.go Normal file
View File

@@ -0,0 +1,80 @@
package config
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewConfig_DefaultValues(t *testing.T) {
cfg := NewConfig("nonexistent.yaml") // Should use defaults since file doesn't exist
assert.Equal(t, "0.0.0.0", cfg.Host)
assert.Equal(t, 7777, cfg.Port)
assert.Equal(t, 10, cfg.KeyLength)
assert.Equal(t, "phonetic", cfg.KeyGenerator)
assert.Equal(t, "file", cfg.Storage.Type)
assert.Equal(t, "data", cfg.Storage.FilePath)
assert.Equal(t, "info", cfg.Logging.Level)
assert.Equal(t, "text", cfg.Logging.Type)
}
func TestNewConfig_OverrideWithEnvVars(t *testing.T) {
os.Setenv("HOST", "127.0.0.1")
os.Setenv("PORT", "8080")
os.Setenv("KEY_LENGTH", "15")
os.Setenv("MAX_LENGTH", "5000000")
os.Setenv("STORAGE_TYPE", "redis")
os.Setenv("STORAGE_HOST", "localhost")
os.Setenv("STORAGE_PORT", "6379")
os.Setenv("LOGGING_LEVEL", "debug")
defer os.Clearenv()
cfg := NewConfig("nonexistent.yaml") // Load with environment variables
assert.Equal(t, "127.0.0.1", cfg.Host)
assert.Equal(t, 8080, cfg.Port)
assert.Equal(t, 15, cfg.KeyLength)
assert.Equal(t, 5000000, cfg.MaxLength)
assert.Equal(t, "redis", cfg.Storage.Type)
assert.Equal(t, "localhost", cfg.Storage.Host)
assert.Equal(t, 6379, cfg.Storage.Port)
assert.Equal(t, "debug", cfg.Logging.Level)
}
func TestNewConfig_LoadFromYAML(t *testing.T) {
yamlContent := `
host: "192.168.1.1"
port: 9090
key_length: 20
storage:
type: "mongodb"
host: "mongo.example.com"
port: 27017
logging:
level: "warn"
type: "json"
`
// Write to a temporary file
tmpFile, err := os.CreateTemp("", "config_test_*.yaml")
assert.NoError(t, err)
defer os.Remove(tmpFile.Name())
_, err = tmpFile.Write([]byte(yamlContent))
assert.NoError(t, err)
tmpFile.Close()
cfg := NewConfig(tmpFile.Name()) // Load config from YAML file
assert.Equal(t, "192.168.1.1", cfg.Host)
assert.Equal(t, 9090, cfg.Port)
assert.Equal(t, 20, cfg.KeyLength)
assert.Equal(t, "mongodb", cfg.Storage.Type)
assert.Equal(t, "mongo.example.com", cfg.Storage.Host)
assert.Equal(t, 27017, cfg.Storage.Port)
assert.Equal(t, "warn", cfg.Logging.Level)
assert.Equal(t, "json", cfg.Logging.Type)
}

102
go.mod Normal file
View File

@@ -0,0 +1,102 @@
module github.com/armbian/ansi-hastebin
go 1.24.0
require (
github.com/aws/aws-sdk-go-v2 v1.36.3
github.com/aws/aws-sdk-go-v2/config v1.29.9
github.com/aws/aws-sdk-go-v2/credentials v1.17.62
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874
github.com/go-chi/chi/v5 v5.2.1
github.com/jackc/pgx/v5 v5.7.2
github.com/prometheus/client_golang v1.21.1
github.com/redis/go-redis/v9 v9.7.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.35.0
github.com/testcontainers/testcontainers-go/modules/minio v0.35.0
go.mongodb.org/mongo-driver v1.17.3
go.mongodb.org/mongo-driver/v2 v2.1.0
gopkg.in/yaml.v3 v3.0.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/containerd v1.7.18 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/shirou/gopsutil/v3 v3.24.2 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
)

326
go.sum Normal file
View File

@@ -0,0 +1,326 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0=
github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U=
github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU=
github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66 h1:MTLivtC3s89de7Fe3P8rzML/8XPNRfuyJhlRTsCEt0k=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66/go.mod h1:NAuQ2s6gaFEsuTIb2+P5t6amB1w5MhvJFxppoezGWH0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI=
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.68 h1:hTqSIfLlpXaKuNy4baAp4Jjy2sqZEN9hRxD0M4aOfrQ=
github.com/minio/minio-go/v7 v7.0.68/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo=
github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4=
github.com/testcontainers/testcontainers-go/modules/minio v0.35.0 h1:oJMrfB0hIABClRsJrVJ43zTEsCVk0JTN7RdTz9r+tk4=
github.com/testcontainers/testcontainers-go/modules/minio v0.35.0/go.mod h1:Q7gSllC2zi78e2OF6Gwn+DXyqbxdbt6PAuaZdIPh3DQ=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.1.0 h1:/ELnVNjmfUKDsoBisXxuJL0noR9CfeUIrP7Yt3R+egg=
go.mongodb.org/mongo-driver/v2 v2.1.0/go.mod h1:AWiLRShSrk5RHQS3AEn3RL19rqOzVq49MCpWQ3x/huI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

133
handler/document.go Normal file
View File

@@ -0,0 +1,133 @@
package handler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/armbian/ansi-hastebin/keygenerator"
"github.com/armbian/ansi-hastebin/storage"
"github.com/go-chi/chi/v5"
"github.com/sirupsen/logrus"
)
// DocumentHandler manages document operations
type DocumentHandler struct {
KeyLength int
MaxLength int
Store storage.Storage
KeyGenerator keygenerator.KeyGenerator
}
func NewDocumentHandler(keyLength, maxLength int, store storage.Storage, keyGenerator keygenerator.KeyGenerator) *DocumentHandler {
keyLength = 10
return &DocumentHandler{
KeyLength: keyLength,
MaxLength: maxLength,
Store: store,
KeyGenerator: keyGenerator,
}
}
// Handle retrieving a document
func (h *DocumentHandler) HandleGet(w http.ResponseWriter, r *http.Request) {
key := strings.Split(chi.URLParam(r, "id"), ".")[0]
data, err := h.Store.Get(key, false)
if data != "" && err == nil {
logrus.WithField("key", key).Info("Retrieved document")
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodHead {
w.WriteHeader(http.StatusOK)
return
}
json.NewEncoder(w).Encode(map[string]string{"data": data, "key": key})
} else {
logrus.WithField("key", key).Warn("Document not found")
http.Error(w, `{"message": "Document not found."}`, http.StatusNotFound)
}
}
// Handle retrieving raw document
func (h *DocumentHandler) HandleRawGet(w http.ResponseWriter, r *http.Request) {
key := strings.Split(chi.URLParam(r, "id"), ".")[0]
data, err := h.Store.Get(key, false)
if data != "" && err == nil {
logrus.WithField("key", key).Info("Retrieved raw document")
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
if r.Method == http.MethodHead {
w.WriteHeader(http.StatusOK)
return
}
w.Write([]byte(data))
} else {
logrus.WithField("key", key).Warn("Raw document not found")
http.Error(w, `{"message": "Document not found."}`, http.StatusNotFound)
}
}
// Handle adding a new document (POST)
func (h *DocumentHandler) HandlePost(w http.ResponseWriter, r *http.Request) {
var buffer strings.Builder
if err := h.readBody(r, &buffer); err != nil {
http.Error(w, `{"message": "Error reading request body."}`, http.StatusInternalServerError)
return
}
if h.MaxLength > 0 && buffer.Len() > h.MaxLength {
logrus.Warn("Document exceeds max length")
http.Error(w, `{"message": "Document exceeds maximum length."}`, http.StatusBadRequest)
return
}
key := h.KeyGenerator.Generate(h.KeyLength)
fmt.Println(key)
h.Store.Set(key, buffer.String(), false)
logrus.WithField("key", key).Info("Added document")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"key": key})
}
// Handle PUT request that returns a direct link
func (h *DocumentHandler) HandlePutLog(w http.ResponseWriter, r *http.Request) {
var buffer strings.Builder
if err := h.readBody(r, &buffer); err != nil {
http.Error(w, `{"message": "Error reading request body."}`, http.StatusInternalServerError)
return
}
if h.MaxLength > 0 && buffer.Len() > h.MaxLength {
logrus.Warn("Document exceeds max length")
http.Error(w, `{"message": "Document exceeds maximum length."}`, http.StatusBadRequest)
return
}
key := h.KeyGenerator.Generate(h.KeyLength)
h.Store.Set(key, buffer.String(), false)
logrus.WithField("key", key).Info("Added document with log link")
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "\nhttps://%s/%s\n\n", r.Host, key)
}
// Reads body from the request
func (h *DocumentHandler) readBody(r *http.Request, buffer *strings.Builder) error {
if strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") {
r.ParseMultipartForm(32 << 20)
if val := r.FormValue("data"); val != "" {
buffer.WriteString(val)
}
} else {
data, err := io.ReadAll(r.Body)
if err != nil {
logrus.Error("Connection error: ", err)
return err
}
buffer.WriteString(string(data))
}
return nil
}

View File

@@ -0,0 +1,5 @@
package keygenerator
type KeyGenerator interface {
Generate(length int) string
}

45
keygenerator/phonetic.go Normal file
View File

@@ -0,0 +1,45 @@
package keygenerator
import (
"crypto/rand"
"math/big"
"strings"
)
type PhoneticKeyGenerator struct {
}
var vowels string = "aeiou"
var consonants string = "bcdfghjklmnpqrstvwxyz"
var twoBig = big.NewInt(2)
func randomFromStr(str string) byte {
maxRandomIndex := big.NewInt(int64(len(str)))
index, _ := rand.Int(rand.Reader, maxRandomIndex)
return str[index.Int64()]
}
func NewPhoneticKeyGenerator() *PhoneticKeyGenerator {
return &PhoneticKeyGenerator{}
}
func (p *PhoneticKeyGenerator) Generate(length int) string {
var out strings.Builder
defer out.Reset()
start, err := rand.Int(rand.Reader, twoBig)
if err != nil {
return ""
}
for i := 0; i < length; i++ {
if i%2 == int(start.Int64()) {
out.WriteByte(randomFromStr(consonants))
} else {
out.WriteByte(randomFromStr(vowels))
}
}
return out.String()
}

View File

@@ -0,0 +1,55 @@
package keygenerator
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestNewPhoneticKeyGenerator(t *testing.T) {
kg := NewPhoneticKeyGenerator()
require.NotNil(t, kg)
}
func TestPhoneticGenerate(t *testing.T) {
kg := NewPhoneticKeyGenerator()
t.Run("Generate with valid length", func(t *testing.T) {
key := kg.Generate(10)
fmt.Print(key)
require.Len(t, key, 10)
for _, ch := range key {
require.Contains(t, vowels+consonants, string(ch))
}
})
t.Run("Generate with zero length", func(t *testing.T) {
key := kg.Generate(0)
require.Empty(t, key)
})
t.Run("Generate with large length", func(t *testing.T) {
key := kg.Generate(1000)
require.Len(t, key, 1000)
})
t.Run("Check different keys", func(t *testing.T) {
var oldKey string
for i := 0; i < 200; i++ {
key := kg.Generate(50)
require.NotEqual(t, key, oldKey)
oldKey = key
}
})
}
func TestPhoneticFromStr(t *testing.T) {
t.Run("RandomFromStr", func(t *testing.T) {
str := "abc"
ch := randomFromStr(str)
require.Contains(t, str, string(ch))
})
}

37
keygenerator/random.go Normal file
View File

@@ -0,0 +1,37 @@
package keygenerator
import (
"crypto/rand"
"math/big"
"strings"
)
type RandomKeyGenerator struct {
keyspace string
}
func NewRandomKeyGenerator(keyspace string) *RandomKeyGenerator {
if keyspace == "" {
keyspace = "abcdefghijklmnopqrstuvwxyz"
}
return &RandomKeyGenerator{
keyspace: keyspace,
}
}
func (r *RandomKeyGenerator) Generate(length int) string {
var out strings.Builder
defer out.Reset()
maxRandomIndex := big.NewInt(int64(len(r.keyspace)))
for i := 0; i < length; i++ {
index, err := rand.Int(rand.Reader, maxRandomIndex)
if err != nil {
continue
}
out.WriteByte(r.keyspace[index.Int64()])
}
return out.String()
}

View File

@@ -0,0 +1,50 @@
package keygenerator
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestNewRandomKeyGenerator(t *testing.T) {
kg := NewRandomKeyGenerator("abc")
require.Equal(t, "abc", kg.keyspace)
defaultKg := NewRandomKeyGenerator("")
require.Equal(t, "abcdefghijklmnopqrstuvwxyz", defaultKg.keyspace)
}
func TestGenerate(t *testing.T) {
kg := NewRandomKeyGenerator("abc")
t.Run("Generate with valid length", func(t *testing.T) {
key := kg.Generate(10)
fmt.Print(key)
require.Len(t, key, 10)
for _, ch := range key {
require.Contains(t, "abc", string(ch))
}
})
t.Run("Generate with zero length", func(t *testing.T) {
key := kg.Generate(0)
require.Empty(t, key)
})
t.Run("Generate with large length", func(t *testing.T) {
key := kg.Generate(1000)
require.Len(t, key, 1000)
})
t.Run("Check different keys", func(t *testing.T) {
var oldKey string
for i := 0; i < 200; i++ {
key := kg.Generate(50)
require.NotEqual(t, key, oldKey)
oldKey = key
}
})
}

52
storage/file.go Normal file
View File

@@ -0,0 +1,52 @@
package storage
import (
"crypto/md5"
"encoding/hex"
"errors"
"io/fs"
"os"
"path/filepath"
"time"
)
type FileStorage struct {
path string
}
var _ Storage = (*FileStorage)(nil)
func md5Hex(input string) string {
sum := md5.Sum([]byte(input))
return hex.EncodeToString(sum[:])
}
func NewFileStorage(path string, _ time.Duration) Storage {
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
os.Mkdir(path, 0700)
}
return &FileStorage{path: path}
}
func (fs *FileStorage) Set(key string, value string, skip_expiration bool) error {
dst := filepath.Join(fs.path, md5Hex(key))
file, err := os.Create(dst)
if err != nil {
return err
}
_, err = file.WriteString(value)
return err
}
func (fs *FileStorage) Get(key string, skip_expiration bool) (string, error) {
dst := filepath.Join(fs.path, md5Hex(key))
file, err := os.ReadFile(dst)
if err != nil {
return "", err
}
return string(file), nil
}

55
storage/file_test.go Normal file
View File

@@ -0,0 +1,55 @@
package storage
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
func setupTempDir(t *testing.T) (string, func()) {
dir, err := os.MkdirTemp("", "filestorage_test")
require.NoError(t, err)
cleanup := func() { os.RemoveAll(dir) }
return dir, cleanup
}
func TestFileStorage(t *testing.T) {
dir, cleanup := setupTempDir(t)
t.Cleanup(cleanup)
const expiration = 2 // seconds
store := NewFileStorage(dir, expiration)
// Test Set
err := store.Set("testKey", "testValue", false)
require.NoError(t, err)
// Test Get
val, err := store.Get("testKey", false)
require.NoError(t, err)
require.Equal(t, "testValue", val)
_, err = store.Get("testKey", false)
require.NoError(t, err)
_, err = store.Get("testKey2", false)
require.True(t, os.IsNotExist(err))
}
func TestFileStorageSkipExpiration(t *testing.T) {
dir, cleanup := setupTempDir(t)
t.Cleanup(cleanup)
const expiration = 2 // seconds
store := NewFileStorage(dir, expiration)
// Test Set
err := store.Set("persistentKey", "persistentValue", true)
require.NoError(t, err)
// Test Get with skip_expiration
val, err := store.Get("persistentKey", true)
require.NoError(t, err)
require.Equal(t, "persistentValue", val)
}

55
storage/memcached.go Normal file
View File

@@ -0,0 +1,55 @@
package storage
import (
"strconv"
"github.com/bradfitz/gomemcache/memcache"
"github.com/sirupsen/logrus"
)
type MemcachedStorage struct {
client *memcache.Client
expiration int
}
func NewMemcachedStorage(host string, port int, expiration int) *MemcachedStorage {
client := memcache.New(host + ":" + strconv.Itoa(port))
// Check if connection is established
if err := client.Ping(); err != nil {
logrus.Fatalf("Failed to connect to Memcached: %v", err)
}
return &MemcachedStorage{client: client, expiration: expiration}
}
var _ Storage = (*MemcachedStorage)(nil)
func (s *MemcachedStorage) Set(key string, value string, skip_expiration bool) error {
item := &memcache.Item{
Key: key,
Value: []byte(value),
Expiration: int32(s.expiration),
}
if skip_expiration {
item.Expiration = 0
}
return s.client.Set(item)
}
func (s *MemcachedStorage) Get(key string, skip_expiration bool) (string, error) {
item, err := s.client.Get(key)
if err != nil {
return "", err
}
if !skip_expiration {
s.client.Replace(&memcache.Item{
Key: key,
Value: item.Value,
Expiration: int32(s.expiration),
})
}
return string(item.Value), nil
}

91
storage/memcached_test.go Normal file
View File

@@ -0,0 +1,91 @@
package storage
import (
"context"
"strings"
"testing"
"time"
"github.com/bradfitz/gomemcache/memcache"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func setupMemcachedContainer(t *testing.T) (string, int, func()) {
ctx := context.Background()
memcachedContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "memcached:latest",
ExposedPorts: []string{"11211/tcp"},
WaitingFor: wait.ForListeningPort("11211/tcp"),
},
Started: true,
})
require.NoError(t, err)
endpoint, err := memcachedContainer.Endpoint(ctx, "")
require.NoError(t, err)
host := strings.Split(endpoint, ":")[0]
port, err := memcachedContainer.MappedPort(ctx, "11211")
require.NoError(t, err)
cleanup := func() {
require.NoError(t, memcachedContainer.Terminate(ctx))
}
return host, port.Int(), cleanup
}
func TestMemcachedStorage(t *testing.T) {
host, port, cleanup := setupMemcachedContainer(t)
t.Cleanup(cleanup)
const expiration = 2 // seconds
store := NewMemcachedStorage(host, port, expiration)
// Test Set
err := store.Set("testKey", "testValue", false)
require.NoError(t, err)
// Test Get before expiration
val, err := store.Get("testKey", false)
require.NoError(t, err)
require.Equal(t, "testValue", val)
// Test if expiration is updated after Get
time.Sleep(1 * time.Second)
_, err = store.Get("testKey", false)
require.NoError(t, err) // Should still exist
time.Sleep(1 * time.Second) // Should reset expiration
_, err = store.Get("testKey", false)
require.NoError(t, err) // Should still exist due to refresh
_, err = store.Get("testKey2", false)
require.ErrorIs(t, memcache.ErrCacheMiss, err) // Should not exist
}
func TestMemcachedStorageSkipExpiration(t *testing.T) {
host, port, cleanup := setupMemcachedContainer(t)
t.Cleanup(cleanup)
const expiration = 2 // seconds
store := NewMemcachedStorage(host, port, expiration)
// Test Set
err := store.Set("persistentKey", "persistentValue", true)
require.NoError(t, err)
// Test Get with skip_expiration
val, err := store.Get("persistentKey", true)
require.NoError(t, err)
require.Equal(t, "persistentValue", val)
// Wait past expiration but skip expiration should still work
time.Sleep(time.Duration(expiration+1) * time.Second)
val, err = store.Get("persistentKey", true)
require.NoError(t, err)
require.Equal(t, "persistentValue", val)
}

144
storage/mongodb.go Normal file
View File

@@ -0,0 +1,144 @@
package storage
import (
"context"
"net/url"
"strconv"
"time"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
type MongoDBStorage struct {
db *mongo.Database
collection *mongo.Collection
expiration time.Duration
}
type item struct {
ObjectID any `json:"_id,omitempty" bson:"_id,omitempty"`
Key string `json:"key" bson:"key"`
Value []byte `json:"value" bson:"value"`
Expiration time.Time `json:"expiration,omitempty" bson:"expiration,omitempty"`
}
func NewMongoDBStorage(host string, port int, username string, password string, database string, expiration time.Duration) *MongoDBStorage {
ctx := context.Background()
dsn := "mongodb://"
if username != "" {
dsn += url.QueryEscape(username)
}
if password != "" {
dsn += ":" + url.QueryEscape(password)
}
if username != "" || password != "" {
dsn += "@"
}
dsn += host + ":" + strconv.Itoa(port)
client, err := mongo.Connect(options.Client().ApplyURI(dsn))
if err != nil {
logrus.Fatalf("Failed to connect to MongoDB: %v", err)
}
// Check if connection is established
if err := client.Ping(ctx, nil); err != nil {
logrus.Fatalf("Failed to connect to MongoDB: %v", err)
}
db := client.Database(database)
command := bson.M{"create": "entries"}
var result bson.M
if err := db.RunCommand(context.Background(), command).Decode(&result); err != nil {
panic(err)
}
// Create collection if not exists
collection := db.Collection("entries")
indexModel := mongo.IndexModel{
Keys: bson.M{"expiration": 1},
Options: options.Index().SetExpireAfterSeconds(0),
}
if _, err := collection.Indexes().CreateOne(ctx, indexModel); err != nil {
panic(err)
}
keyIndexModel := mongo.IndexModel{
Keys: bson.M{"key": 1},
Options: options.Index().SetUnique(true),
}
if _, err := collection.Indexes().CreateOne(ctx, keyIndexModel); err != nil {
panic(err)
}
return &MongoDBStorage{db: db, collection: collection, expiration: expiration}
}
func (s *MongoDBStorage) Set(key string, value string, skip_expiration bool) error {
ctx := context.Background()
// Convert value to []byte
valueBytes := []byte(value)
expiration := time.Now().Add(s.expiration)
if skip_expiration {
expiration = time.Time{}
}
// Create item
i := item{
Key: key,
Value: valueBytes,
Expiration: expiration,
}
// Insert item
if _, err := s.collection.InsertOne(ctx, i); err != nil {
return err
}
return nil
}
func (s *MongoDBStorage) Get(key string, skip_expiration bool) (string, error) {
ctx := context.Background()
// Find item
filter := bson.M{"key": key}
var i item
if err := s.collection.FindOne(ctx, filter).Decode(&i); err != nil {
return "", err
}
if !i.Expiration.IsZero() && i.Expiration.Unix() <= time.Now().Unix() {
_, err := s.collection.DeleteOne(ctx, filter)
if err != nil {
return "", err
}
return "", ErrNotFound
}
// Update expiration
if !skip_expiration {
i.Expiration = time.Now().Add(s.expiration)
update := bson.M{"$set": bson.M{"expiration": i.Expiration}}
if _, err := s.collection.UpdateOne(ctx, filter, update); err != nil {
return "", err
}
}
return string(i.Value), nil
}

100
storage/mongodb_test.go Normal file
View File

@@ -0,0 +1,100 @@
package storage
import (
"context"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
func setupMongoContainer(t *testing.T) (string, int, func()) {
ctx := context.Background()
mongoContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "mongo:latest",
ExposedPorts: []string{"27017/tcp"},
WaitingFor: wait.ForListeningPort("27017/tcp"),
},
Started: true,
})
require.NoError(t, err)
// Get container host and port
endpoint, err := mongoContainer.Endpoint(ctx, "")
require.NoError(t, err)
host := strings.Split(endpoint, ":")[0]
port, err := mongoContainer.MappedPort(ctx, "27017")
require.NoError(t, err)
// Connect to MongoDB
client, err := mongo.Connect(options.Client().ApplyURI("mongodb://" + endpoint))
require.NoError(t, err)
db := client.Database("testdb")
cleanup := func() {
require.NoError(t, db.Drop(ctx))
require.NoError(t, client.Disconnect(ctx))
require.NoError(t, mongoContainer.Terminate(ctx))
}
return host, port.Int(), cleanup
}
func TestMongoDBStorage(t *testing.T) {
host, port, cleanup := setupMongoContainer(t)
defer cleanup()
const expiration = 2 // seconds
store := NewMongoDBStorage(host, port, "", "", "testdb", expiration*time.Second)
// Test Set
err := store.Set("testKey", "testValue", false)
require.NoError(t, err)
// Test Get before expiration
val, err := store.Get("testKey", false)
require.NoError(t, err)
require.Equal(t, "testValue", val)
// Test expiration mechanism
time.Sleep(time.Duration(expiration+1) * time.Second)
val, err = store.Get("testKey", false)
require.Equal(t, "", val)
require.ErrorIs(t, ErrNotFound, err) // Should return error because the key should be expired
// Test key not existing
val, err = store.Get("testKey2", false)
require.Equal(t, "", val)
require.ErrorIs(t, mongo.ErrNoDocuments, err)
}
func TestMongoDBStorageSkipExpiration(t *testing.T) {
host, port, cleanup := setupMongoContainer(t)
defer cleanup()
const expiration = 2 // seconds
store := NewMongoDBStorage(host, port, "", "", "testdb", expiration*time.Second)
// Test Set
err := store.Set("persistentKey", "persistentValue", true)
require.NoError(t, err)
// Test Get with skip_expiration
val, err := store.Get("persistentKey", true)
require.NoError(t, err)
require.Equal(t, "persistentValue", val)
// Wait past expiration but skip expiration should still work
time.Sleep(time.Duration(expiration+1) * time.Second)
val, err = store.Get("persistentKey", true)
require.NoError(t, err)
require.Equal(t, "persistentValue", val)
}

88
storage/postgres.go Normal file
View File

@@ -0,0 +1,88 @@
package storage
import (
"context"
"strconv"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/sirupsen/logrus"
)
const setSQLQuery = "INSERT INTO entries (key, value, expiration) VALUES ($1, $2, $3) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, expiration = EXCLUDED.expiration"
const getSQLQuery = "SELECT id, value, expiration FROM entries WHERE key = $1"
const deleteSQLQuery = "DELETE FROM entries WHERE id = $1"
const updateSQLQuery = "UPDATE entries SET expiration = $1 WHERE id = $2"
type PostgresStorage struct {
pool *pgxpool.Pool
expiration int
}
var _ Storage = (*PostgresStorage)(nil)
func NewPostgresStorage(host string, port int, username string, passowrd string, database string, expiration int) *PostgresStorage {
dsn := "postgres://" + username + ":" + passowrd + "@" + host + ":" + strconv.Itoa(port) + "/" + database
pool, err := pgxpool.New(context.Background(), dsn)
if err != nil {
logrus.Fatalf("Failed to connect to PostgreSQL: %v", err)
}
// Check if connection is established
if err := pool.Ping(context.Background()); err != nil {
logrus.Fatalf("Failed to connect to PostgreSQL: %v", err)
}
// Create table if not exists
_, err = pool.Exec(context.Background(), "CREATE TABLE IF NOT EXISTS entries (id SERIAL PRIMARY KEY, key VARCHAR(255) NOT NULL UNIQUE, value TEXT, expiration BIGINT)")
if err != nil {
logrus.Fatalf("Failed to create table: %v", err)
}
return &PostgresStorage{pool: pool, expiration: expiration}
}
func (s *PostgresStorage) Set(key string, value string, skip_expiration bool) error {
ctx := context.Background() // TODO: Add timeout control
now := time.Now()
expiration := now.Add(time.Duration(s.expiration)).Unix()
if skip_expiration {
expiration = 0
}
_, err := s.pool.Exec(ctx, setSQLQuery, key, value, expiration)
return err
}
func (s *PostgresStorage) Get(key string, skip_expiration bool) (string, error) {
ctx := context.Background() // TODO: Add timeout control
var id int
var value string
var expiration int64
err := s.pool.QueryRow(ctx, getSQLQuery, key).Scan(&id, &value, &expiration)
if err != nil {
return "", err
}
// Delete if expired
if expiration != 0 && time.Now().Unix() > expiration {
_, err = s.pool.Exec(ctx, deleteSQLQuery, id)
if err != nil {
return "", err
}
return "", nil
}
// Update expiration
if !skip_expiration {
_, err = s.pool.Exec(ctx, updateSQLQuery, time.Now().Add(time.Duration(s.expiration)).Unix(), id)
if err != nil {
return "", err
}
}
return value, nil
}

70
storage/postgres_test.go Normal file
View File

@@ -0,0 +1,70 @@
package storage
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func setupTestContainer(t *testing.T) (string, int, func()) {
req := testcontainers.ContainerRequest{
Image: "postgres:latest",
Env: map[string]string{"POSTGRES_USER": "test", "POSTGRES_PASSWORD": "test", "POSTGRES_DB": "testdb"},
ExposedPorts: []string{"5432/tcp"},
WaitingFor: wait.ForListeningPort("5432/tcp"),
}
container, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
require.NoError(t, err)
host, err := container.Host(context.Background())
require.NoError(t, err)
port, err := container.MappedPort(context.Background(), "5432")
require.NoError(t, err)
return host, port.Int(), func() {
require.NoError(t, container.Terminate(context.Background()))
}
}
func TestPostgresStorage(t *testing.T) {
host, port, cleanup := setupTestContainer(t)
defer cleanup()
store := NewPostgresStorage(host, port, "test", "test", "testdb", 2)
err := store.Set("key1", "value1", false)
require.NoError(t, err)
val, err := store.Get("key1", false)
require.NoError(t, err)
require.Equal(t, "value1", val)
time.Sleep(3 * time.Second)
val, err = store.Get("key1", false)
require.NoError(t, err)
require.Empty(t, val)
// Test with skip expiration
err = store.Set("key1", "value1", false)
require.NoError(t, err)
val, err = store.Get("key1", true)
require.NoError(t, err)
require.Equal(t, "value1", val)
time.Sleep(2 * time.Second)
val, err = store.Get("key1", false)
require.NoError(t, err)
require.Empty(t, val)
}

Some files were not shown because too many files have changed in this diff Show More