refactor: replace kv usage with boltdb and update related configurations

This commit is contained in:
Bhunter 2025-01-12 14:11:51 +01:00 committed by divyam234
parent e966119bd4
commit 518435fcd3
26 changed files with 525 additions and 730 deletions

View file

@ -5,24 +5,15 @@ import (
"fmt"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"reflect"
"regexp"
"strings"
"syscall"
"time"
"unicode"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-co-op/gocron"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/tgdrive/teldrive/internal/api"
"github.com/tgdrive/teldrive/internal/appcontext"
"github.com/tgdrive/teldrive/internal/auth"
@ -31,12 +22,11 @@ import (
"github.com/tgdrive/teldrive/internal/config"
"github.com/tgdrive/teldrive/internal/database"
"github.com/tgdrive/teldrive/internal/duration"
"github.com/tgdrive/teldrive/internal/kv"
"github.com/tgdrive/teldrive/internal/logging"
"github.com/tgdrive/teldrive/internal/middleware"
"github.com/tgdrive/teldrive/internal/tgc"
"github.com/tgdrive/teldrive/internal/utils"
"github.com/tgdrive/teldrive/ui"
"go.etcd.io/bbolt"
"github.com/tgdrive/teldrive/pkg/cron"
"github.com/tgdrive/teldrive/pkg/services"
@ -46,85 +36,95 @@ import (
)
func NewRun() *cobra.Command {
config := config.Config{}
runCmd := &cobra.Command{
var cfg config.ServerCmdConfig
cmd := &cobra.Command{
Use: "run",
Short: "Start Teldrive Server",
Run: func(cmd *cobra.Command, args []string) {
runApplication(&config)
runApplication(cmd.Context(), &cfg)
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initViperConfig(cmd)
loader := config.NewConfigLoader()
if err := loader.InitializeConfig(cmd); err != nil {
return err
}
if err := loader.Load(&cfg); err != nil {
return err
}
if err := checkRequiredRunFlags(&cfg); err != nil {
return err
}
return nil
},
}
runCmd.Flags().StringP("config", "c", "", "Config file path (default $HOME/.teldrive/config.toml)")
runCmd.Flags().IntVarP(&config.Server.Port, "server-port", "p", 8080, "Server port")
duration.DurationVar(runCmd.Flags(), &config.Server.GracefulShutdown, "server-graceful-shutdown", 10*time.Second, "Server graceful shutdown timeout")
runCmd.Flags().BoolVar(&config.Server.EnablePprof, "server-enable-pprof", false, "Enable Pprof Profiling")
duration.DurationVar(runCmd.Flags(), &config.Server.ReadTimeout, "server-read-timeout", 1*time.Hour, "Server read timeout")
duration.DurationVar(runCmd.Flags(), &config.Server.WriteTimeout, "server-write-timeout", 1*time.Hour, "Server write timeout")
runCmd.Flags().BoolVar(&config.CronJobs.Enable, "cronjobs-enable", true, "Run cron jobs")
duration.DurationVar(runCmd.Flags(), &config.CronJobs.CleanFilesInterval, "cronjobs-clean-files-interval", 1*time.Hour, "Clean files interval")
duration.DurationVar(runCmd.Flags(), &config.CronJobs.CleanUploadsInterval, "cronjobs-clean-uploads-interval", 12*time.Hour, "Clean uploads interval")
duration.DurationVar(runCmd.Flags(), &config.CronJobs.FolderSizeInterval, "cronjobs-folder-size-interval", 2*time.Hour, "Folder size update interval")
runCmd.Flags().IntVar(&config.Cache.MaxSize, "cache-max-size", 10*1024*1024, "Max Cache max size (memory)")
runCmd.Flags().StringVar(&config.Cache.RedisAddr, "cache-redis-addr", "", "Redis address")
runCmd.Flags().StringVar(&config.Cache.RedisPass, "cache-redis-pass", "", "Redis password")
runCmd.Flags().IntVarP(&config.Log.Level, "log-level", "", -1, "Logging level")
runCmd.Flags().StringVar(&config.Log.File, "log-file", "", "Logging file path")
runCmd.Flags().BoolVar(&config.Log.Development, "log-development", false, "Enable development mode")
runCmd.Flags().StringVar(&config.JWT.Secret, "jwt-secret", "", "JWT secret key")
duration.DurationVar(runCmd.Flags(), &config.JWT.SessionTime, "jwt-session-time", (30*24)*time.Hour, "JWT session duration")
runCmd.Flags().StringSliceVar(&config.JWT.AllowedUsers, "jwt-allowed-users", []string{}, "Allowed users")
runCmd.Flags().StringVar(&config.DB.DataSource, "db-data-source", "", "Database connection string")
runCmd.Flags().IntVar(&config.DB.LogLevel, "db-log-level", 1, "Database log level")
runCmd.Flags().BoolVar(&config.DB.PrepareStmt, "db-prepare-stmt", true, "Enable prepared statements")
runCmd.Flags().BoolVar(&config.DB.Pool.Enable, "db-pool-enable", true, "Enable database pool")
runCmd.Flags().IntVar(&config.DB.Pool.MaxIdleConnections, "db-pool-max-open-connections", 25, "Database max open connections")
runCmd.Flags().IntVar(&config.DB.Pool.MaxIdleConnections, "db-pool-max-idle-connections", 25, "Database max idle connections")
duration.DurationVar(runCmd.Flags(), &config.DB.Pool.MaxLifetime, "db-pool-max-lifetime", 10*time.Minute, "Database max connection lifetime")
runCmd.Flags().IntVar(&config.TG.AppId, "tg-app-id", 0, "Telegram app ID")
runCmd.Flags().StringVar(&config.TG.AppHash, "tg-app-hash", "", "Telegram app hash")
runCmd.Flags().StringVar(&config.TG.SessionFile, "tg-session-file", "", "Bot session file path")
runCmd.Flags().BoolVar(&config.TG.RateLimit, "tg-rate-limit", true, "Enable rate limiting for telegram client")
runCmd.Flags().IntVar(&config.TG.RateBurst, "tg-rate-burst", 5, "Limiting burst for telegram client")
runCmd.Flags().IntVar(&config.TG.Rate, "tg-rate", 100, "Limiting rate for telegram client")
runCmd.Flags().StringVar(&config.TG.DeviceModel, "tg-device-model",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0", "Device model")
runCmd.Flags().StringVar(&config.TG.SystemVersion, "tg-system-version", "Win32", "System version")
runCmd.Flags().StringVar(&config.TG.AppVersion, "tg-app-version", "4.6.3 K", "App version")
runCmd.Flags().StringVar(&config.TG.LangCode, "tg-lang-code", "en", "Language code")
runCmd.Flags().StringVar(&config.TG.SystemLangCode, "tg-system-lang-code", "en-US", "System language code")
runCmd.Flags().StringVar(&config.TG.LangPack, "tg-lang-pack", "webk", "Language pack")
runCmd.Flags().StringVar(&config.TG.Proxy, "tg-proxy", "", "HTTP OR SOCKS5 proxy URL")
runCmd.Flags().BoolVar(&config.TG.DisableStreamBots, "tg-disable-stream-bots", false, "Disable Stream bots")
runCmd.Flags().BoolVar(&config.TG.Ntp, "tg-ntp", false, "Use NTP server time")
runCmd.Flags().BoolVar(&config.TG.EnableLogging, "tg-enable-logging", false, "Enable telegram client logging")
runCmd.Flags().StringVar(&config.TG.Uploads.EncryptionKey, "tg-uploads-encryption-key", "", "Uploads encryption key")
runCmd.Flags().IntVar(&config.TG.Uploads.Threads, "tg-uploads-threads", 8, "Uploads threads")
runCmd.Flags().IntVar(&config.TG.Uploads.MaxRetries, "tg-uploads-max-retries", 10, "Uploads Retries")
runCmd.Flags().Int64Var(&config.TG.PoolSize, "tg-pool-size", 8, "Telegram Session pool size")
duration.DurationVar(runCmd.Flags(), &config.TG.ReconnectTimeout, "tg-reconnect-timeout", 5*time.Minute, "Reconnect Timeout")
duration.DurationVar(runCmd.Flags(), &config.TG.Uploads.Retention, "tg-uploads-retention", (24*7)*time.Hour, "Uploads retention duration")
duration.DurationVar(runCmd.Flags(), &config.TG.BgBotsCheckInterval, "tg-bg-bots-check-interval", 4*time.Hour, "Interval for checking Idle background bots")
runCmd.Flags().IntVar(&config.TG.Stream.MultiThreads, "tg-stream-multi-threads", 0, "Stream multi-threads")
runCmd.Flags().IntVar(&config.TG.Stream.Buffers, "tg-stream-buffers", 8, "No of Stream buffers")
duration.DurationVar(runCmd.Flags(), &config.TG.Stream.ChunkTimeout, "tg-stream-chunk-timeout", 20*time.Second, "Chunk Fetch Timeout")
runCmd.MarkFlagRequired("tg-app-id")
runCmd.MarkFlagRequired("tg-app-hash")
runCmd.MarkFlagRequired("db-data-source")
runCmd.MarkFlagRequired("jwt-secret")
return runCmd
addServerFlags(cmd, &cfg)
return cmd
}
func addServerFlags(cmd *cobra.Command, cfg *config.ServerCmdConfig) {
flags := cmd.Flags()
config.AddCommonFlags(flags, cfg)
// Server config
flags.IntVarP(&cfg.Server.Port, "server-port", "p", 8080, "Server port")
duration.DurationVar(flags, &cfg.Server.GracefulShutdown, "server-graceful-shutdown", 10*time.Second, "Server graceful shutdown timeout")
flags.BoolVar(&cfg.Server.EnablePprof, "server-enable-pprof", false, "Enable Pprof Profiling")
duration.DurationVar(flags, &cfg.Server.ReadTimeout, "server-read-timeout", 1*time.Hour, "Server read timeout")
duration.DurationVar(flags, &cfg.Server.WriteTimeout, "server-write-timeout", 1*time.Hour, "Server write timeout")
// CronJobs config
flags.BoolVar(&cfg.CronJobs.Enable, "cronjobs-enable", true, "Run cron jobs")
duration.DurationVar(flags, &cfg.CronJobs.CleanFilesInterval, "cronjobs-clean-files-interval", 1*time.Hour, "Clean files interval")
duration.DurationVar(flags, &cfg.CronJobs.CleanUploadsInterval, "cronjobs-clean-uploads-interval", 12*time.Hour, "Clean uploads interval")
duration.DurationVar(flags, &cfg.CronJobs.FolderSizeInterval, "cronjobs-folder-size-interval", 2*time.Hour, "Folder size update interval")
// Cache config
flags.IntVar(&cfg.Cache.MaxSize, "cache-max-size", 10*1024*1024, "Max Cache max size (memory)")
flags.StringVar(&cfg.Cache.RedisAddr, "cache-redis-addr", "", "Redis address")
flags.StringVar(&cfg.Cache.RedisPass, "cache-redis-pass", "", "Redis password")
// JWT config
flags.StringVar(&cfg.JWT.Secret, "jwt-secret", "", "JWT secret key")
duration.DurationVar(flags, &cfg.JWT.SessionTime, "jwt-session-time", (30*24)*time.Hour, "JWT session duration")
flags.StringSliceVar(&cfg.JWT.AllowedUsers, "jwt-allowed-users", []string{}, "Allowed users")
// Telegram Uploads config
flags.StringVar(&cfg.TG.Uploads.EncryptionKey, "tg-uploads-encryption-key", "", "Uploads encryption key")
flags.IntVar(&cfg.TG.Uploads.Threads, "tg-uploads-threads", 8, "Uploads threads")
flags.IntVar(&cfg.TG.Uploads.MaxRetries, "tg-uploads-max-retries", 10, "Uploads Retries")
duration.DurationVar(flags, &cfg.TG.ReconnectTimeout, "tg-reconnect-timeout", 5*time.Minute, "Reconnect Timeout")
duration.DurationVar(flags, &cfg.TG.Uploads.Retention, "tg-uploads-retention", (24*7)*time.Hour, "Uploads retention duration")
flags.IntVar(&cfg.TG.Stream.MultiThreads, "tg-stream-multi-threads", 0, "Stream multi-threads")
flags.IntVar(&cfg.TG.Stream.Buffers, "tg-stream-buffers", 8, "No of Stream buffers")
duration.DurationVar(flags, &cfg.TG.Stream.ChunkTimeout, "tg-stream-chunk-timeout", 20*time.Second, "Chunk Fetch Timeout")
}
func checkRequiredRunFlags(cfg *config.ServerCmdConfig) error {
var missingFields []string
if cfg.DB.DataSource == "" {
missingFields = append(missingFields, "db-data-source")
}
if cfg.JWT.Secret == "" {
missingFields = append(missingFields, "jwt-secret")
}
if cfg.TG.AppHash == "" {
missingFields = append(missingFields, "tg-app-hash")
}
if cfg.TG.AppId == 0 {
missingFields = append(missingFields, "tg-app-id")
}
if len(missingFields) > 0 {
return fmt.Errorf("required configuration values not set: %s", strings.Join(missingFields, ", "))
}
return nil
}
func findAvailablePort(startPort int) (int, error) {
for port := startPort; port < startPort+100; port++ {
addr := fmt.Sprintf(":%d", port)
@ -138,21 +138,16 @@ func findAvailablePort(startPort int) (int, error) {
return 0, fmt.Errorf("no available ports found between %d and %d", startPort, startPort+100)
}
func runApplication(conf *config.Config) {
func runApplication(ctx context.Context, conf *config.ServerCmdConfig) {
logging.SetConfig(&logging.Config{
Level: zapcore.Level(conf.Log.Level),
Development: conf.Log.Development,
FilePath: conf.Log.File,
})
ctx, cancel := context.WithCancel(context.Background())
lg := logging.DefaultLogger().Sugar()
defer func() {
logging.DefaultLogger().Sync()
cancel()
}()
defer lg.Sync()
port, err := findAvailablePort(conf.Server.Port)
if err != nil {
@ -165,23 +160,29 @@ func runApplication(conf *config.Config) {
scheduler := gocron.NewScheduler(time.UTC)
cacher := cache.NewCache(ctx, conf)
cacher := cache.NewCache(ctx, &conf.Cache)
db, err := database.NewDatabase(conf, lg)
db, err := database.NewDatabase(&conf.DB, lg)
if err != nil {
lg.Fatalw("failed to create database", "err", err)
}
kv := kv.NewBoltKV(conf)
err = database.MigrateDB(db)
if err != nil {
lg.Fatalw("failed to migrate database", "err", err)
}
boltDb, err := tgc.NewBoltDB(conf.TG.SessionFile)
if err != nil {
lg.Fatalw("failed to create bolt db", "err", err)
}
worker := tgc.NewBotWorker()
srv := setupServer(conf, db, cacher, kv, worker)
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
srv := setupServer(conf, db, cacher, boltDb, worker)
cron.StartCronJobs(scheduler, db, conf)
@ -192,7 +193,7 @@ func runApplication(conf *config.Config) {
}
}()
<-stop
<-ctx.Done()
lg.Info("Shutting down server...")
@ -209,13 +210,13 @@ func runApplication(conf *config.Config) {
lg.Info("Server stopped")
}
func setupServer(cfg *config.Config, db *gorm.DB, cache cache.Cacher, kv kv.KV, worker *tgc.BotWorker) *http.Server {
func setupServer(cfg *config.ServerCmdConfig, db *gorm.DB, cache cache.Cacher, boltdb *bbolt.DB, worker *tgc.BotWorker) *http.Server {
lg := logging.DefaultLogger()
apiSrv := services.NewApiService(db, cfg, cache, kv, worker)
apiSrv := services.NewApiService(db, cfg, cache, boltdb, worker)
srv, err := api.NewServer(apiSrv, auth.NewSecurityHandler(db, cache, cfg))
srv, err := api.NewServer(apiSrv, auth.NewSecurityHandler(db, cache, &cfg.JWT))
if err != nil {
lg.Fatal("failed to create server", zap.Error(err))
@ -254,72 +255,3 @@ func setupServer(cfg *config.Config, db *gorm.DB, cache cache.Cacher, kv kv.KV,
IdleTimeout: 60 * time.Second,
}
}
func initViperConfig(cmd *cobra.Command) error {
viper.SetConfigType("toml")
cfgFile := cmd.Flags().Lookup("config").Value.String()
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, _ := homedir.Dir()
viper.AddConfigPath(filepath.Join(home, ".teldrive"))
viper.AddConfigPath(".")
viper.AddConfigPath(utils.ExecutableDir())
viper.SetConfigName("config")
}
viper.SetEnvPrefix("teldrive")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.AutomaticEnv()
viper.ReadInConfig()
bindFlags(cmd.Flags(), "", reflect.ValueOf(config.Config{}))
return nil
}
func bindFlags(flags *pflag.FlagSet, prefix string, v reflect.Value) {
t := v.Type()
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
for i := range t.NumField() {
field := t.Field(i)
switch field.Type.Kind() {
case reflect.Struct:
bindFlags(flags, fmt.Sprintf("%s.%s", prefix, strings.ToLower(field.Name)), v.Field(i))
default:
newPrefix := prefix[1:]
newName := modifyFlag(field.Name)
configName := fmt.Sprintf("%s.%s", newPrefix, newName)
flag := flags.Lookup(fmt.Sprintf("%s-%s", strings.ReplaceAll(newPrefix, ".", "-"), newName))
if !flag.Changed && viper.IsSet(configName) {
confVal := viper.Get(configName)
if field.Type.Kind() == reflect.Slice {
sliceValue, ok := confVal.([]interface{})
if ok {
for _, v := range sliceValue {
flag.Value.Set(fmt.Sprintf("%v", v))
}
}
} else {
flags.Set(flag.Name, fmt.Sprintf("%v", confVal))
}
}
}
}
}
func modifyFlag(s string) string {
var result []rune
for i, c := range s {
if i > 0 && unicode.IsUpper(c) {
result = append(result, '-')
}
result = append(result, unicode.ToLower(c))
}
return string(result)
}

25
go.mod
View file

@ -11,9 +11,10 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/gotd/contrib v0.21.0
github.com/gotd/td v0.116.0
github.com/gotd/td v0.117.0
github.com/iyear/connectproxy v0.1.1
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.5.0
github.com/ogen-go/ogen v1.8.1
github.com/redis/go-redis/v9 v9.7.0
github.com/spf13/cobra v1.8.1
@ -22,7 +23,7 @@ require (
github.com/vmihailenco/msgpack/v5 v5.4.1
go.etcd.io/bbolt v1.3.11
go.uber.org/zap v1.27.0
golang.org/x/time v0.8.0
golang.org/x/time v0.9.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/datatypes v1.2.5
gorm.io/driver/postgres v1.5.11
@ -46,23 +47,23 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.org/x/tools v0.29.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/mysql v1.5.7 // indirect
@ -85,17 +86,17 @@ require (
github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pressly/goose/v3 v3.24.0
github.com/pressly/goose/v3 v3.24.1
github.com/segmentio/asm v1.2.0 // indirect
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.31.0
golang.org/x/net v0.33.0
golang.org/x/crypto v0.32.0
golang.org/x/net v0.34.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/qr v0.2.0 // indirect

92
go.sum
View file

@ -2,10 +2,14 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/WinterYukky/gorm-extra-clause-plugin v0.3.0 h1:fQfTkxoRso6mlm7eOfBIk94aqamJeqxCEfU+MyLWhgo=
github.com/WinterYukky/gorm-extra-clause-plugin v0.3.0/go.mod h1:GFT8TzxeeGKYXNU/65PsiN2+zNHigm9HjybnbL1T7eg=
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
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/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=
@ -15,6 +19,18 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA=
github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
@ -37,6 +53,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
@ -54,15 +72,23 @@ github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/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-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -76,8 +102,8 @@ github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
github.com/gotd/td v0.116.0 h1:MSZZ4lrfn6wDSYyVSKLvFDtBzBOmb9jv0MUBw/PiIgg=
github.com/gotd/td v0.116.0/go.mod h1:G65yEk83sFJU9s0xowP3dDtvXsTp0tmSrv5NfgI+ZY0=
github.com/gotd/td v0.117.0 h1:Z6vU5thb5DW/I1s0sLSeSfA/QWvwszx6SxHhEEYJiU8=
github.com/gotd/td v0.117.0/go.mod h1:jf1Zf1ViTN+H1x8dhDTCBHOYY/2E/40HsyOsohxqXYA=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
@ -112,9 +138,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
@ -127,6 +152,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ogen-go/ogen v1.8.1 h1:7TZ+oIeLkcBiyl0qu0fHPrFUrGWDj3Fi/zKSWg2i2Tg=
@ -134,11 +161,21 @@ github.com/ogen-go/ogen v1.8.1/go.mod h1:2ShRm6u/nXUHuwdVKv2SeaG8enBKPKAE3kSbHww
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.24.0 h1:sFbNms7Bd++2VMq6HSgDHDLWa7kHz1qXzPb3ZIU72VU=
github.com/pressly/goose/v3 v3.24.0/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4LY=
github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
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.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
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.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@ -150,8 +187,8 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
@ -160,8 +197,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@ -188,8 +225,12 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@ -201,27 +242,30 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View file

@ -93,7 +93,7 @@ func GetSessionByHash(db *gorm.DB, cache cache.Cacher, hash string) (*models.Ses
type securityHandler struct {
db *gorm.DB
cache cache.Cacher
cfg *config.Config
cfg *config.JWTConfig
}
func (s *securityHandler) HandleApiKeyAuth(ctx context.Context, operationName api.OperationName, t api.ApiKeyAuth) (context.Context, error) {
@ -105,14 +105,14 @@ func (s *securityHandler) HandleBearerAuth(ctx context.Context, operationName ap
}
func (s *securityHandler) handleAuth(ctx context.Context, token string) (context.Context, error) {
claims, err := VerifyUser(s.db, s.cache, s.cfg.JWT.Secret, token)
claims, err := VerifyUser(s.db, s.cache, s.cfg.Secret, token)
if err != nil {
return nil, &ogenerrors.SecurityError{Err: err}
}
return context.WithValue(ctx, authKey, claims), nil
}
func NewSecurityHandler(db *gorm.DB, cache cache.Cacher, cfg *config.Config) api.SecurityHandler {
func NewSecurityHandler(db *gorm.DB, cache cache.Cacher, cfg *config.JWTConfig) api.SecurityHandler {
return &securityHandler{db: db, cache: cache, cfg: cfg}
}

View file

@ -23,14 +23,14 @@ type MemoryCache struct {
mu sync.RWMutex
}
func NewCache(ctx context.Context, conf *config.Config) Cacher {
func NewCache(ctx context.Context, conf *config.CacheConfig) Cacher {
var cacher Cacher
if conf.Cache.RedisAddr == "" {
cacher = NewMemoryCache(conf.Cache.MaxSize)
if conf.RedisAddr == "" {
cacher = NewMemoryCache(conf.MaxSize)
} else {
cacher = NewRedisCache(ctx, redis.NewClient(&redis.Options{
Addr: conf.Cache.RedisAddr,
Password: conf.Cache.RedisPass,
Addr: conf.RedisAddr,
Password: conf.RedisPass,
}))
}
return cacher

View file

@ -1,91 +1,231 @@
package config
import (
"fmt"
"path/filepath"
"reflect"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/tgdrive/teldrive/internal/duration"
)
type Config struct {
Server ServerConfig
Log LoggingConfig
JWT JWTConfig
DB DBConfig
TG TGConfig
CronJobs CronJobConfig
Cache struct {
MaxSize int
RedisAddr string
RedisPass string
}
}
type ServerConfig struct {
Port int
GracefulShutdown time.Duration
EnablePprof bool
ReadTimeout time.Duration
WriteTimeout time.Duration
Port int `mapstructure:"port"`
GracefulShutdown time.Duration `mapstructure:"graceful-shutdown"`
EnablePprof bool `mapstructure:"enable-pprof"`
ReadTimeout time.Duration `mapstructure:"read-timeout"`
WriteTimeout time.Duration `mapstructure:"write-timeout"`
}
type CronJobConfig struct {
Enable bool
CleanFilesInterval time.Duration
CleanUploadsInterval time.Duration
FolderSizeInterval time.Duration
}
type TGConfig struct {
AppId int
AppHash string
RateLimit bool
RateBurst int
Rate int
DeviceModel string
SystemVersion string
AppVersion string
LangCode string
SystemLangCode string
LangPack string
Ntp bool
SessionFile string
DisableStreamBots bool
BgBotsCheckInterval time.Duration
Proxy string
ReconnectTimeout time.Duration
PoolSize int64
EnableLogging bool
Uploads struct {
EncryptionKey string
Threads int
MaxRetries int
Retention time.Duration
}
Stream struct {
MultiThreads int
Buffers int
ChunkTimeout time.Duration
}
type CacheConfig struct {
MaxSize int `mapstructure:"max-size"`
RedisAddr string `mapstructure:"redis-addr"`
RedisPass string `mapstructure:"redis-pass"`
}
type LoggingConfig struct {
Level int
Development bool
File string
Level int `mapstructure:"level"`
Development bool `mapstructure:"development"`
File string `mapstructure:"file"`
}
type JWTConfig struct {
Secret string
SessionTime time.Duration
AllowedUsers []string
Secret string `mapstructure:"secret"`
SessionTime time.Duration `mapstructure:"session-time"`
AllowedUsers []string `mapstructure:"allowed-users"`
}
type DBConfig struct {
DataSource string
PrepareStmt bool
LogLevel int
DataSource string `mapstructure:"data-source"`
PrepareStmt bool `mapstructure:"prepare-stmt"`
LogLevel int `mapstructure:"log-level"`
Pool struct {
Enable bool
MaxOpenConnections int
MaxIdleConnections int
MaxLifetime time.Duration
Enable bool `mapstructure:"enable"`
MaxOpenConnections int `mapstructure:"max-open-connections"`
MaxIdleConnections int `mapstructure:"max-idle-connections"`
MaxLifetime time.Duration `mapstructure:"max-lifetime"`
} `mapstructure:"pool"`
}
type CronJobConfig struct {
Enable bool `mapstructure:"enable"`
CleanFilesInterval time.Duration `mapstructure:"clean-files-interval"`
CleanUploadsInterval time.Duration `mapstructure:"clean-uploads-interval"`
FolderSizeInterval time.Duration `mapstructure:"folder-size-interval"`
}
type TGConfig struct {
AppId int `mapstructure:"app-id"`
AppHash string `mapstructure:"app-hash"`
RateLimit bool `mapstructure:"rate-limit"`
RateBurst int `mapstructure:"rate-burst"`
Rate int `mapstructure:"rate"`
UserName string `mapstructure:"user-name"`
DeviceModel string `mapstructure:"device-model"`
SystemVersion string `mapstructure:"system-version"`
AppVersion string `mapstructure:"app-version"`
LangCode string `mapstructure:"lang-code"`
SystemLangCode string `mapstructure:"system-lang-code"`
LangPack string `mapstructure:"lang-pack"`
Ntp bool `mapstructure:"ntp"`
SessionFile string `mapstructure:"session-file"`
DisableStreamBots bool `mapstructure:"disable-stream-bots"`
Proxy string `mapstructure:"proxy"`
ReconnectTimeout time.Duration `mapstructure:"reconnect-timeout"`
PoolSize int64 `mapstructure:"pool-size"`
EnableLogging bool `mapstructure:"enable-logging"`
Uploads struct {
EncryptionKey string `mapstructure:"encryption-key"`
Threads int `mapstructure:"threads"`
MaxRetries int `mapstructure:"max-retries"`
Retention time.Duration `mapstructure:"retention"`
} `mapstructure:"uploads"`
Stream struct {
MultiThreads int `mapstructure:"multi-threads"`
Buffers int `mapstructure:"buffers"`
ChunkTimeout time.Duration `mapstructure:"chunk-timeout"`
} `mapstructure:"stream"`
}
type ServerCmdConfig struct {
Server ServerConfig `mapstructure:"server"`
Log LoggingConfig `mapstructure:"log"`
JWT JWTConfig `mapstructure:"jwt"`
DB DBConfig `mapstructure:"db"`
TG TGConfig `mapstructure:"tg"`
CronJobs CronJobConfig `mapstructure:"cronjobs"`
Cache CacheConfig `mapstructure:"cache"`
}
type MigrateCmdConfig struct {
DB DBConfig `mapstructure:"db"`
Log LoggingConfig `mapstructure:"log"`
}
type ConfigLoader struct {
v *viper.Viper
}
func NewConfigLoader() *ConfigLoader {
return &ConfigLoader{
v: viper.New(),
}
}
func StringToDurationHook() mapstructure.DecodeHookFunc {
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(time.Duration(0)) {
return data, nil
}
str, ok := data.(string)
if !ok {
return data, nil
}
return duration.ParseDuration(str)
}
}
func (cl *ConfigLoader) InitializeConfig(cmd *cobra.Command) error {
cl.v.SetConfigType("toml")
cfgFile := cmd.Flags().Lookup("config").Value.String()
if cfgFile != "" {
cl.v.SetConfigFile(cfgFile)
} else {
home, err := homedir.Dir()
if err != nil {
return fmt.Errorf("error getting home directory: %v", err)
}
cl.v.AddConfigPath(filepath.Join(home, ".teldrive"))
cl.v.AddConfigPath(".")
cl.v.SetConfigName("config")
}
cl.v.SetEnvPrefix("teldrive")
cl.v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
cl.v.AutomaticEnv()
if err := cl.v.BindPFlags(cmd.Flags()); err != nil {
return fmt.Errorf("error binding flags: %v", err)
}
if err := cl.v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return fmt.Errorf("error reading config file: %v", err)
}
}
return nil
}
func (cl *ConfigLoader) Load(cfg interface{}) error {
config := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
StringToDurationHook(),
),
WeaklyTypedInput: true,
Result: cfg,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return fmt.Errorf("failed to create decoder: %v", err)
}
if err := decoder.Decode(cl.v.AllSettings()); err != nil {
return fmt.Errorf("failed to decode config: %v", err)
}
return nil
}
func AddCommonFlags(flags *pflag.FlagSet, config *ServerCmdConfig) {
flags.StringP("config", "c", "", "Config file path (default $HOME/.teldrive/config.toml)")
// Log config
flags.IntVarP(&config.Log.Level, "log-level", "", -1, "Logging level")
flags.StringVar(&config.Log.File, "log-file", "", "Logging file path")
flags.BoolVar(&config.Log.Development, "log-development", false, "Enable development mode")
// DB config
flags.StringVar(&config.DB.DataSource, "db-data-source", "", "Database connection string")
flags.IntVar(&config.DB.LogLevel, "db-log-level", 1, "Database log level")
flags.BoolVar(&config.DB.PrepareStmt, "db-prepare-stmt", true, "Enable prepared statements")
flags.BoolVar(&config.DB.Pool.Enable, "db-pool-enable", true, "Enable database pool")
flags.IntVar(&config.DB.Pool.MaxIdleConnections, "db-pool-max-open-connections", 25, "Database max open connections")
flags.IntVar(&config.DB.Pool.MaxIdleConnections, "db-pool-max-idle-connections", 25, "Database max idle connections")
duration.DurationVar(flags, &config.DB.Pool.MaxLifetime, "db-pool-max-lifetime", 10*time.Minute, "Database max connection lifetime")
// Telegram config
flags.IntVar(&config.TG.AppId, "tg-app-id", 0, "Telegram app ID")
flags.StringVar(&config.TG.AppHash, "tg-app-hash", "", "Telegram app hash")
flags.StringVar(&config.TG.SessionFile, "tg-session-file", "", "Bot session file path")
flags.BoolVar(&config.TG.RateLimit, "tg-rate-limit", true, "Enable rate limiting for telegram client")
flags.IntVar(&config.TG.RateBurst, "tg-rate-burst", 5, "Limiting burst for telegram client")
flags.IntVar(&config.TG.Rate, "tg-rate", 100, "Limiting rate for telegram client")
flags.StringVar(&config.TG.DeviceModel, "tg-device-model",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0", "Device model")
flags.StringVar(&config.TG.SystemVersion, "tg-system-version", "Win32", "System version")
flags.StringVar(&config.TG.AppVersion, "tg-app-version", "4.6.3 K", "App version")
flags.StringVar(&config.TG.LangCode, "tg-lang-code", "en", "Language code")
flags.StringVar(&config.TG.SystemLangCode, "tg-system-lang-code", "en-US", "System language code")
flags.StringVar(&config.TG.LangPack, "tg-lang-pack", "webk", "Language pack")
flags.StringVar(&config.TG.Proxy, "tg-proxy", "", "HTTP OR SOCKS5 proxy URL")
flags.BoolVar(&config.TG.DisableStreamBots, "tg-disable-stream-bots", false, "Disable Stream bots")
flags.BoolVar(&config.TG.Ntp, "tg-ntp", false, "Use NTP server time")
flags.BoolVar(&config.TG.EnableLogging, "tg-enable-logging", false, "Enable telegram client logging")
flags.Int64Var(&config.TG.PoolSize, "tg-pool-size", 8, "Telegram Session pool size")
}

View file

@ -13,16 +13,16 @@ import (
"gorm.io/gorm/schema"
)
func NewDatabase(cfg *config.Config, lg *zap.SugaredLogger) (*gorm.DB, error) {
func NewDatabase(cfg *config.DBConfig, lg *zap.SugaredLogger) (*gorm.DB, error) {
var (
db *gorm.DB
err error
logger = NewLogger(lg, time.Second, true, zapcore.Level(cfg.DB.LogLevel))
logger = NewLogger(lg, time.Second, true, zapcore.Level(cfg.LogLevel))
)
for i := 0; i <= 5; i++ {
db, err = gorm.Open(postgres.New(postgres.Config{
DSN: cfg.DB.DataSource,
PreferSimpleProtocol: !cfg.DB.PrepareStmt,
DSN: cfg.DataSource,
PreferSimpleProtocol: !cfg.PrepareStmt,
}), &gorm.Config{
Logger: logger,
NamingStrategy: schema.NamingStrategy{
@ -46,20 +46,14 @@ func NewDatabase(cfg *config.Config, lg *zap.SugaredLogger) (*gorm.DB, error) {
db.Use(extraClausePlugin.New())
if cfg.DB.Pool.Enable {
if cfg.Pool.Enable {
rawDB, err := db.DB()
if err != nil {
return nil, err
}
rawDB.SetMaxOpenConns(cfg.DB.Pool.MaxOpenConnections)
rawDB.SetMaxIdleConns(cfg.DB.Pool.MaxIdleConnections)
rawDB.SetConnMaxLifetime(cfg.DB.Pool.MaxLifetime)
}
sqlDb, _ := db.DB()
err = migrateDB(sqlDb)
if err != nil {
lg.Fatalf("database: %v", err)
rawDB.SetMaxOpenConns(cfg.Pool.MaxOpenConnections)
rawDB.SetMaxIdleConns(cfg.Pool.MaxIdleConnections)
rawDB.SetConnMaxLifetime(cfg.Pool.MaxLifetime)
}
return db, nil

View file

@ -1,7 +1,6 @@
package database
import (
"database/sql"
"embed"
"errors"
"fmt"
@ -44,7 +43,7 @@ func NewTestDatabase(tb testing.TB, migration bool) *gorm.DB {
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
if migration {
migrateDB(sqlDB)
MigrateDB(db)
}
return db
@ -69,14 +68,15 @@ func DeleteRecordAll(_ testing.TB, db *gorm.DB, tableWhereClauses []string) erro
return nil
}
func migrateDB(db *sql.DB) error {
func MigrateDB(db *gorm.DB) error {
sqlDb, _ := db.DB()
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect("postgres"); err != nil {
return fmt.Errorf("failed run migrate: %w", err)
return err
}
if err := goose.Up(db, "migrations"); err != nil {
return fmt.Errorf("failed run migrate: %w", err)
if err := goose.Up(sqlDb, "migrations"); err != nil {
return err
}
return nil
}

View file

@ -27,7 +27,7 @@ func (d *Duration) String() string {
}
func (d *Duration) Set(s string) error {
v, err := parseDuration(s)
v, err := ParseDuration(s)
*d = Duration(v)
return err
}
@ -89,10 +89,17 @@ func DurationVar(f *pflag.FlagSet, p *time.Duration, name string, value time.Dur
f.VarP(newDurationValue(value, p), name, "", usage)
}
func parseDuration(age string) (time.Duration, error) {
func ParseDuration(age string) (time.Duration, error) {
return parseDurationFromNow(age)
}
func (d *Duration) UnmarshalText(text []byte) error {
if err := d.Set(string(text)); err != nil {
return err
}
return nil
}
func (d *Duration) Type() string {
return "Duration"
}

View file

@ -3,6 +3,6 @@ package duration
import "testing"
func TestDate(t *testing.T) {
res, _ := parseDuration("15h2m10s")
res, _ := ParseDuration("15h2m10s")
_ = res
}

View file

@ -1,76 +0,0 @@
package kv
import (
"os"
"path/filepath"
"time"
"github.com/mitchellh/go-homedir"
"github.com/tgdrive/teldrive/internal/config"
"github.com/tgdrive/teldrive/internal/utils"
"go.etcd.io/bbolt"
)
type Bolt struct {
bucket []byte
db *bbolt.DB
}
func (b *Bolt) Get(key string) ([]byte, error) {
var val []byte
if err := b.db.View(func(tx *bbolt.Tx) error {
val = tx.Bucket(b.bucket).Get([]byte(key))
return nil
}); err != nil {
return nil, err
}
if val == nil {
return nil, ErrNotFound
}
return val, nil
}
func (b *Bolt) Set(key string, val []byte) error {
return b.db.Update(func(tx *bbolt.Tx) error {
return tx.Bucket(b.bucket).Put([]byte(key), val)
})
}
func (b *Bolt) Delete(key string) error {
return b.db.Update(func(tx *bbolt.Tx) error {
return tx.Bucket(b.bucket).Delete([]byte(key))
})
}
func NewBoltKV(cnf *config.Config) KV {
sessionFile := cnf.TG.SessionFile
if sessionFile == "" {
dir, err := homedir.Dir()
if err != nil {
dir = utils.ExecutableDir()
} else {
dir = filepath.Join(dir, ".teldrive")
err := os.Mkdir(dir, 0755)
if err != nil && !os.IsExist(err) {
dir = utils.ExecutableDir()
}
}
sessionFile = filepath.Join(dir, "session.db")
}
boltDB, err := bbolt.Open(sessionFile, 0666, &bbolt.Options{
Timeout: time.Second,
NoGrowSync: false,
})
if err != nil {
panic(err)
}
kv, err := New(Options{Bucket: "teldrive", DB: boltDB})
if err != nil {
panic(err)
}
return kv
}

View file

@ -1,9 +0,0 @@
package kv
import (
"strings"
)
func Key(indexes ...string) string {
return strings.Join(indexes, ":")
}

View file

@ -1,32 +0,0 @@
package kv
import (
"errors"
"go.etcd.io/bbolt"
)
var ErrNotFound = errors.New("key not found")
type KV interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
Delete(key string) error
}
type Options struct {
Bucket string
DB *bbolt.DB
}
func New(opts Options) (KV, error) {
if err := opts.DB.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(opts.Bucket))
return err
}); err != nil {
return nil, err
}
return &Bolt{db: opts.DB, bucket: []byte(opts.Bucket)}, nil
}

View file

@ -1,41 +0,0 @@
package kv
import (
"context"
"errors"
"sync"
"github.com/gotd/td/telegram"
)
type Session struct {
kv KV
key string
mu sync.Mutex
}
func NewSession(kv KV, key string) telegram.SessionStorage {
return &Session{kv: kv, key: key}
}
func (s *Session) LoadSession(_ context.Context) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
b, err := s.kv.Get(s.key)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, nil
}
return nil, err
}
data := make([]byte, len(b))
copy(data, b)
return data, nil
}
func (s *Session) StoreSession(_ context.Context, data []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.kv.Set(s.key, data)
}

View file

@ -87,10 +87,11 @@ func (p *pool) Default(ctx context.Context) *tg.Client {
return p.Client(ctx, p.current())
}
func (p *pool) Close() (err error) {
func (p *pool) Close() error {
if p.close != nil {
return p.close()
}
return nil
}

View file

@ -1,142 +0,0 @@
package reader
import (
"context"
"crypto/rand"
"io"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/tgdrive/teldrive/internal/config"
)
type testChunkSource struct {
buffer []byte
}
func (m *testChunkSource) Chunk(ctx context.Context, offset int64, limit int64) ([]byte, error) {
return m.buffer[offset : offset+limit], nil
}
func (m *testChunkSource) ChunkSize(start, end int64) int64 {
return 1
}
type testChunkSourceTimeout struct {
buffer []byte
}
func (m *testChunkSourceTimeout) Chunk(ctx context.Context, offset int64, limit int64) ([]byte, error) {
if offset == 8 {
time.Sleep(2 * time.Second)
}
return m.buffer[offset : offset+limit], nil
}
func (m *testChunkSourceTimeout) ChunkSize(start, end int64) int64 {
return 1
}
type TestSuite struct {
suite.Suite
config *config.TGConfig
}
func (suite *TestSuite) SetupTest() {
suite.config = &config.TGConfig{Stream: struct {
MultiThreads int
Buffers int
ChunkTimeout time.Duration
}{MultiThreads: 8, Buffers: 10, ChunkTimeout: 1 * time.Second}}
}
func (suite *TestSuite) TestFullRead() {
ctx := context.Background()
start := int64(0)
end := int64(99)
data := make([]byte, 100)
rand.Read(data)
chunkSrc := &testChunkSource{buffer: data}
reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc)
assert.NoError(suite.T(), err)
test_data, err := io.ReadAll(reader)
assert.Equal(suite.T(), nil, err)
assert.Equal(suite.T(), data[start:end+1], test_data)
}
func (suite *TestSuite) TestPartialRead() {
ctx := context.Background()
start := int64(0)
end := int64(65)
data := make([]byte, 100)
rand.Read(data)
chunkSrc := &testChunkSource{buffer: data}
reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc)
assert.NoError(suite.T(), err)
test_data, err := io.ReadAll(reader)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), data[start:end+1], test_data)
}
func (suite *TestSuite) TestTimeout() {
ctx := context.Background()
start := int64(0)
end := int64(65)
data := make([]byte, 100)
rand.Read(data)
chunkSrc := &testChunkSourceTimeout{buffer: data}
reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc)
assert.NoError(suite.T(), err)
test_data, err := io.ReadAll(reader)
assert.Greater(suite.T(), len(test_data), 0)
assert.Equal(suite.T(), err, ErrStreamAbandoned)
}
func (suite *TestSuite) TestClose() {
ctx := context.Background()
start := int64(0)
end := int64(65)
data := make([]byte, 100)
rand.Read(data)
chunkSrc := &testChunkSource{buffer: data}
reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc)
assert.NoError(suite.T(), err)
_, err = io.ReadAll(reader)
assert.NoError(suite.T(), err)
assert.NoError(suite.T(), reader.Close())
}
func (suite *TestSuite) TestCancellation() {
ctx, cancel := context.WithCancel(context.Background())
start := int64(0)
end := int64(65)
data := make([]byte, 100)
rand.Read(data)
chunkSrc := &testChunkSource{buffer: data}
reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc)
assert.NoError(suite.T(), err)
cancel()
_, err = io.ReadAll(reader)
assert.Equal(suite.T(), err, context.Canceled)
assert.Equal(suite.T(), len(reader.bufferChan), 0)
}
func (suite *TestSuite) TestCancellationWithTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
_ = cancel
start := int64(0)
end := int64(65)
data := make([]byte, 100)
rand.Read(data)
chunkSrc := &testChunkSourceTimeout{buffer: data}
reader, err := newTGMultiReader(ctx, start, end, suite.config, chunkSrc)
assert.NoError(suite.T(), err)
_, err = io.ReadAll(reader)
assert.Equal(suite.T(), err, context.DeadlineExceeded)
assert.Equal(suite.T(), len(reader.bufferChan), 0)
}
func Test(t *testing.T) {
suite.Run(t, new(TestSuite))
}

36
internal/tgc/bolt.go Normal file
View file

@ -0,0 +1,36 @@
package tgc
import (
"os"
"path/filepath"
"time"
"github.com/mitchellh/go-homedir"
"github.com/tgdrive/teldrive/internal/utils"
"go.etcd.io/bbolt"
)
func NewBoltDB(sessionFile string) (*bbolt.DB, error) {
if sessionFile == "" {
dir, err := homedir.Dir()
if err != nil {
dir = utils.ExecutableDir()
} else {
dir = filepath.Join(dir, ".teldrive")
err := os.Mkdir(dir, 0755)
if err != nil && !os.IsExist(err) {
dir = utils.ExecutableDir()
}
}
sessionFile = filepath.Join(dir, "session.db")
}
db, err := bbolt.Open(sessionFile, 0666, &bbolt.Options{
Timeout: time.Second,
NoGrowSync: false,
})
if err != nil {
return nil, err
}
return db, nil
}

View file

@ -1,78 +0,0 @@
package tgc
import (
"context"
"errors"
"github.com/gotd/td/telegram"
)
type StopFunc func() error
type connectOptions struct {
ctx context.Context
token string
}
type Option interface {
apply(o *connectOptions)
}
type fnOption func(o *connectOptions)
func (f fnOption) apply(o *connectOptions) {
f(o)
}
func WithContext(ctx context.Context) Option {
return fnOption(func(o *connectOptions) {
o.ctx = ctx
})
}
func WithBotToken(token string) Option {
return fnOption(func(o *connectOptions) {
o.token = token
})
}
func Connect(client *telegram.Client, options ...Option) (StopFunc, error) {
opt := &connectOptions{
ctx: context.Background(),
}
for _, o := range options {
o.apply(opt)
}
ctx, cancel := context.WithCancel(opt.ctx)
errC := make(chan error, 1)
initDone := make(chan struct{})
go func() {
defer close(errC)
errC <- RunWithAuth(ctx, client, opt.token, func(ctx context.Context) error {
close(initDone)
<-ctx.Done()
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return ctx.Err()
})
}()
select {
case <-ctx.Done(): // context canceled
cancel()
return func() error { return nil }, ctx.Err()
case err := <-errC: // startup timeout
cancel()
return func() error { return nil }, err
case <-initDone: // init done
}
stopFn := func() error {
cancel()
return <-errC
}
return stopFn, nil
}

View file

@ -12,8 +12,8 @@ import (
"github.com/gotd/td/telegram"
"github.com/gotd/td/tg"
"github.com/tgdrive/teldrive/internal/config"
"github.com/tgdrive/teldrive/internal/kv"
"github.com/tgdrive/teldrive/pkg/types"
"go.etcd.io/bbolt"
"golang.org/x/sync/errgroup"
)
@ -100,7 +100,7 @@ func GetMessages(ctx context.Context, client *tg.Client, ids []int, channelId in
return nil, err
}
batchSize := 200
batchSize := 100
batchCount := int(math.Ceil(float64(len(ids)) / float64(batchSize)))
@ -184,10 +184,10 @@ func GetMediaContent(ctx context.Context, client *tg.Client, location tg.InputFi
return buff, nil
}
func GetBotInfo(ctx context.Context, KV kv.KV, config *config.TGConfig, token string) (*types.BotInfo, error) {
func GetBotInfo(ctx context.Context, boltdb *bbolt.DB, config *config.TGConfig, token string) (*types.BotInfo, error) {
var user *tg.User
middlewares := NewMiddleware(config, WithFloodWait(), WithRateLimit())
client, _ := BotClient(ctx, KV, config, token, middlewares...)
client, _ := BotClient(ctx, boltdb, config, token, middlewares...)
err := RunWithAuth(ctx, client, token, func(ctx context.Context) error {
user, _ = client.Self(ctx)
return nil

View file

@ -2,10 +2,12 @@ package tgc
import (
"context"
"strings"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/go-faster/errors"
tgbbolt "github.com/gotd/contrib/bbolt"
"github.com/gotd/contrib/clock"
"github.com/gotd/contrib/middleware/floodwait"
"github.com/gotd/contrib/middleware/ratelimit"
@ -13,17 +15,21 @@ import (
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/dcs"
"github.com/tgdrive/teldrive/internal/config"
"github.com/tgdrive/teldrive/internal/kv"
"github.com/tgdrive/teldrive/internal/logging"
"github.com/tgdrive/teldrive/internal/recovery"
"github.com/tgdrive/teldrive/internal/retry"
"github.com/tgdrive/teldrive/internal/utils"
"go.etcd.io/bbolt"
"go.uber.org/zap"
"golang.org/x/net/proxy"
"golang.org/x/time/rate"
)
func New(ctx context.Context, config *config.TGConfig, handler telegram.UpdateHandler, storage session.Storage, middlewares ...telegram.Middleware) (*telegram.Client, error) {
func sessionKey(indexes ...string) string {
return strings.Join(indexes, ":")
}
func newClient(ctx context.Context, config *config.TGConfig, handler telegram.UpdateHandler, storage session.Storage, middlewares ...telegram.Middleware) (*telegram.Client, error) {
var dialer dcs.DialFunc = proxy.Direct.DialContext
if config.Proxy != "" {
@ -80,7 +86,7 @@ func NoAuthClient(ctx context.Context, config *config.TGConfig, handler telegram
floodwait.NewSimpleWaiter(),
}
middlewares = append(middlewares, ratelimit.New(rate.Every(time.Millisecond*100), 5))
return New(ctx, config, handler, storage, middlewares...)
return newClient(ctx, config, handler, storage, middlewares...)
}
func AuthClient(ctx context.Context, config *config.TGConfig, sessionStr string, middlewares ...telegram.Middleware) (*telegram.Client, error) {
@ -98,14 +104,14 @@ func AuthClient(ctx context.Context, config *config.TGConfig, sessionStr string,
if err := loader.Save(context.TODO(), data); err != nil {
return nil, err
}
return New(ctx, config, nil, storage, middlewares...)
return newClient(ctx, config, nil, storage, middlewares...)
}
func BotClient(ctx context.Context, KV kv.KV, config *config.TGConfig, token string, middlewares ...telegram.Middleware) (*telegram.Client, error) {
func BotClient(ctx context.Context, boltdb *bbolt.DB, config *config.TGConfig, token string, middlewares ...telegram.Middleware) (*telegram.Client, error) {
storage := kv.NewSession(KV, kv.Key("botsession", token))
storage := tgbbolt.NewSessionStorage(boltdb, sessionKey("botsession", token), []byte("teldrive"))
return New(ctx, config, nil, storage, middlewares...)
return newClient(ctx, config, nil, storage, middlewares...)
}

View file

@ -4,12 +4,13 @@ import (
"context"
"os"
"os/signal"
"syscall"
"github.com/tgdrive/teldrive/cmd"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
defer cancel()
if err := cmd.New().ExecuteContext(ctx); err != nil {

View file

@ -37,11 +37,11 @@ type UploadResult struct {
type CronService struct {
db *gorm.DB
cnf *config.Config
cnf *config.ServerCmdConfig
logger *zap.SugaredLogger
}
func StartCronJobs(scheduler *gocron.Scheduler, db *gorm.DB, cnf *config.Config) {
func StartCronJobs(scheduler *gocron.Scheduler, db *gorm.DB, cnf *config.ServerCmdConfig) {
if !cnf.CronJobs.Enable {
return
}

View file

@ -7,12 +7,12 @@ import (
"github.com/go-faster/errors"
"github.com/gotd/td/telegram"
"github.com/ogen-go/ogen/ogenerrors"
"go.etcd.io/bbolt"
ht "github.com/ogen-go/ogen/http"
"github.com/tgdrive/teldrive/internal/api"
"github.com/tgdrive/teldrive/internal/cache"
"github.com/tgdrive/teldrive/internal/config"
"github.com/tgdrive/teldrive/internal/kv"
"github.com/tgdrive/teldrive/internal/tgc"
"github.com/tgdrive/teldrive/internal/version"
"gorm.io/gorm"
@ -20,9 +20,9 @@ import (
type apiService struct {
db *gorm.DB
cnf *config.Config
cnf *config.ServerCmdConfig
cache cache.Cacher
kv kv.KV
boltdb *bbolt.DB
worker *tgc.BotWorker
middlewares []telegram.Middleware
}
@ -53,11 +53,11 @@ func (a *apiService) NewError(ctx context.Context, err error) *api.ErrorStatusCo
}
func NewApiService(db *gorm.DB,
cnf *config.Config,
cnf *config.ServerCmdConfig,
cache cache.Cacher,
kv kv.KV,
boltdb *bbolt.DB,
worker *tgc.BotWorker) *apiService {
return &apiService{db: db, cnf: cnf, cache: cache, kv: kv, worker: worker,
return &apiService{db: db, cnf: cnf, cache: cache, boltdb: boltdb, worker: worker,
middlewares: tgc.NewMiddleware(&cnf.TG, tgc.WithFloodWait(), tgc.WithRateLimit())}
}

View file

@ -765,7 +765,7 @@ func (e *extendedService) FilesStream(w http.ResponseWriter, r *http.Request, fi
token, _ = e.api.worker.Next(file.ChannelId.Value)
client, err = tgc.BotClient(ctx, e.api.kv, &e.api.cnf.TG, token, middlewares...)
client, err = tgc.BotClient(ctx, e.api.boltdb, &e.api.cnf.TG, token, middlewares...)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View file

@ -120,7 +120,7 @@ func (a *apiService) UploadsUpload(ctx context.Context, req *api.UploadsUploadRe
} else {
a.worker.Set(tokens, channelId)
token, index = a.worker.Next(channelId)
client, err = tgc.BotClient(ctx, a.kv, &a.cnf.TG, token)
client, err = tgc.BotClient(ctx, a.boltdb, &a.cnf.TG, token)
if err != nil {
return nil, err

View file

@ -21,6 +21,8 @@ import (
"github.com/tgdrive/teldrive/pkg/types"
"golang.org/x/sync/errgroup"
tgbbolt "github.com/gotd/contrib/bbolt"
"github.com/gotd/contrib/storage"
"gorm.io/gorm/clause"
)
@ -44,36 +46,28 @@ func (a *apiService) UsersAddBots(ctx context.Context, req *api.AddBots) error {
}
func (a *apiService) UsersListChannels(ctx context.Context) ([]api.Channel, error) {
_, session := auth.GetUser(ctx)
client, err := tgc.AuthClient(ctx, &a.cnf.TG, session, a.middlewares...)
if err != nil {
return nil, &apiError{err: err}
}
if client == nil {
return nil, &apiError{err: errors.New("failed to initialise tg client")}
}
userID, _ := auth.GetUser(ctx)
channels := make(map[int64]*api.Channel)
client.Run(ctx, func(ctx context.Context) error {
peerStorage := tgbbolt.NewPeerStorage(a.boltdb, []byte(fmt.Sprintf("peers:%d", userID)))
dialogs, _ := query.GetDialogs(client.API()).BatchSize(100).Collect(ctx)
for _, dialog := range dialogs {
if !dialog.Deleted() {
for _, channel := range dialog.Entities.Channels() {
_, exists := channels[channel.ID]
if !exists && channel.AdminRights.AddAdmins {
channels[channel.ID] = &api.Channel{ChannelId: channel.ID, ChannelName: channel.Title}
}
}
iter, err := peerStorage.Iterate(ctx)
if err != nil {
return []api.Channel{}, nil
}
for iter.Next(ctx) {
peer := iter.Value()
if peer.Channel != nil && peer.Channel.AdminRights.AddAdmins {
_, exists := channels[peer.Channel.ID]
if !exists {
channels[peer.Channel.ID] = &api.Channel{ChannelId: peer.Channel.ID, ChannelName: peer.Channel.Title}
}
}
return nil
})
}
res := []api.Channel{}
for _, channel := range channels {
res = append(res, *channel)
@ -84,6 +78,23 @@ func (a *apiService) UsersListChannels(ctx context.Context) ([]api.Channel, erro
return res, nil
}
func (a *apiService) UsersSyncChannels(ctx context.Context) error {
userId, session := auth.GetUser(ctx)
peerStorage := tgbbolt.NewPeerStorage(a.boltdb, []byte(fmt.Sprintf("peers:%d", userId)))
collector := storage.CollectPeers(peerStorage)
client, err := tgc.AuthClient(ctx, &a.cnf.TG, session, a.middlewares...)
if err != nil {
return &apiError{err: err}
}
err = client.Run(ctx, func(ctx context.Context) error {
return collector.Dialogs(ctx, query.GetDialogs(client.API()).Iter())
})
if err != nil {
return &apiError{err: err}
}
return nil
}
func (a *apiService) UsersListSessions(ctx context.Context) ([]api.UserSession, error) {
userId, userSession := auth.GetUser(ctx)
@ -287,7 +298,7 @@ func (a *apiService) addBots(c context.Context, client *telegram.Client, userId
for _, token := range botsTokens {
g.Go(func() error {
info, err := tgc.GetBotInfo(c, a.kv, &a.cnf.TG, token)
info, err := tgc.GetBotInfo(c, a.boltdb, &a.cnf.TG, token)
if err != nil {
return err
}