diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..4782184 --- /dev/null +++ b/cmd/main.go @@ -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 + } +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..f3bc996 --- /dev/null +++ b/config.yaml @@ -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 \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..73bda69 --- /dev/null +++ b/config/config.go @@ -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 +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..5406ed9 --- /dev/null +++ b/config/config_test.go @@ -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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2aa1ebc --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..090e8fe --- /dev/null +++ b/go.sum @@ -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= diff --git a/handler/document.go b/handler/document.go new file mode 100644 index 0000000..92363cf --- /dev/null +++ b/handler/document.go @@ -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 +} diff --git a/keygenerator/key_generator.go b/keygenerator/key_generator.go new file mode 100644 index 0000000..9d276b5 --- /dev/null +++ b/keygenerator/key_generator.go @@ -0,0 +1,5 @@ +package keygenerator + +type KeyGenerator interface { + Generate(length int) string +} diff --git a/keygenerator/phonetic.go b/keygenerator/phonetic.go new file mode 100644 index 0000000..ef9276f --- /dev/null +++ b/keygenerator/phonetic.go @@ -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() +} diff --git a/keygenerator/phonetic_test.go b/keygenerator/phonetic_test.go new file mode 100644 index 0000000..324daac --- /dev/null +++ b/keygenerator/phonetic_test.go @@ -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)) + }) +} diff --git a/keygenerator/random.go b/keygenerator/random.go new file mode 100644 index 0000000..8c5aadb --- /dev/null +++ b/keygenerator/random.go @@ -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() +} diff --git a/keygenerator/random_test.go b/keygenerator/random_test.go new file mode 100644 index 0000000..64769ae --- /dev/null +++ b/keygenerator/random_test.go @@ -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 + } + }) +} diff --git a/storage/file.go b/storage/file.go new file mode 100644 index 0000000..6f4ec27 --- /dev/null +++ b/storage/file.go @@ -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 +} diff --git a/storage/file_test.go b/storage/file_test.go new file mode 100644 index 0000000..8a27e59 --- /dev/null +++ b/storage/file_test.go @@ -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) +} diff --git a/storage/memcached.go b/storage/memcached.go new file mode 100644 index 0000000..eee6a73 --- /dev/null +++ b/storage/memcached.go @@ -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 +} diff --git a/storage/memcached_test.go b/storage/memcached_test.go new file mode 100644 index 0000000..ce16233 --- /dev/null +++ b/storage/memcached_test.go @@ -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) +} diff --git a/storage/mongodb.go b/storage/mongodb.go new file mode 100644 index 0000000..9868363 --- /dev/null +++ b/storage/mongodb.go @@ -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 +} diff --git a/storage/mongodb_test.go b/storage/mongodb_test.go new file mode 100644 index 0000000..3da8155 --- /dev/null +++ b/storage/mongodb_test.go @@ -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) +} diff --git a/storage/postgres.go b/storage/postgres.go new file mode 100644 index 0000000..12de8f4 --- /dev/null +++ b/storage/postgres.go @@ -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 +} diff --git a/storage/postgres_test.go b/storage/postgres_test.go new file mode 100644 index 0000000..5fc4ca9 --- /dev/null +++ b/storage/postgres_test.go @@ -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) +} diff --git a/storage/redis.go b/storage/redis.go new file mode 100644 index 0000000..bfb79a1 --- /dev/null +++ b/storage/redis.go @@ -0,0 +1,62 @@ +package storage + +import ( + "context" + "strconv" + "time" + + "github.com/redis/go-redis/v9" + "github.com/sirupsen/logrus" +) + +type RedisStorage struct { + client *redis.Client + expiration time.Duration +} + +func NewRedisStorage(host string, port int, username string, password string, expiration time.Duration) *RedisStorage { + client := redis.NewClient(&redis.Options{ + Addr: host + ":" + strconv.Itoa(port), + Username: username, + Password: password, + }) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Check if connection is established with 5s timeout + if status := client.Ping(ctx); status.Err() != nil { + logrus.Fatalf("Failed to connect to Redis: %v", status.Err()) + } + + return &RedisStorage{client: client, expiration: expiration} +} + +var _ Storage = (*RedisStorage)(nil) + +func (s *RedisStorage) Set(key string, value string, skip_expiration bool) error { + ctx := context.Background() // TODO: Add timeout control + + expiry := s.expiration + if skip_expiration { + expiry = 0 + } + + return s.client.Set(ctx, key, value, expiry).Err() +} + +func (s *RedisStorage) Get(key string, skip_expiration bool) (string, error) { + ctx := context.Background() // TODO: Add timeout control + + res, err := s.client.Get(ctx, key).Result() + if err != nil { + return "", err + } + + // Update expiration + if !skip_expiration { + s.client.Expire(ctx, key, s.expiration) + } + + return res, nil +} diff --git a/storage/redis_test.go b/storage/redis_test.go new file mode 100644 index 0000000..6cae015 --- /dev/null +++ b/storage/redis_test.go @@ -0,0 +1,100 @@ +package storage + +import ( + "context" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +func setupRedisContainer(t *testing.T) (string, int, func()) { + ctx := context.Background() + + req := testcontainers.ContainerRequest{ + Image: "redis:7-alpine", + ExposedPorts: []string{"6379/tcp"}, + WaitingFor: wait.ForListeningPort("6379/tcp"), + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + require.NoError(t, err) + + host, err := container.Host(ctx) + require.NoError(t, err) + + port, err := container.MappedPort(ctx, "6379") + require.NoError(t, err) + + return host, port.Int(), func() { + require.NoError(t, container.Terminate(ctx)) + } +} + +func TestRedisStorage_SetAndGet(t *testing.T) { + host, port, close := setupRedisContainer(t) + defer close() + + expiration := time.Second * 2 + storage := NewRedisStorage(host, port, "", "", expiration) + + key := "testKey" + value := "testValue" + + err := storage.Set(key, value, false) + require.NoError(t, err) + + ttl, err := storage.client.TTL(context.Background(), key).Result() + require.NoError(t, err) + + require.True(t, ttl > 0 && ttl <= time.Duration(expiration)) + + time.Sleep(time.Second) + + ttl, err = storage.client.TTL(context.Background(), key).Result() + require.NoError(t, err) + + require.True(t, ttl > 0 && ttl <= time.Duration(expiration)) + + got, err := storage.Get(key, false) + require.NoError(t, err) + require.Equal(t, value, got) + + ttlAfterGet, err := storage.client.TTL(context.Background(), key).Result() // TTL should be reset after GET operation + require.NoError(t, err) + require.True(t, ttlAfterGet > time.Second) + + keyNoExpire := "testKeyNoExpire" + err = storage.Set(keyNoExpire, value, true) + require.NoError(t, err) + + ttlNoExpire, err := storage.client.TTL(context.Background(), keyNoExpire).Result() + require.NoError(t, err) + require.Equal(t, -1*time.Nanosecond, ttlNoExpire) + + got, err = storage.Get(keyNoExpire, true) + require.NoError(t, err) + require.Equal(t, value, got) + + ttlAfterGetNoExpire, err := storage.client.TTL(context.Background(), keyNoExpire).Result() + require.NoError(t, err) + require.Equal(t, -1*time.Nanosecond, ttlAfterGetNoExpire) // -1ns means no expiration +} + +func TestRedisStorage_GetNonExistentKey(t *testing.T) { + host, port, close := setupRedisContainer(t) + defer close() + + expiration := time.Second * 2 + storage := NewRedisStorage(host, port, "", "", expiration) + + _, err := storage.Get("nonExistentKey", false) + require.Error(t, err) + require.Equal(t, redis.Nil, err) +} diff --git a/storage/s3.go b/storage/s3.go new file mode 100644 index 0000000..6ac519c --- /dev/null +++ b/storage/s3.go @@ -0,0 +1,96 @@ +package storage + +import ( + "bytes" + "context" + "errors" + "strconv" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/retry" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/sirupsen/logrus" +) + +type S3Storage struct { + svc *s3.Client + downloader *manager.Downloader + uploader *manager.Uploader + bucket string +} + +func NewS3Storage(host string, port int, username string, password string, region string, bucket string) *S3Storage { + creds := credentials.NewStaticCredentialsProvider(username, password, "") + awscfg, err := config.LoadDefaultConfig(context.Background(), + config.WithRegion(region), + config.WithCredentialsProvider(creds), + config.WithRetryer(func() aws.Retryer { + return retry.AddWithMaxAttempts(retry.NewStandard(), 3) + }), + ) + + if err != nil { + logrus.Fatalf("unable to load SDK config, %v", err) + } + + svc := s3.NewFromConfig(awscfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String("http://" + host + ":" + strconv.Itoa(port)) + o.UsePathStyle = true + }) + + downloader := manager.NewDownloader(svc) + uploader := manager.NewUploader(svc) + + // Check if connection is established + _, err = svc.ListBuckets(context.Background(), &s3.ListBucketsInput{}) + if err != nil { + logrus.Fatalf("Failed to connect to S3: %v", err) + } + + // Create bucket if not exists + _, err = svc.CreateBucket(context.Background(), &s3.CreateBucketInput{ + Bucket: &bucket, + }) + if err != nil { + logrus.Fatalf("Failed to create bucket: %v", err) + } + + return &S3Storage{svc: svc, downloader: downloader, uploader: uploader, bucket: bucket} +} + +var ErrNotFound = errors.New("not found") + +var _ Storage = (*S3Storage)(nil) + +func (s *S3Storage) Set(key string, value string, skip_expiration bool) error { + ctx := context.Background() // TODO: Add timeout control + + _, err := s.uploader.Upload(ctx, &s3.PutObjectInput{ + Bucket: &s.bucket, + Key: aws.String(key), + Body: bytes.NewReader([]byte(value)), + }) + + return err +} + +func (s *S3Storage) Get(key string, skip_expiration bool) (string, error) { + var nsk *types.NoSuchKey + + ctx := context.Background() // TODO: Add timeout control + + buf := manager.NewWriteAtBuffer([]byte{}) + _, err := s.downloader.Download(ctx, buf, &s3.GetObjectInput{ + Bucket: &s.bucket, + Key: aws.String(key), + }) + if errors.As(err, &nsk) { + return "", ErrNotFound + } + + return string(buf.Bytes()), err +} diff --git a/storage/s3_test.go b/storage/s3_test.go new file mode 100644 index 0000000..3d241e8 --- /dev/null +++ b/storage/s3_test.go @@ -0,0 +1,64 @@ +package storage + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go/modules/minio" +) + +const ( + minioUser string = "minio-user" + minioPass string = "minio-password" + minioRegion string = "us-east-1" + minioBucket string = "test-bucket" +) + +func setupMinio(t *testing.T) (string, int, func()) { + ctx := context.Background() + + c, err := minio.Run(ctx, + "docker.io/minio/minio", + minio.WithUsername(minioUser), + minio.WithPassword(minioPass), + ) + if err != nil { + panic(err) + } + + host, err := c.Host(ctx) + require.NoError(t, err) + + fmt.Println(c.ConnectionString(ctx)) + + port, err := c.MappedPort(ctx, "9000") + require.NoError(t, err) + + return host, port.Int(), func() { + c.Terminate(ctx) + } +} + +func TestS3Storage(t *testing.T) { + host, port, cleanup := setupMinio(t) + defer cleanup() + + store := NewS3Storage(host, port, minioUser, minioPass, minioRegion, minioBucket) + + // 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) + + // Test Get not existing key + val, err = store.Get("nonExistingKey", false) + require.ErrorIs(t, ErrNotFound, err) + require.Equal(t, "", val) +} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..237f184 --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,6 @@ +package storage + +type Storage interface { + Set(key string, value string, skip_expiration bool) error + Get(key string, skip_expiration bool) (string, error) +}