feat: add new check command to check integrity of file parts

This commit is contained in:
Bhunter 2025-01-16 14:14:00 +01:00
parent a1875379a7
commit 0540ae1075
11 changed files with 498 additions and 49 deletions

413
cmd/check.go Normal file
View file

@ -0,0 +1,413 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/query"
"github.com/gotd/td/telegram/query/messages"
"github.com/gotd/td/tg"
"github.com/k0kubun/go-ansi"
"github.com/manifoldco/promptui"
"github.com/schollz/progressbar/v3"
"github.com/spf13/cobra"
"github.com/tgdrive/teldrive/internal/api"
"github.com/tgdrive/teldrive/internal/config"
"github.com/tgdrive/teldrive/internal/database"
"github.com/tgdrive/teldrive/internal/logging"
"github.com/tgdrive/teldrive/internal/tgc"
"github.com/tgdrive/teldrive/internal/utils"
"github.com/tgdrive/teldrive/pkg/models"
"golang.org/x/term"
"gorm.io/datatypes"
)
type channel struct {
tg.InputPeerChannel
ChannelName string
}
type file struct {
ID string
Name string
Parts datatypes.JSONSlice[api.Part]
}
type exportFile struct {
ID string `json:"id"`
Name string `json:"name"`
}
type channelExport struct {
ChannelID int64 `json:"channel_id"`
Timestamp string `json:"timestamp"`
FileCount int `json:"file_count"`
Files []exportFile `json:"files"`
}
const dateLayout = "2006-01-02_15-04-05"
var termWidth = func() (width int, err error) {
width, _, err = term.GetSize(int(os.Stdout.Fd()))
if err == nil {
return width, nil
}
return 0, err
}
func NewCheckCmd() *cobra.Command {
var cfg config.ServerCmdConfig
cmd := &cobra.Command{
Use: "check",
Short: "Check and purge incomplete files",
Run: func(cmd *cobra.Command, args []string) {
runCheckCmd(cmd, &cfg)
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
loader := config.NewConfigLoader()
if err := loader.InitializeConfig(cmd); err != nil {
return err
}
if err := loader.Load(&cfg); err != nil {
return err
}
if err := checkRequiredCheckFlags(&cfg); err != nil {
return err
}
return nil
},
}
addChecktFlags(cmd, &cfg)
return cmd
}
func addChecktFlags(cmd *cobra.Command, cfg *config.ServerCmdConfig) {
flags := cmd.Flags()
config.AddCommonFlags(flags, cfg)
flags.Bool("export", true, "Export incomplete files to json file")
flags.Bool("clean", false, "Clean missing and orphan file parts")
flags.String("user", "", "Telegram User Name")
}
func checkRequiredCheckFlags(cfg *config.ServerCmdConfig) error {
var missingFields []string
if cfg.DB.DataSource == "" {
missingFields = append(missingFields, "db-data-source")
}
if len(missingFields) > 0 {
return fmt.Errorf("required configuration values not set: %s", strings.Join(missingFields, ", "))
}
return nil
}
func selectUser(user string, users []models.User) (*models.User, error) {
if user != "" {
res := utils.Filter(users, func(u models.User) bool {
return u.UserName == user
})
if len(res) == 0 {
return nil, fmt.Errorf("invalid user name: %s", user)
}
return &res[0], nil
}
templates := &promptui.SelectTemplates{
Label: "{{ . }}",
Active: "{{ .UserName | cyan }}",
Inactive: "{{ .UserName | white }}",
Selected: "{{ .UserName | red | cyan }}",
}
prompt := promptui.Select{
Label: "Select User",
Items: users,
Templates: templates,
Size: 50,
}
index, _, err := prompt.Run()
if err != nil {
return nil, err
}
return &users[index], nil
}
func runCheckCmd(cmd *cobra.Command, cfg *config.ServerCmdConfig) {
lg := logging.DefaultLogger().Sugar()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
defer func() {
stop()
logging.DefaultLogger().Sync()
}()
db, err := database.NewDatabase(&cfg.DB, lg)
if err != nil {
lg.Fatalw("failed to create database", "err", err)
}
users := []models.User{}
if err := db.Model(&models.User{}).Find(&users).Error; err != nil {
lg.Fatalw("failed to get users", "err", err)
}
userName, _ := cmd.Flags().GetString("user")
user, err := selectUser(userName, users)
if err != nil {
lg.Fatalw("failed to select user", "err", err)
}
session := models.Session{}
if err := db.Model(&models.Session{}).Where("user_id = ?", user.UserId).Order("created_at desc").First(&session).Error; err != nil {
lg.Fatalw("failed to get session", "err", err)
}
channelIds := []int64{}
if err := db.Model(&models.Channel{}).Where("user_id = ?", user.UserId).Pluck("channel_id", &channelIds).Error; err != nil {
lg.Fatalw("failed to get channels", "err", err)
}
if len(channelIds) == 0 {
lg.Fatalw("no channels found")
}
tgconfig := &config.TGConfig{
RateLimit: true,
RateBurst: 5,
Rate: 100,
}
middlewares := tgc.NewMiddleware(tgconfig, tgc.WithFloodWait(), tgc.WithRateLimit())
export, _ := cmd.Flags().GetBool("export")
clean, _ := cmd.Flags().GetBool("clean")
var channelExports []channelExport
for _, id := range channelIds {
lg.Info("Processing channel: ", id)
const batchSize = 1000
var offset int
var files []file
for {
var batch []file
result := db.Model(&models.File{}).
Offset(offset).
Limit(batchSize).
Where("user_id = ?", user.UserId).
Where("channel_id = ?", id).
Where("type = ?", "file").
Scan(&batch)
if result.Error != nil {
lg.Errorw("failed to load files", "err", result.Error)
break
}
files = append(files, batch...)
if len(batch) < batchSize {
break
}
offset += batchSize
}
if len(files) == 0 {
continue
}
lg.Infof("Channel %d: %d files found", id, len(files))
lg.Infof("Loading messages from telegram")
client, err := tgc.AuthClient(ctx, tgconfig, session.Session, middlewares...)
if err != nil {
lg.Fatalw("failed to create client", "err", err)
}
msgs, total, err := loadChannelMessages(ctx, client, id)
if err != nil {
lg.Fatalw("failed to load channel messages", "err", err)
}
if total == 0 && len(msgs) == 0 {
lg.Infof("Channel %d: no messages found", id)
continue
}
if len(msgs) < total {
lg.Fatalf("Channel %d: found %d messages out of %d", id, len(msgs), total)
continue
}
uploadPartIds := []int{}
if err := db.Model(&models.Upload{}).Where("user_id = ?", user.UserId).Where("channel_id = ?", id).
Pluck("part_id", &uploadPartIds).Error; err != nil {
lg.Errorw("failed to get upload part ids", "err", err)
}
uploadPartMap := make(map[int]bool)
for _, partID := range uploadPartIds {
uploadPartMap[partID] = true
}
msgMap := make(map[int]bool)
for _, m := range msgs {
if m > 0 && !uploadPartMap[m] {
msgMap[m] = true
}
}
filesWithMissingParts := []file{}
allPartIDs := make(map[int]bool)
for _, f := range files {
for _, p := range f.Parts {
if p.ID == 0 {
filesWithMissingParts = append(filesWithMissingParts, f)
break
}
allPartIDs[p.ID] = true
}
}
if len(allPartIDs) == 0 {
continue
}
for _, f := range files {
for _, p := range f.Parts {
if !msgMap[p.ID] {
filesWithMissingParts = append(filesWithMissingParts, f)
break
}
}
}
missingMsgIDs := []int{}
for msgID := range msgMap {
if !allPartIDs[msgID] {
missingMsgIDs = append(missingMsgIDs, msgID)
}
}
if len(filesWithMissingParts) > 0 {
lg.Infof("Channel %d: found %d files with missing parts", id, len(filesWithMissingParts))
}
if export && len(filesWithMissingParts) > 0 {
channelData := channelExport{
ChannelID: id,
Timestamp: time.Now().Format(time.RFC3339),
FileCount: len(filesWithMissingParts),
Files: make([]exportFile, 0, len(filesWithMissingParts)),
}
for _, f := range filesWithMissingParts {
channelData.Files = append(channelData.Files, exportFile{
ID: f.ID,
Name: f.Name,
})
}
if clean {
err = db.Exec("call teldrive.delete_files_bulk($1 , $2)",
utils.Map(filesWithMissingParts, func(f file) string { return f.ID }), user.UserId).Error
if err != nil {
lg.Errorw("failed to delete files", "err", err)
}
}
channelExports = append(channelExports, channelData)
}
if clean && len(missingMsgIDs) > 0 {
lg.Infof("Channel %d: cleaning %d orphan messages", id, len(missingMsgIDs))
tgc.DeleteMessages(ctx, client, id, missingMsgIDs)
}
}
if len(channelExports) > 0 {
jsonData, err := json.MarshalIndent(channelExports, "", " ")
if err != nil {
lg.Errorw("failed to marshal JSON", "err", err)
return
}
err = os.WriteFile("missing_files.json", jsonData, 0644)
if err != nil {
lg.Errorw("failed to write JSON file", "err", err)
return
}
lg.Infof("Exported data to missing_files.json")
}
}
func loadChannelMessages(ctx context.Context, client *telegram.Client, channelId int64) (msgs []int, total int, err error) {
errChan := make(chan error, 1)
go func() {
errChan <- tgc.RunWithAuth(ctx, client, "", func(ctx context.Context) error {
channel, err := tgc.GetChannelById(ctx, client.API(), channelId)
if err != nil {
return err
}
count := 0
q := query.NewQuery(client.API()).Messages().GetHistory(&tg.InputPeerChannel{
ChannelID: channelId,
AccessHash: channel.AccessHash,
})
msgiter := messages.NewIterator(q, 100)
total, err = msgiter.Total(ctx)
if err != nil {
return fmt.Errorf("failed to get total messages: %w", err)
}
width, err := termWidth()
if err != nil {
width = 50
}
bar := progressbar.NewOptions(
total,
progressbar.OptionSetWriter(ansi.NewAnsiStdout()),
progressbar.OptionEnableColorCodes(true),
progressbar.OptionThrottle(65*time.Millisecond),
progressbar.OptionShowCount(),
progressbar.OptionSetWidth(width/3),
progressbar.OptionShowIts(),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "[green]=[reset]",
SaucerHead: "[green]>[reset]",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
)
defer bar.Clear()
for msgiter.Next(ctx) {
msg := msgiter.Value()
msgs = append(msgs, msg.Msg.GetID())
count++
bar.Set(count)
}
return nil
})
}()
select {
case err = <-errChan:
if err != nil {
return
}
case <-ctx.Done():
fmt.Print("\r\033[K")
err = ctx.Err()
}
return
}

View file

@ -12,6 +12,6 @@ func New() *cobra.Command {
cmd.Help()
},
}
cmd.AddCommand(NewRun(), NewVersion())
cmd.AddCommand(NewRun(), NewCheckCmd(), NewVersion())
return cmd
}

View file

@ -90,6 +90,17 @@ func addServerFlags(cmd *cobra.Command, cfg *config.ServerCmdConfig) {
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 config
flags.StringVar(&cfg.TG.StorageFile, "tg-storage-file", "", "Sqlite Storage file path")
flags.BoolVar(&cfg.TG.RateLimit, "tg-rate-limit", true, "Enable rate limiting for telegram client")
flags.IntVar(&cfg.TG.RateBurst, "tg-rate-burst", 5, "Limiting burst for telegram client")
flags.IntVar(&cfg.TG.Rate, "tg-rate", 100, "Limiting rate for telegram client")
flags.StringVar(&cfg.TG.Proxy, "tg-proxy", "", "HTTP OR SOCKS5 proxy URL")
flags.BoolVar(&cfg.TG.DisableStreamBots, "tg-disable-stream-bots", false, "Disable Stream bots")
flags.BoolVar(&cfg.TG.Ntp, "tg-ntp", false, "Use NTP server time")
flags.BoolVar(&cfg.TG.EnableLogging, "tg-enable-logging", false, "Enable telegram client logging")
flags.Int64Var(&cfg.TG.PoolSize, "tg-pool-size", 8, "Telegram Session pool size")
// 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")

7
go.mod
View file

@ -16,13 +16,17 @@ require (
github.com/gotd/contrib v0.21.0
github.com/gotd/td v0.117.0
github.com/iyear/connectproxy v0.1.1
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
github.com/manifoldco/promptui v0.9.0
github.com/ogen-go/ogen v1.8.1
github.com/redis/go-redis/v9 v9.7.0
github.com/schollz/progressbar/v3 v3.18.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.19.0
github.com/vmihailenco/msgpack/v5 v5.4.1
go.uber.org/zap v1.27.0
golang.org/x/term v0.28.0
golang.org/x/time v0.9.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/datatypes v1.2.5
@ -34,6 +38,7 @@ require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/beevik/ntp v1.4.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@ -52,10 +57,12 @@ require (
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/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect

23
go.sum
View file

@ -21,6 +21,14 @@ 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/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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=
@ -135,6 +143,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -148,16 +158,22 @@ 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/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
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=
@ -188,6 +204,8 @@ github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
@ -199,6 +217,8 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF
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/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
@ -259,9 +279,12 @@ 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-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=

View file

@ -149,6 +149,16 @@ func Fetch[T any](cache Cacher, key string, expiration time.Duration, fn func()
return value, nil
}
func FetchArg[T any, A any](
cache Cacher,
key string,
expiration time.Duration,
fn func(a A) (T, error), a A) (T, error) {
return Fetch(cache, key, expiration, func() (T, error) {
return fn(a)
})
}
func Key(args ...interface{}) string {
parts := make([]string, len(args))
for i, arg := range args {

View file

@ -208,23 +208,4 @@ func AddCommonFlags(flags *pflag.FlagSet, config *ServerCmdConfig) {
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.StorageFile, "tg-storage-file", "", "Sqlite Storage 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

@ -26,6 +26,20 @@ import (
"gorm.io/gorm"
)
const (
AppId = 2040
AppHash = "b18441a1ff607e10a989891a5462e627"
)
var Device = telegram.DeviceConfig{
DeviceModel: "Desktop",
SystemVersion: "Windows 11",
AppVersion: "5.10.3 x64",
LangCode: "en",
SystemLangCode: "en-US",
LangPack: "tdesktop",
}
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
@ -50,14 +64,7 @@ func newClient(ctx context.Context, config *config.TGConfig, handler telegram.Up
ReconnectionBackoff: func() backoff.BackOff {
return newBackoff(config.ReconnectTimeout)
},
Device: telegram.DeviceConfig{
DeviceModel: config.DeviceModel,
SystemVersion: config.SystemVersion,
AppVersion: config.AppVersion,
SystemLangCode: config.SystemLangCode,
LangPack: config.LangPack,
LangCode: config.LangCode,
},
Device: Device,
SessionStorage: storage,
RetryInterval: 2 * time.Second,
MaxRetries: 10,
@ -75,7 +82,7 @@ func newClient(ctx context.Context, config *config.TGConfig, handler telegram.Up
}
return telegram.NewClient(config.AppId, config.AppHash, opts), nil
return telegram.NewClient(AppId, AppHash, opts), nil
}
func NoAuthClient(ctx context.Context, config *config.TGConfig, handler telegram.UpdateHandler, storage session.Storage) (*telegram.Client, error) {

View file

@ -7,11 +7,13 @@ import (
"github.com/go-faster/errors"
"github.com/gotd/td/telegram"
"github.com/ogen-go/ogen/ogenerrors"
"go.uber.org/zap"
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/logging"
"github.com/tgdrive/teldrive/internal/tgc"
"github.com/tgdrive/teldrive/internal/version"
"gorm.io/gorm"
@ -45,8 +47,14 @@ func (a *apiService) NewError(ctx context.Context, err error) *api.ErrorStatusCo
code = ogenErr.Code()
message = ogenErr.Error()
case errors.As(err, &apiError):
code = apiError.Code()
message = apiError.Error()
if apiError.code == 0 {
code = http.StatusInternalServerError
message = http.StatusText(code)
} else {
code = apiError.code
message = apiError.Error()
}
logging.FromContext(ctx).Error("api error", zap.Error(apiError))
}
return &api.ErrorStatusCode{StatusCode: code, Response: api.Error{Code: code, Message: message}}
}
@ -108,13 +116,6 @@ func (a apiError) Error() string {
return a.err.Error()
}
func (a *apiError) Code() int {
if a.code == 0 {
return http.StatusInternalServerError
}
return a.code
}
func (a *apiError) Unwrap() error {
return a.err
}

View file

@ -285,7 +285,7 @@ func (a *apiService) FilesCreate(ctx context.Context, fileIn *api.File) (*api.Fi
if len(fileIn.Parts) > 0 {
fileDB.Parts = datatypes.NewJSONSlice(mapParts(fileIn.Parts))
}
fileDB.Size = utils.Ptr(fileIn.Size.Or(0))
fileDB.Size = utils.Ptr(fileIn.Size.Value)
}
fileDB.Name = fileIn.Name
fileDB.Type = string(fileIn.Type)

View file

@ -10,6 +10,7 @@ import (
"github.com/tgdrive/teldrive/internal/api"
"github.com/tgdrive/teldrive/internal/appcontext"
"github.com/tgdrive/teldrive/internal/cache"
"github.com/tgdrive/teldrive/internal/database"
"github.com/tgdrive/teldrive/pkg/mapper"
"github.com/tgdrive/teldrive/pkg/models"
@ -120,16 +121,10 @@ func (a *apiService) SharesListFiles(ctx context.Context, params api.SharesListF
}
func (a *apiService) validFileShare(r *http.Request, id string) (*fileShare, error) {
share := &fileShare{}
share, err := cache.FetchArg(a.cache, cache.Key("shares", id), 0, a.shareGetById, id)
key := "shares:" + id
if err := a.cache.Get(key, share); err != nil {
share, err = a.shareGetById(id)
if err != nil {
return nil, &apiError{err: err}
}
a.cache.Set(key, share, 0)
if err != nil {
return nil, &apiError{err: err}
}
if share.Password != nil {
@ -138,10 +133,11 @@ func (a *apiService) validFileShare(r *http.Request, id string) (*fileShare, err
return nil, &apiError{err: ErrEmptyAuth, code: http.StatusUnauthorized}
}
bytes, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic "))
password := strings.Split(string(bytes), ":")[1]
if err != nil {
return nil, &apiError{err: err}
}
password := strings.Split(string(bytes), ":")[1]
if err := bcrypt.CompareHashAndPassword([]byte(*share.Password), []byte(password)); err != nil {
return nil, &apiError{err: ErrInvalidPassword, code: http.StatusUnauthorized}
}