Compare commits

...

22 Commits

Author SHA1 Message Date
krau
ec09289d5f feat: enhance message formatting with entity support and improve command registration 2025-02-15 17:20:16 +08:00
krau
13c87debcc fix: add error handling for message retrieval in saveCmd function 2025-02-15 16:26:12 +08:00
krau
5f3b38c788 feat: add /path command to change file save path and improve configuration handling 2025-02-15 16:25:16 +08:00
krau
8ba0c623c9 chore: update dependencies for github.com/gotd/td to v0.120.0 and modernc.org/sqlite to v1.35.0 2025-02-15 15:11:58 +08:00
krau
6fa8e89191 feat: update download task message to include detailed progress information 2025-02-15 15:10:52 +08:00
krau
3a4effab33 feat: refactor file processing and storage handling with improved path management 2025-02-15 15:06:06 +08:00
krau
7692286d78 docs: update docker upgrade cmd 2025-02-12 13:49:36 +08:00
krau
93ffc940ce docs: add docker deploy 2025-02-12 13:47:25 +08:00
krau
4aadfc1273 feat: add docker-compose 2025-02-12 13:41:02 +08:00
krau
d26a8df15f feat: add default telegram api id and hash 2025-02-12 13:40:49 +08:00
krau
a746cc0fc7 feat: add cache cleaning functionality and configuration option 2025-02-12 12:24:15 +08:00
krau
0fb5634874 feat: support custom storage path in filename 2025-02-12 12:24:11 +08:00
krau
930e838b2e feat: add custom file name support for saved files and improve error messages 2025-02-12 12:02:28 +08:00
krau
1701d1ab86 refactor: improve error handling for empty file, document, and photo cases 2025-02-12 11:13:55 +08:00
krau
a17492d4ae feat(task): enhance download progress reporting and add speed calculation 2025-02-12 11:06:25 +08:00
krau
a32bf43cdc fix(webdav): replace filepath with path for directory creation 2025-02-12 10:29:12 +08:00
krau
a25a58f8a2 refactor(alist): replace req.Client with http.Client and improve error handling 2025-02-01 17:01:46 +08:00
krau
804a86cbdd fix: close file 2025-02-01 16:00:56 +08:00
krau
cf1e9299c0 chore: upgrade deps 2025-02-01 16:00:50 +08:00
krau
e3f7380341 fix(alist): use filebytes to upload file 2025-02-01 14:46:10 +08:00
krau
6c6ee77067 fix: alist error resp 2025-02-01 14:36:08 +08:00
krau
f00aa189e3 docs: 添加 systemd 服务配置说明到 README 2025-01-22 22:01:16 +08:00
19 changed files with 655 additions and 233 deletions

View File

@@ -18,6 +18,8 @@ Demo Video:
## 部署 ## 部署
### 从二进制文件部署
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件. 在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
在解压后目录新建 `config.toml` 文件, 参考 [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件. 在解压后目录新建 `config.toml` 文件, 参考 [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
@@ -29,6 +31,52 @@ chmod +x saveany-bot
./saveany-bot ./saveany-bot
``` ```
#### 添加为 systemd 服务
创建文件 `/etc/systemd/system/saveany-bot.service` 并写入以下内容:
```
[Unit]
Description=SaveAnyBot
After=systemd-user-sessions.service
[Service]
Type=simple
WorkingDirectory=/yourpath/
ExecStart=/yourpath/saveany-bot
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
设为开机启动并启动服务:
```bash
systemctl enable --now saveany-bot
```
### 使用 Docker 部署
#### Docker Compose
下载 [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) 文件, 并修改其中的配置.
运行:
```bash
docker compose up -d
```
#### Docker
```shell
docker run -d --name saveany-bot \
-v /path/to/config.toml:/app/config.toml \
-v /path/to/downloads:/app/downloads \
ghcr.io/krau/saveany-bot:latest
```
## 更新 ## 更新
使用 `upgrade``up` 升级到最新版 使用 `upgrade``up` 升级到最新版
@@ -37,6 +85,13 @@ chmod +x saveany-bot
./saveany-bot upgrade ./saveany-bot upgrade
``` ```
如果是 Docker 部署, 使用以下命令更新:
```bash
docker pull ghcr.io/krau/saveany-bot:latest
docker restart saveany-bot
```
## 使用 ## 使用
向 Bot 发送(转发)文件, 按照提示操作. 向 Bot 发送(转发)文件, 按照提示操作.

View File

@@ -10,6 +10,7 @@ import (
"github.com/celestix/gotgproto/sessionMaker" "github.com/celestix/gotgproto/sessionMaker"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/gotd/td/telegram/dcs" "github.com/gotd/td/telegram/dcs"
"github.com/gotd/td/tg"
"github.com/krau/SaveAny-Bot/config" "github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/logger" "github.com/krau/SaveAny-Bot/logger"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
@@ -60,6 +61,24 @@ func Init() {
Resolver: resolver, Resolver: resolver,
}, },
) )
if err != nil {
resultChan <- struct {
client *gotgproto.Client
err error
}{nil, err}
return
}
_, err = client.API().BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{
Scope: &tg.BotCommandScopeDefault{},
Commands: []tg.BotCommand{
{Command: "start", Description: "开始使用"},
{Command: "help", Description: "显示帮助"},
{Command: "silent", Description: "开启/关闭静默模式"},
{Command: "storage", Description: "设置默认存储端"},
{Command: "save", Description: "保存所回复的文件"},
{Command: "path", Description: "更改保存路径配置"},
},
})
resultChan <- struct { resultChan <- struct {
client *gotgproto.Client client *gotgproto.Client
err error err error

View File

@@ -1,12 +1,14 @@
package bot package bot
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/duke-git/lancet/v2/slice" "github.com/duke-git/lancet/v2/slice"
"github.com/gookit/goutil/maputil" "github.com/gookit/goutil/maputil"
"github.com/gotd/td/telegram/message/entity"
"github.com/gotd/td/telegram/message/styling" "github.com/gotd/td/telegram/message/styling"
"github.com/gotd/td/tg" "github.com/gotd/td/tg"
@@ -29,6 +31,7 @@ func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
dispatcher.AddHandler(handlers.NewCommand("silent", silent)) dispatcher.AddHandler(handlers.NewCommand("silent", silent))
dispatcher.AddHandler(handlers.NewCommand("storage", setDefaultStorage)) dispatcher.AddHandler(handlers.NewCommand("storage", setDefaultStorage))
dispatcher.AddHandler(handlers.NewCommand("save", saveCmd)) dispatcher.AddHandler(handlers.NewCommand("save", saveCmd))
dispatcher.AddHandler(handlers.NewCommand("path", setPath))
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("add"), AddToQueue)) dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("add"), AddToQueue))
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage)) dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
} }
@@ -56,13 +59,14 @@ func start(ctx *ext.Context, update *ext.Update) error {
} }
const helpText string = ` const helpText string = `
SaveAny Bot - 转存你的 Telegram 文件 Save Any Bot - 转存你的 Telegram 文件
命令: 命令:
/start - 开始使用 /start - 开始使用
/help - 显示帮助 /help - 显示帮助
/silent - 静默模式 /silent - 静默模式
/storage - 设置默认存储位置 /storage - 设置默认存储位置
/save - 保存文件 /save [自定义文件名] - 保存文件
/path <存储类型> <路径> - 更改文件保存路径
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问 静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
` `
@@ -83,12 +87,7 @@ func silent(ctx *ext.Context, update *ext.Update) error {
logger.L.Errorf("Failed to update user: %s", err) logger.L.Errorf("Failed to update user: %s", err)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s静默模式", func() string { ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s静默模式", map[bool]string{true: "开启", false: "关闭"}[user.Silent])), nil)
if user.Silent {
return "开启"
}
return "关闭"
}())), nil)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
@@ -146,7 +145,13 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil) ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
msg, err := GetTGMessage(ctx, Client, replyToMsgID) msg, err := GetTGMessage(ctx, Client, replyToMsgID)
if err != nil {
logger.L.Errorf("Failed to get message: %s", err)
ctx.Reply(update, ext.ReplyTextString("无法获取消息"), nil)
return dispatcher.EndGroups
}
supported, _ := supportedMediaFilter(msg) supported, _ := supportedMediaFilter(msg)
if !supported { if !supported {
@@ -165,7 +170,11 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
logger.L.Errorf("Failed to reply: %s", err) logger.L.Errorf("Failed to reply: %s", err)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
file, err := FileFromMessage(ctx, Client, update.EffectiveChat().GetID(), msg.ID)
cmdText := update.EffectiveMessage.Text
customFileName := strings.TrimSpace(strings.TrimPrefix(cmdText, "/save"))
file, err := FileFromMessage(ctx, Client, update.EffectiveChat().GetID(), msg.ID, customFileName)
if err != nil { if err != nil {
logger.L.Errorf("Failed to get file from message: %s", err) logger.L.Errorf("Failed to get file from message: %s", err)
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
@@ -183,16 +192,18 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
return dispatcher.EndGroups return dispatcher.EndGroups
} }
if err := dao.AddReceivedFile(&types.ReceivedFile{ receivedFile := &types.ReceivedFile{
Processing: false, Processing: false,
FileName: file.FileName, FileName: file.FileName,
ChatID: update.EffectiveChat().GetID(), ChatID: update.EffectiveChat().GetID(),
MessageID: replyToMsgID, MessageID: replyToMsgID,
ReplyMessageID: replied.ID, ReplyMessageID: replied.ID,
}); err != nil { }
logger.L.Errorf("Failed to add received file: %s", err)
if err := dao.SaveReceivedFile(receivedFile); err != nil {
logger.L.Errorf("Failed to save received file: %s", err)
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
Message: "无法保存文件", Message: fmt.Sprintf("Failed to save received file: %s", err),
ID: replied.ID, ID: replied.ID,
}); err != nil { }); err != nil {
logger.L.Errorf("Failed to edit message: %s", err) logger.L.Errorf("Failed to edit message: %s", err)
@@ -201,9 +212,21 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
} }
if !user.Silent { if !user.Silent {
text := "请选择存储位置" entityBuilder := entity.Builder{}
var entities []tg.MessageEntityClass
text := fmt.Sprintf("文件名: %s\n请选择存储位置", file.FileName)
if err := styling.Perform(&entityBuilder,
styling.Plain("文件名: "),
styling.Code(file.FileName),
styling.Plain("\n请选择存储位置"),
); err != nil {
logger.L.Errorf("Failed to build entity: %s", err)
} else {
text, entities = entityBuilder.Complete()
}
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ _, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
Message: text, Message: text,
Entities: entities,
ReplyMarkup: getAddTaskMarkup(msg.ID), ReplyMarkup: getAddTaskMarkup(msg.ID),
ID: replied.ID, ID: replied.ID,
}) })
@@ -236,6 +259,51 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
return dispatcher.EndGroups return dispatcher.EndGroups
} }
func setPath(ctx *ext.Context, update *ext.Update) error {
if len(storage.Storages) == 0 {
ctx.Reply(update, ext.ReplyTextString("未配置存储"), nil)
return dispatcher.EndGroups
}
if update.EffectiveMessage == nil {
logger.L.Error("No effective message")
return dispatcher.EndGroups
}
args := strings.Split(update.EffectiveMessage.Text, " ")
if len(args) < 3 {
text := []styling.StyledTextOption{
styling.Plain("请提供存储位置名称和路径, 可用项:"),
}
for name := range storage.Storages {
text = append(text, styling.Plain("\n"))
text = append(text, styling.Code(string(name)))
}
text = append(text, styling.Plain("\n示例: /path local /path/to/save"))
ctx.Reply(update, ext.ReplyTextStyledTextArray(text), nil)
return dispatcher.EndGroups
}
storageName := args[1]
if _, ok := storage.Storages[types.StorageType(storageName)]; !ok {
ctx.Reply(update, ext.ReplyTextString("存储位置不存在"), nil)
return dispatcher.EndGroups
}
path := strings.Join(args[2:], " ")
switch storageName {
case "local":
config.Set("storage.local.base_path", path)
case "webdav":
config.Set("storage.webdav.base_path", path)
case "alist":
config.Set("storage.alist.base_path", path)
}
if err := config.ReloadConfig(); err != nil {
logger.L.Errorf("Failed to reload config: %s", err)
ctx.Reply(update, ext.ReplyTextString("设置失败: "+err.Error()), nil)
return dispatcher.EndGroups
}
ctx.Reply(update, ext.ReplyTextString("设置成功"), nil)
return dispatcher.EndGroups
}
func handleFileMessage(ctx *ext.Context, update *ext.Update) error { func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
logger.L.Trace("Got media: ", update.EffectiveMessage.Media.TypeName()) logger.L.Trace("Got media: ", update.EffectiveMessage.Media.TypeName())
supported, err := supportedMediaFilter(update.EffectiveMessage.Message) supported, err := supportedMediaFilter(update.EffectiveMessage.Message)
@@ -258,10 +326,14 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
return dispatcher.EndGroups return dispatcher.EndGroups
} }
media := update.EffectiveMessage.Media media := update.EffectiveMessage.Media
file, err := FileFromMedia(media) file, err := FileFromMedia(media, "")
if err != nil { if err != nil {
logger.L.Errorf("Failed to get file from media: %s", err) logger.L.Errorf("Failed to get file from media: %s", err)
ctx.Reply(update, ext.ReplyTextString("无法获取文件"), nil) if errors.Is(err, ErrEmptyFileName) {
ctx.Reply(update, ext.ReplyTextString("无法获取文件名, 请使用 /save <自定义文件名> 回复此文件"), nil)
} else {
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
}
return dispatcher.EndGroups return dispatcher.EndGroups
} }
if file.FileName == "" { if file.FileName == "" {
@@ -269,7 +341,7 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
return dispatcher.EndGroups return dispatcher.EndGroups
} }
if err := dao.AddReceivedFile(&types.ReceivedFile{ if err := dao.SaveReceivedFile(&types.ReceivedFile{
Processing: false, Processing: false,
FileName: file.FileName, FileName: file.FileName,
ChatID: update.EffectiveChat().GetID(), ChatID: update.EffectiveChat().GetID(),
@@ -278,19 +350,30 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
}); err != nil { }); err != nil {
logger.L.Errorf("Failed to add received file: %s", err) logger.L.Errorf("Failed to add received file: %s", err)
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
Message: "无法保存文件", Message: fmt.Sprintf("Failed to add received file: %s", err),
ID: msg.ID, ID: msg.ID,
}); err != nil { }); err != nil {
logger.L.Errorf("Failed to edit message: %s", err) logger.L.Errorf("Failed to edit message: %s", err)
} }
return dispatcher.EndGroups return dispatcher.EndGroups
} }
if !user.Silent { if !user.Silent {
text := "请选择存储位置" entityBuilder := entity.Builder{}
var entities []tg.MessageEntityClass
text := fmt.Sprintf("文件名: %s\n请选择存储位置", file.FileName)
if err := styling.Perform(&entityBuilder,
styling.Plain("文件名: "),
styling.Code(file.FileName),
styling.Plain("\n请选择存储位置"),
); err != nil {
logger.L.Errorf("Failed to build entity: %s", err)
} else {
text, entities = entityBuilder.Complete()
}
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ _, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
Message: text, Message: text,
Entities: entities,
ReplyMarkup: getAddTaskMarkup(update.EffectiveMessage.ID), ReplyMarkup: getAddTaskMarkup(update.EffectiveMessage.ID),
ID: msg.ID, ID: msg.ID,
}) })
@@ -351,18 +434,18 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
} }
if update.CallbackQuery.MsgID != record.ReplyMessageID { if update.CallbackQuery.MsgID != record.ReplyMessageID {
record.ReplyMessageID = update.CallbackQuery.MsgID record.ReplyMessageID = update.CallbackQuery.MsgID
if err := dao.UpdateReceivedFile(record); err != nil { if err := dao.SaveReceivedFile(record); err != nil {
logger.L.Errorf("Failed to update received file: %s", err) logger.L.Errorf("Failed to update received file: %s", err)
} }
} }
file, err := FileFromMessage(ctx, Client, record.ChatID, record.MessageID) file, err := FileFromMessage(ctx, Client, record.ChatID, record.MessageID, record.FileName)
if err != nil { if err != nil {
logger.L.Errorf("Failed to get file from message: %s", err) logger.L.Errorf("Failed to get file from message: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
Message: "获取消息文件失败", Message: fmt.Sprintf("获取消息中的文件失败: %s", err),
CacheTime: 5, CacheTime: 5,
}) })
return dispatcher.EndGroups return dispatcher.EndGroups
@@ -377,9 +460,25 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
ReplyMessageID: record.ReplyMessageID, ReplyMessageID: record.ReplyMessageID,
MessageID: record.MessageID, MessageID: record.MessageID,
}) })
entityBuilder := entity.Builder{}
var entities []tg.MessageEntityClass
text := fmt.Sprintf("已添加到任务队列\n文件名: %s\n当前排队任务数: %d", record.FileName, queue.Len())
if err := styling.Perform(&entityBuilder,
styling.Plain("已添加到任务队列\n文件名: "),
styling.Code(record.FileName),
styling.Plain("\n当前排队任务数: "),
styling.Bold(strconv.Itoa(queue.Len())),
); err != nil {
logger.L.Errorf("Failed to build entity: %s", err)
} else {
text, entities = entityBuilder.Complete()
}
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", record.FileName, queue.Len()), Message: text,
ID: record.ReplyMessageID, Entities: entities,
ID: record.ReplyMessageID,
}) })
return dispatcher.EndGroups return dispatcher.EndGroups
} }

View File

@@ -2,7 +2,7 @@ package bot
import ( import (
"context" "context"
"crypto/md5" "errors"
"fmt" "fmt"
"time" "time"
@@ -15,6 +15,14 @@ import (
"github.com/krau/SaveAny-Bot/types" "github.com/krau/SaveAny-Bot/types"
) )
var (
ErrEmptyFileName = errors.New("file name is empty")
ErrEmptyDocument = errors.New("document is empty")
ErrEmptyPhoto = errors.New("photo is empty")
ErrEmptyPhotoSize = errors.New("photo size is empty")
ErrEmptyPhotoSizes = errors.New("photo size slice is empty")
)
func supportedMediaFilter(m *tg.Message) (bool, error) { func supportedMediaFilter(m *tg.Message) (bool, error) {
if not := m.Media == nil; not { if not := m.Media == nil; not {
return false, dispatcher.EndGroups return false, dispatcher.EndGroups
@@ -74,14 +82,21 @@ func getAddTaskMarkup(messageID int) *tg.ReplyInlineMarkup {
} }
} }
func FileFromMedia(media tg.MessageMediaClass) (*types.File, error) { func FileFromMedia(media tg.MessageMediaClass, customFileName string) (*types.File, error) {
switch media := media.(type) { switch media := media.(type) {
case *tg.MessageMediaDocument: case *tg.MessageMediaDocument:
document, ok := media.Document.AsNotEmpty() document, ok := media.Document.AsNotEmpty()
if !ok { if !ok {
return nil, fmt.Errorf("document is empty") return nil, ErrEmptyDocument
} }
var fileName string if customFileName != "" {
return &types.File{
Location: document.AsInputDocumentFileLocation(),
FileSize: document.Size,
FileName: customFileName,
}, nil
}
fileName := ""
for _, attribute := range document.Attributes { for _, attribute := range document.Attributes {
if name, ok := attribute.(*tg.DocumentAttributeFilename); ok { if name, ok := attribute.(*tg.DocumentAttributeFilename); ok {
fileName = name.GetFileName() fileName = name.GetFileName()
@@ -89,8 +104,7 @@ func FileFromMedia(media tg.MessageMediaClass) (*types.File, error) {
} }
} }
if fileName == "" { if fileName == "" {
fileName = fmt.Sprintf("%x", md5.Sum(document.GetFileReference())) return nil, ErrEmptyFileName
logger.L.Warnf("File name is empty, using hash: %s", fileName)
} }
return &types.File{ return &types.File{
Location: document.AsInputDocumentFileLocation(), Location: document.AsInputDocumentFileLocation(),
@@ -100,33 +114,37 @@ func FileFromMedia(media tg.MessageMediaClass) (*types.File, error) {
case *tg.MessageMediaPhoto: case *tg.MessageMediaPhoto:
photo, ok := media.Photo.AsNotEmpty() photo, ok := media.Photo.AsNotEmpty()
if !ok { if !ok {
return nil, fmt.Errorf("photo is empty") return nil, ErrEmptyPhoto
} }
sizes := photo.Sizes sizes := photo.Sizes
if len(sizes) == 0 { if len(sizes) == 0 {
return nil, fmt.Errorf("photo sizes is empty") return nil, ErrEmptyPhotoSizes
} }
photoSize := sizes[len(sizes)-1] photoSize := sizes[len(sizes)-1]
size, ok := photoSize.AsNotEmpty() size, ok := photoSize.AsNotEmpty()
if !ok { if !ok {
return nil, fmt.Errorf("photo size is empty") return nil, ErrEmptyPhotoSize
} }
location := new(tg.InputPhotoFileLocation) location := new(tg.InputPhotoFileLocation)
location.ID = photo.GetID() location.ID = photo.GetID()
location.AccessHash = photo.GetAccessHash() location.AccessHash = photo.GetAccessHash()
location.FileReference = photo.GetFileReference() location.FileReference = photo.GetFileReference()
location.ThumbSize = size.GetType() location.ThumbSize = size.GetType()
fileName := customFileName
if fileName == "" {
fileName = fmt.Sprintf("photo_%s_%d.jpg", time.Now().Format("2006-01-02_15-04-05"), photo.GetID())
}
return &types.File{ return &types.File{
Location: location, Location: location,
FileSize: 0, FileSize: 0,
FileName: fmt.Sprintf("photo_%s_%d.jpg", time.Now().Format("2006-01-02_15-04-05"), photo.GetID()), FileName: fileName,
}, nil }, nil
} }
return nil, fmt.Errorf("unexpected type %T", media) return nil, fmt.Errorf("unexpected type %T", media)
} }
func FileFromMessage(ctx context.Context, client *gotgproto.Client, chatID int64, messageID int) (*types.File, error) { func FileFromMessage(ctx context.Context, client *gotgproto.Client, chatID int64, messageID int, customFileName string) (*types.File, error) {
key := fmt.Sprintf("file:%d:%d", chatID, messageID) key := fmt.Sprintf("file:%d:%d", chatID, messageID)
logger.L.Debugf("Getting file: %s", key) logger.L.Debugf("Getting file: %s", key)
var cachedFile types.File var cachedFile types.File
@@ -139,7 +157,7 @@ func FileFromMessage(ctx context.Context, client *gotgproto.Client, chatID int64
if err != nil { if err != nil {
return nil, err return nil, err
} }
file, err := FileFromMedia(message.Media) file, err := FileFromMedia(message.Media, customFileName)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -3,9 +3,11 @@ package cmd
import ( import (
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"syscall" "syscall"
"github.com/krau/SaveAny-Bot/bootstrap" "github.com/krau/SaveAny-Bot/bootstrap"
"github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/core" "github.com/krau/SaveAny-Bot/core"
"github.com/krau/SaveAny-Bot/logger" "github.com/krau/SaveAny-Bot/logger"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -18,5 +20,32 @@ func Run(_ *cobra.Command, _ []string) {
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit sig := <-quit
logger.L.Info(sig, "exit") logger.L.Info(sig, ", exitting...")
defer logger.L.Info("Bye!")
if config.Cfg.NoCleanCache {
return
}
if config.Cfg.Temp.BasePath != "" {
for _, path := range []string{"/", ".", "\\", ".."} {
if filepath.Clean(config.Cfg.Temp.BasePath) == path {
logger.L.Error("Invalid cache dir: ", config.Cfg.Temp.BasePath)
return
}
}
currentDir, err := os.Getwd()
if err != nil {
logger.L.Error("Failed to get current dir: ", err)
return
}
cachePath := filepath.Join(currentDir, config.Cfg.Temp.BasePath)
cachePath, err = filepath.Abs(cachePath)
if err != nil {
logger.L.Error("Failed to get absolute path: ", err)
return
}
logger.L.Info("Cleaning cache dir: ", cachePath)
if err := os.RemoveAll(cachePath); err != nil {
logger.L.Error("Failed to clean cache dir: ", err)
}
}
} }

View File

@@ -9,8 +9,9 @@ import (
) )
type Config struct { type Config struct {
Workers int `toml:"workers" mapstructure:"workers"` Workers int `toml:"workers" mapstructure:"workers"`
Retry int `toml:"retry" mapstructure:"retry"` Retry int `toml:"retry" mapstructure:"retry"`
NoCleanCache bool `toml:"no_clean_cache" mapstructure:"no_clean_cache"`
Temp tempConfig `toml:"temp" mapstructure:"temp"` Temp tempConfig `toml:"temp" mapstructure:"temp"`
Log logConfig `toml:"log" mapstructure:"log"` Log logConfig `toml:"log" mapstructure:"log"`
@@ -90,6 +91,9 @@ func Init() {
viper.SetDefault("workers", 3) viper.SetDefault("workers", 3)
viper.SetDefault("retry", 3) viper.SetDefault("retry", 3)
viper.SetDefault("telegram.app_id", 1025907)
viper.SetDefault("telegram.app_hash", "452b0359b988148995f22ff0f4229750")
viper.SetDefault("temp.base_path", "cache/") viper.SetDefault("temp.base_path", "cache/")
viper.SetDefault("temp.cache_ttl", 3600) viper.SetDefault("temp.cache_ttl", 3600)
@@ -117,3 +121,20 @@ func Init() {
os.Exit(1) os.Exit(1)
} }
} }
func Set(key string, value any) {
viper.Set(key, value)
}
func ReloadConfig() error {
if err := viper.WriteConfig(); err != nil {
return err
}
if err := viper.ReadInConfig(); err != nil {
return err
}
if error := viper.Unmarshal(Cfg); error != nil {
return error
}
return nil
}

View File

@@ -6,9 +6,12 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"time"
"github.com/celestix/gotgproto/ext" "github.com/celestix/gotgproto/ext"
"github.com/duke-git/lancet/v2/fileutil"
"github.com/gotd/td/tg" "github.com/gotd/td/tg"
"github.com/krau/SaveAny-Bot/bot" "github.com/krau/SaveAny-Bot/bot"
"github.com/krau/SaveAny-Bot/config" "github.com/krau/SaveAny-Bot/config"
@@ -19,46 +22,33 @@ import (
func processPendingTask(task *types.Task) error { func processPendingTask(task *types.Task) error {
logger.L.Debugf("Start processing task: %s", task.String()) logger.L.Debugf("Start processing task: %s", task.String())
os.MkdirAll(config.Cfg.Temp.BasePath, os.ModePerm) cacheDestPath := filepath.Join(config.Cfg.Temp.BasePath, task.FileName())
cacheDestPath, err := filepath.Abs(cacheDestPath)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
if err := fileutil.CreateDir(filepath.Dir(cacheDestPath)); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
ctx := task.Ctx.(*ext.Context)
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
Message: "正在下载: " + task.String(),
ID: task.ReplyMessageID,
})
destPath := filepath.Join(config.Cfg.Temp.BasePath, task.File.FileName)
if task.StoragePath == "" { if task.StoragePath == "" {
task.StoragePath = task.File.FileName task.StoragePath = task.File.FileName
} }
switch task.Storage {
// process photo case types.Local:
if task.File.FileSize == 0 { task.StoragePath = filepath.Join(config.Cfg.Storage.Local.BasePath, task.StoragePath)
res, err := bot.Client.API().UploadGetFile(task.Ctx, &tg.UploadGetFileRequest{ case types.Webdav:
Location: task.File.Location, task.StoragePath = path.Join(config.Cfg.Storage.Webdav.BasePath, task.StoragePath)
Offset: 0, case types.Alist:
Limit: 1024 * 1024, task.StoragePath = path.Join(config.Cfg.Storage.Alist.BasePath, task.StoragePath)
})
if err != nil {
return fmt.Errorf("Failed to get file: %w", err)
}
result, ok := res.(*tg.UploadFile)
if !ok {
return fmt.Errorf("unexpected type %T", res)
}
if err := os.WriteFile(destPath, result.Bytes, os.ModePerm); err != nil {
return fmt.Errorf("Failed to write file: %w", err)
}
defer cleanCacheFile(destPath)
logger.L.Infof("Downloaded file: %s", destPath)
return saveFileWithRetry(task, destPath)
} }
if task.File.FileSize == 0 {
return processPhoto(task, cacheDestPath)
}
ctx := task.Ctx.(*ext.Context)
barTotalCount := calculateBarTotalCount(task.File.FileSize) barTotalCount := calculateBarTotalCount(task.File.FileSize)
progressCallback := func(bytesRead, contentLength int64) { progressCallback := func(bytesRead, contentLength int64) {
@@ -67,44 +57,47 @@ func processPendingTask(task *types.Task) error {
if task.File.FileSize < 1024*1024*50 || int(progress)%(100/barTotalCount) != 0 { if task.File.FileSize < 1024*1024*50 || int(progress)%(100/barTotalCount) != 0 {
return return
} }
text := fmt.Sprintf("正在下载: %s\n[%s] %.2f%%", text, entities := buildProgressMessageEntity(task, barTotalCount, bytesRead, task.StartTime, progress)
task.String(),
getProgressBar(progress, barTotalCount),
progress,
)
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{ ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
Message: text, Message: text,
ID: task.ReplyMessageID, Entities: entities,
ID: task.ReplyMessageID,
}) })
} }
text, entities := buildProgressMessageEntity(task, barTotalCount, 0, task.StartTime, 0)
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
Message: text,
Entities: entities,
ID: task.ReplyMessageID,
})
readCloser, err := NewTelegramReader(task.Ctx, bot.Client, &task.File.Location, readCloser, err := NewTelegramReader(task.Ctx, bot.Client, &task.File.Location,
0, task.File.FileSize-1, task.File.FileSize, 0, task.File.FileSize-1, task.File.FileSize,
progressCallback, task.File.FileSize/100) progressCallback, task.File.FileSize/100)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create reader: %w", err) return fmt.Errorf("failed to create reader: %w", err)
} }
defer readCloser.Close() defer readCloser.Close()
dest, err := os.Create(destPath) dest, err := os.Create(cacheDestPath)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create file: %w", err) return fmt.Errorf("failed to create file: %w", err)
} }
defer dest.Close() defer dest.Close()
task.StartTime = time.Now()
if _, err := io.CopyN(dest, readCloser, task.File.FileSize); err != nil { if _, err := io.CopyN(dest, readCloser, task.File.FileSize); err != nil {
return fmt.Errorf("Failed to download file: %w", err) return fmt.Errorf("failed to download file: %w", err)
} }
defer cleanCacheFile(destPath) defer cleanCacheFile(cacheDestPath)
logger.L.Infof("Downloaded file: %s", destPath) logger.L.Infof("Downloaded file: %s", cacheDestPath)
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{ ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()), Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
ID: task.ReplyMessageID, ID: task.ReplyMessageID,
}) })
return saveFileWithRetry(task, destPath) return saveFileWithRetry(task, cacheDestPath)
} }
func worker(queue *queue.TaskQueue, semaphore chan struct{}) { func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
@@ -132,7 +125,7 @@ func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
case types.Succeeded: case types.Succeeded:
logger.L.Infof("Task succeeded: %s", task.String()) logger.L.Infof("Task succeeded: %s", task.String())
task.Ctx.(*ext.Context).EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{ task.Ctx.(*ext.Context).EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
Message: "保存成功\n" + task.FileName(), Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.Storage, task.StoragePath),
ID: task.ReplyMessageID, ID: task.ReplyMessageID,
}) })
case types.Failed: case types.Failed:

View File

@@ -5,6 +5,10 @@ import (
"os" "os"
"time" "time"
"github.com/gotd/td/telegram/message/entity"
"github.com/gotd/td/telegram/message/styling"
"github.com/gotd/td/tg"
"github.com/krau/SaveAny-Bot/bot"
"github.com/krau/SaveAny-Bot/common" "github.com/krau/SaveAny-Bot/common"
"github.com/krau/SaveAny-Bot/config" "github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/logger" "github.com/krau/SaveAny-Bot/logger"
@@ -12,11 +16,11 @@ import (
"github.com/krau/SaveAny-Bot/types" "github.com/krau/SaveAny-Bot/types"
) )
func saveFileWithRetry(task *types.Task, destPath string) error { func saveFileWithRetry(task *types.Task, localFilePath string) error {
for i := 0; i <= config.Cfg.Retry; i++ { for i := 0; i <= config.Cfg.Retry; i++ {
if err := storage.Save(task.Storage, task.Ctx, destPath, task.StoragePath); err != nil { if err := storage.Save(task.Storage, task.Ctx, localFilePath, task.StoragePath); err != nil {
if i == config.Cfg.Retry { if i == config.Cfg.Retry {
return fmt.Errorf("Failed to save file: %w", err) return fmt.Errorf("failed to save file: %w", err)
} }
logger.L.Errorf("Failed to save file: %s, retrying...", err) logger.L.Errorf("Failed to save file: %s, retrying...", err)
continue continue
@@ -26,6 +30,32 @@ func saveFileWithRetry(task *types.Task, destPath string) error {
return nil return nil
} }
func processPhoto(task *types.Task, cachePath string) error {
res, err := bot.Client.API().UploadGetFile(task.Ctx, &tg.UploadGetFileRequest{
Location: task.File.Location,
Offset: 0,
Limit: 1024 * 1024,
})
if err != nil {
return fmt.Errorf("failed to get file: %w", err)
}
result, ok := res.(*tg.UploadFile)
if !ok {
return fmt.Errorf("unexpected type %T", res)
}
if err := os.WriteFile(cachePath, result.Bytes, os.ModePerm); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
defer cleanCacheFile(cachePath)
logger.L.Infof("Downloaded file: %s", cachePath)
return saveFileWithRetry(task, cachePath)
}
func getProgressBar(progress float64, totalCount int) string { func getProgressBar(progress float64, totalCount int) string {
bar := "" bar := ""
barSize := 100 / totalCount barSize := 100 / totalCount
@@ -52,7 +82,7 @@ func cleanCacheFile(destPath string) {
func calculateBarTotalCount(fileSize int64) int { func calculateBarTotalCount(fileSize int64) int {
barTotalCount := 5 barTotalCount := 5
if fileSize > 1024*1024*1000 { if fileSize > 1024*1024*1000 {
barTotalCount = 50 barTotalCount = 40
} else if fileSize > 1024*1024*500 { } else if fileSize > 1024*1024*500 {
barTotalCount = 20 barTotalCount = 20
} else if fileSize > 1024*1024*200 { } else if fileSize > 1024*1024*200 {
@@ -60,3 +90,38 @@ func calculateBarTotalCount(fileSize int64) int {
} }
return barTotalCount return barTotalCount
} }
func getSpeed(bytesRead int64, startTime time.Time) string {
if startTime.IsZero() {
return "0MB/s"
}
elapsed := time.Since(startTime)
speed := float64(bytesRead) / 1024 / 1024 / elapsed.Seconds()
return fmt.Sprintf("%.2fMB/s", speed)
}
func buildProgressMessageEntity(task *types.Task, barTotalCount int, bytesRead int64, startTime time.Time, progress float64) (string, []tg.MessageEntityClass) {
entityBuilder := entity.Builder{}
text := fmt.Sprintf("正在处理下载任务\n文件名: %s\n保存路径: %s\n平均速度: %s\n当前进度: [%s] %.2f%%",
task.FileName(),
fmt.Sprintf("[%s]:%s", task.Storage, task.StoragePath),
getSpeed(bytesRead, startTime),
getProgressBar(progress, barTotalCount),
progress,
)
var entities []tg.MessageEntityClass
if err := styling.Perform(&entityBuilder,
styling.Plain("正在处理下载任务\n文件名: "),
styling.Code(task.FileName()),
styling.Plain("\n保存路径: "),
styling.Code(fmt.Sprintf("[%s]:%s", task.Storage, task.StoragePath)),
styling.Plain("\n平均速度: "),
styling.Bold(getSpeed(bytesRead, task.StartTime)),
styling.Plain("\n当前进度:\n "),
styling.Code(fmt.Sprintf("[%s] %.2f%%", getProgressBar(progress, barTotalCount), progress)),
); err != nil {
logger.L.Errorf("Failed to build entities: %s", err)
return text, entities
}
return entityBuilder.Complete()
}

View File

@@ -3,12 +3,14 @@ package dao
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/krau/SaveAny-Bot/config" "github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/logger" "github.com/krau/SaveAny-Bot/logger"
"github.com/krau/SaveAny-Bot/types" "github.com/krau/SaveAny-Bot/types"
"gorm.io/gorm" "gorm.io/gorm"
glogger "gorm.io/gorm/logger"
) )
var db *gorm.DB var db *gorm.DB
@@ -19,7 +21,16 @@ func Init() {
os.Exit(1) os.Exit(1)
} }
var err error var err error
db, err = gorm.Open(sqlite.Open(config.Cfg.DB.Path), &gorm.Config{}) db, err = gorm.Open(sqlite.Open(config.Cfg.DB.Path), &gorm.Config{
Logger: glogger.New(logger.L, glogger.Config{
Colorful: true,
SlowThreshold: time.Second * 5,
LogLevel: glogger.Error,
IgnoreRecordNotFoundError: true,
ParameterizedQueries: true,
}),
PrepareStmt: true,
})
if err != nil { if err != nil {
logger.L.Fatal("Failed to open database: ", err) logger.L.Fatal("Failed to open database: ", err)
os.Exit(1) os.Exit(1)

View File

@@ -2,8 +2,12 @@ package dao
import "github.com/krau/SaveAny-Bot/types" import "github.com/krau/SaveAny-Bot/types"
func AddReceivedFile(receivedFile *types.ReceivedFile) error { func SaveReceivedFile(receivedFile *types.ReceivedFile) error {
return db.Create(receivedFile).Error record, err := GetReceivedFileByChatAndMessageID(receivedFile.ChatID, receivedFile.MessageID)
if err == nil {
receivedFile.ID = record.ID
}
return db.Save(receivedFile).Error
} }
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.ReceivedFile, error) { func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.ReceivedFile, error) {
@@ -15,10 +19,6 @@ func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.Rece
return &receivedFile, nil return &receivedFile, nil
} }
func UpdateReceivedFile(receivedFile *types.ReceivedFile) error {
return db.Save(receivedFile).Error
}
func DeleteReceivedFile(receivedFile *types.ReceivedFile) error { func DeleteReceivedFile(receivedFile *types.ReceivedFile) error {
return db.Delete(receivedFile).Error return db.Delete(receivedFile).Error
} }

32
docker-compose.yml Normal file
View File

@@ -0,0 +1,32 @@
services:
saveany-bot:
image: ghcr.io/krau/saveany-bot:latest
container_name: saveany-bot
restart: unless-stopped
environment:
- SAVEANY_TELEGRAM_TOKEN=bot_token
- SAVEANY_TELEGRAM_ADMINS=admin_id1,admin_id2
# 推荐使用自己的 API ID 和 API HASH (https://my.telegram.org)
# 若不配置将使用默认的 API ID 和 API HASH
# - SAVEANY_TELEGRAM_APP_ID=app_id
# - SAVEANY_TELEGRAM_APP_HASH=app_hash
# 本地存储
- SAVEANY_STORAGE_LOCAL_ENABLE=true
- SAVEANY_STORAGE_LOCAL_BASE_PATH=/app/downloads
# Alist
- SAVEANY_STORAGE_ALIST_ENABLE=true
- SAVEANY_STORAGE_ALIST_BASE_PATH=/saveany
- SAVEANY_STORAGE_ALIST_URL=https://example.com
- SAVEANY_STORAGE_ALIST_USERNAME=username
- SAVEANY_STORAGE_ALIST_PASSWORD=password
# webdav
- SAVEANY_STORAGE_WEBDAV_ENABLE=true
- SAVEANY_STORAGE_WEBDAV_BASE_PATH=/saveany
- SAVEANY_STORAGE_WEBDAV_URL=https://example.com
- SAVEANY_STORAGE_WEBDAV_USERNAME=username
- SAVEANY_STORAGE_WEBDAV_PASSWORD=password
volumes:
- ./data:/app/data
- ./downloads:/app/downloads
- ./cache:/app/cache

48
go.mod
View File

@@ -4,26 +4,24 @@ go 1.23.5
require ( require (
github.com/blang/semver v3.5.1+incompatible github.com/blang/semver v3.5.1+incompatible
github.com/celestix/gotgproto v1.0.0-beta20 github.com/celestix/gotgproto v1.0.0-beta20.2
github.com/gookit/slog v0.5.7 github.com/gookit/slog v0.5.7
github.com/gotd/contrib v0.21.0 github.com/gotd/contrib v0.21.0
github.com/gotd/td v0.117.0 github.com/gotd/td v0.120.0
github.com/imroc/req/v3 v3.49.1
github.com/rhysd/go-github-selfupdate v1.2.3 github.com/rhysd/go-github-selfupdate v1.2.3
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
github.com/studio-b12/gowebdav v0.10.0 github.com/studio-b12/gowebdav v0.10.0
golang.org/x/net v0.34.0 golang.org/x/net v0.35.0
golang.org/x/time v0.9.0 golang.org/x/time v0.10.0
) )
require ( require (
github.com/AnimeKaizoku/cacher v1.0.2 // indirect github.com/AnimeKaizoku/cacher v1.0.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/coder/websocket v1.8.12 // indirect github.com/coder/websocket v1.8.12 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
@@ -32,27 +30,21 @@ require (
github.com/go-faster/jx v1.1.0 // indirect github.com/go-faster/jx v1.1.0 // indirect
github.com/go-faster/xor v1.0.0 // indirect github.com/go-faster/xor v1.0.0 // indirect
github.com/go-faster/yaml v0.4.6 // indirect github.com/go-faster/yaml v0.4.6 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-github/v30 v30.1.0 // indirect github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gotd/ige v0.2.2 // indirect github.com/gotd/ige v0.2.2 // indirect
github.com/gotd/neo v0.1.5 // indirect github.com/gotd/neo v0.1.5 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ogen-go/ogen v1.8.1 // indirect github.com/ogen-go/ogen v1.10.0 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/onsi/gomega v1.36.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/refraction-networking/utls v1.6.7 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.0 // indirect
github.com/tcnksm/go-gitconfig v0.1.2 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect
@@ -61,22 +53,20 @@ require (
go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.23.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/tools v0.29.0 // indirect golang.org/x/tools v0.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.61.9 // indirect modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.34.5 // indirect modernc.org/sqlite v1.35.0 // indirect
rsc.io/qr v0.2.0 // indirect rsc.io/qr v0.2.0 // indirect
) )
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/coocood/freecache v1.2.4 github.com/coocood/freecache v1.2.4
github.com/duke-git/lancet/v2 v2.3.4 github.com/duke-git/lancet/v2 v2.3.4
github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect
@@ -95,15 +85,15 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.22.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/gorm v1.25.12 gorm.io/gorm v1.25.12

101
go.sum
View File

@@ -1,18 +1,16 @@
github.com/AnimeKaizoku/cacher v1.0.2 h1:7Bf5qRylWb7q2Evib0OXlhG37/t7BP2HK/7IyPvSmGQ= github.com/AnimeKaizoku/cacher v1.0.2 h1:7Bf5qRylWb7q2Evib0OXlhG37/t7BP2HK/7IyPvSmGQ=
github.com/AnimeKaizoku/cacher v1.0.2/go.mod h1:jw0de/b0K6W7Y3T9rHCMGVKUf6oG7hENNcssxYcZTCc= github.com/AnimeKaizoku/cacher v1.0.2/go.mod h1:jw0de/b0K6W7Y3T9rHCMGVKUf6oG7hENNcssxYcZTCc=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/celestix/gotgproto v1.0.0-beta20 h1:JxgcgBFidbFLeCxk6O9tREpB1BOZVcWGX019J+hP/hw= github.com/celestix/gotgproto v1.0.0-beta20.1 h1:F7H08CuSiHP0YlZqATBi2wJvg7dxXFvFbpauWFd0IbI=
github.com/celestix/gotgproto v1.0.0-beta20/go.mod h1:pSYS0xcqmQDYR7ZGe7vEKuz3CDuwQoIkn3Awyr92gbU= github.com/celestix/gotgproto v1.0.0-beta20.1/go.mod h1:j42ZhBMUke6QyBLvCgx8tA+TL9L3+pq/Q46B+b5+3aU=
github.com/celestix/gotgproto v1.0.0-beta20.2 h1:+WcsKdsyj4xy+TAV+4Sw6zp1xiQrIr4dMnM31+k8NYM=
github.com/celestix/gotgproto v1.0.0-beta20.2/go.mod h1:j42ZhBMUke6QyBLvCgx8tA+TL9L3+pq/Q46B+b5+3aU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M= github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
@@ -23,6 +21,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls= github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -53,8 +53,6 @@ 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/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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -65,8 +63,8 @@ github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQF
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf h1:BvBLUD2hkvLI3dJTJMiopAq8/wp43AAZKTP7qdpptbU=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
@@ -83,18 +81,13 @@ github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0= 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 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ= github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
github.com/gotd/td v0.117.0 h1:Z6vU5thb5DW/I1s0sLSeSfA/QWvwszx6SxHhEEYJiU8= github.com/gotd/td v0.118.0 h1:iPGkaOAd3QO72TcvzNJGKGpLDzYOW8GIz+Va2upxBbY=
github.com/gotd/td v0.117.0/go.mod h1:jf1Zf1ViTN+H1x8dhDTCBHOYY/2E/40HsyOsohxqXYA= github.com/gotd/td v0.118.0/go.mod h1:FUNVeJB9Id2Vqps9yF+8kmBNNyCGO6VXDyO8Ah7bVSw=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/gotd/td v0.120.0 h1:XeiafJM82/9SaB+ZMjMm/dnUx5+avINwVZOEsnV0zMo=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/gotd/td v0.120.0/go.mod h1:BCc2jFj1l5zP9Trk4J7nxeqW0KBGl6K95eXMgszkbOI=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imroc/req/v3 v3.49.1 h1:Nvwo02riiPEzh74ozFHeEJrtjakFxnoWNR3YZYuQm9U=
github.com/imroc/req/v3 v3.49.1/go.mod h1:tsOk8K7zI6cU4xu/VWCZVtq9Djw9IWm4MslKzme5woU=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -122,11 +115,11 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 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/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ogen-go/ogen v1.8.1 h1:7TZ+oIeLkcBiyl0qu0fHPrFUrGWDj3Fi/zKSWg2i2Tg= github.com/ogen-go/ogen v1.9.0 h1:n+lDQpiSFYC9G4hTvuNVWnqmIP0LR8ws0faDn9jX3hU=
github.com/ogen-go/ogen v1.8.1/go.mod h1:2ShRm6u/nXUHuwdVKv2SeaG8enBKPKAE3kSbHwwFh6o= github.com/ogen-go/ogen v1.9.0/go.mod h1:vkHpuRyzjdfuRCy81EShi4t9sIgZDcNPGmiDKipRloc=
github.com/ogen-go/ogen v1.10.0 h1:x3ukRtq/pdn/k8+pYBtqWceVASiSmgK9M5lrH89Q+04=
github.com/ogen-go/ogen v1.10.0/go.mod h1:WExXrswerPzGWD0NpzBFsz+5eQIbP7HAtZUmpV8dqqI=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
@@ -136,12 +129,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag= github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
@@ -163,8 +150,9 @@ github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 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= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@@ -182,8 +170,6 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 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/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
@@ -196,8 +182,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 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/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 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
@@ -206,51 +190,70 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 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/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 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/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 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/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 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/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 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/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
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 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -264,14 +267,18 @@ gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00= modernc.org/ccgo/v4 v4.23.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc=
modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8= modernc.org/gc/v2 v2.6.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds=
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM= modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk= modernc.org/libc v1.61.11 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw=
modernc.org/libc v1.61.11/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
@@ -282,6 +289,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw=
modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -1,33 +1,33 @@
package alist package alist
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path"
"time" "time"
"github.com/imroc/req/v3"
"github.com/krau/SaveAny-Bot/config" "github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/logger" "github.com/krau/SaveAny-Bot/logger"
) )
type Alist struct{} type Alist struct {
client *http.Client
token string
baseURL string
loginInfo *loginRequest
}
var ( var (
basePath string
baseUrl string
reqClient *req.Client
loginReq *loginRequset
ErrAlistLoginFailed = errors.New("failed to login to Alist") ErrAlistLoginFailed = errors.New("failed to login to Alist")
) )
type loginRequset struct { type loginRequest struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} }
@@ -40,70 +40,135 @@ type loginResponse struct {
} `json:"data"` } `json:"data"`
} }
func getToken() (string, error) { type putResponse struct {
resp, err := reqClient.R().SetBodyJsonMarshal(loginReq).Post("/api/auth/login") Code int `json:"code"`
if err != nil { Message string `json:"message"`
return "", err Data struct {
} Task struct {
var loginResp loginResponse ID string `json:"id"`
if err := json.Unmarshal(resp.Bytes(), &loginResp); err != nil { Name string `json:"name"`
return "", err State int `json:"state"`
} Status string `json:"status"`
if loginResp.Code != http.StatusOK { Progress int `json:"progress"`
return "", fmt.Errorf("%w: %s", ErrAlistLoginFailed, loginResp.Message) Error string `json:"error"`
} } `json:"task"`
return loginResp.Data.Token, nil } `json:"data"`
} }
func refreshToken(client *req.Client) { func (a *Alist) getToken() error {
loginBody, err := json.Marshal(a.loginInfo)
if err != nil {
return fmt.Errorf("failed to marshal login request: %w", err)
}
req, err := http.NewRequest(http.MethodPost, a.baseURL+"/api/auth/login", bytes.NewBuffer(loginBody))
if err != nil {
return fmt.Errorf("failed to create login request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := a.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send login request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read login response: %w", err)
}
var loginResp loginResponse
if err := json.Unmarshal(body, &loginResp); err != nil {
return fmt.Errorf("failed to unmarshal login response: %w", err)
}
if loginResp.Code != http.StatusOK {
return fmt.Errorf("%w: %s", ErrAlistLoginFailed, loginResp.Message)
}
a.token = loginResp.Data.Token
return nil
}
func (a *Alist) refreshToken() {
for { for {
time.Sleep(time.Duration(config.Cfg.Storage.Alist.TokenExp) * time.Second) time.Sleep(time.Duration(config.Cfg.Storage.Alist.TokenExp) * time.Second)
token, err := getToken() if err := a.getToken(); err != nil {
if err != nil {
logger.L.Errorf("Failed to refresh jwt token: %v", err) logger.L.Errorf("Failed to refresh jwt token: %v", err)
continue continue
} }
client.SetCommonHeader("Authorization", token)
logger.L.Info("Refreshed Alist jwt token") logger.L.Info("Refreshed Alist jwt token")
} }
} }
func (a *Alist) Init() { func (a *Alist) Init() {
basePath = config.Cfg.Storage.Alist.BasePath a.baseURL = config.Cfg.Storage.Alist.URL
baseUrl = config.Cfg.Storage.Alist.URL a.client = &http.Client{
reqClient = req.C().SetTLSHandshakeTimeout(time.Second * 10).SetBaseURL(baseUrl).SetTimeout(time.Hour * 24) Timeout: 12 * time.Hour,
loginReq = &loginRequset{ Transport: &http.Transport{
TLSHandshakeTimeout: 10 * time.Second,
},
}
a.loginInfo = &loginRequest{
Username: config.Cfg.Storage.Alist.Username, Username: config.Cfg.Storage.Alist.Username,
Password: config.Cfg.Storage.Alist.Password, Password: config.Cfg.Storage.Alist.Password,
} }
token, err := getToken()
if err != nil { if err := a.getToken(); err != nil {
logger.L.Fatalf("Failed to login to Alist: %v", err) logger.L.Fatalf("Failed to login to Alist: %v", err)
os.Exit(1) os.Exit(1)
} }
logger.L.Debug("Logged in to Alist") logger.L.Debug("Logged in to Alist")
reqClient.SetCommonHeader("Authorization", token)
go refreshToken(reqClient) go a.refreshToken()
} }
func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error { func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
storagePath = path.Join(basePath, storagePath)
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to open file: %w", err)
} }
resp, err := reqClient.R(). defer file.Close()
SetContext(ctx).
SetBody(file). filestat, err := file.Stat()
SetHeaders(map[string]string{
"File-Path": url.PathEscape(storagePath),
"As-Task": "true",
}).Put("/api/fs/put")
if err != nil { if err != nil {
return err return fmt.Errorf("failed to get file stats: %w", err)
} }
req, err := http.NewRequestWithContext(ctx, http.MethodPut, a.baseURL+"/api/fs/put", file)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", a.token)
req.Header.Set("File-Path", url.PathEscape(storagePath))
req.Header.Set("As-Task", "true")
req.Header.Set("Content-Type", "application/octet-stream")
req.ContentLength = filestat.Size()
resp, err := a.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to save file to Alist: %s", resp.Status) return fmt.Errorf("failed to save file to Alist: %s", resp.Status)
} }
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
var putResp putResponse
if err := json.Unmarshal(body, &putResp); err != nil {
return fmt.Errorf("failed to unmarshal put response: %w", err)
}
if putResp.Code != http.StatusOK {
return fmt.Errorf("failed to save file to Alist: %d, %s", putResp.Code, putResp.Message)
}
return nil return nil
} }

View File

@@ -21,5 +21,12 @@ func (l *Local) Init() {
} }
func (l *Local) Save(ctx context.Context, filePath, storagePath string) error { func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
return fileutil.CopyFile(filePath, filepath.Join(config.Cfg.Storage.Local.BasePath, storagePath)) absPath, err := filepath.Abs(storagePath)
if err != nil {
return err
}
if err := fileutil.CreateDir(filepath.Dir(absPath)); err != nil {
return err
}
return fileutil.CopyFile(filePath, storagePath)
} }

View File

@@ -3,6 +3,8 @@ package storage
import ( import (
"context" "context"
"errors" "errors"
"path"
"path/filepath"
"sync" "sync"
"github.com/duke-git/lancet/v2/slice" "github.com/duke-git/lancet/v2/slice"
@@ -16,7 +18,7 @@ import (
type Storage interface { type Storage interface {
Init() Init()
Save(cttx context.Context, filePath, storagePath string) error Save(cttx context.Context, localFilePath, storagePath string) error
} }
var Storages = make(map[types.StorageType]Storage) var Storages = make(map[types.StorageType]Storage)
@@ -47,6 +49,7 @@ func Init() {
} }
func Save(storageType types.StorageType, ctx context.Context, filePath, storagePath string) error { func Save(storageType types.StorageType, ctx context.Context, filePath, storagePath string) error {
logger.L.Debugf("Saving file %s to storage: [%s] %s", filePath, storageType, storagePath)
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
@@ -59,7 +62,16 @@ func Save(storageType types.StorageType, ctx context.Context, filePath, storageP
wg.Add(1) wg.Add(1)
go func(storage Storage) { go func(storage Storage) {
defer wg.Done() defer wg.Done()
if err := storage.Save(ctx, filePath, storagePath); err != nil { storageDestPath := storagePath
switch storage.(type) {
case *local.Local:
storageDestPath = filepath.Join(config.Cfg.Storage.Local.BasePath, storagePath)
case *webdav.Webdav:
storageDestPath = path.Join(config.Cfg.Storage.Webdav.BasePath, storagePath)
case *alist.Alist:
storageDestPath = path.Join(config.Cfg.Storage.Alist.BasePath, storagePath)
}
if err := storage.Save(ctx, filePath, storageDestPath); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
}(storage) }(storage)

View File

@@ -4,8 +4,6 @@ import (
"context" "context"
"os" "os"
"path" "path"
"path/filepath"
"strings"
"time" "time"
"github.com/krau/SaveAny-Bot/config" "github.com/krau/SaveAny-Bot/config"
@@ -16,13 +14,11 @@ import (
type Webdav struct{} type Webdav struct{}
var ( var (
Client *gowebdav.Client Client *gowebdav.Client
basePath string
) )
func (w *Webdav) Init() { func (w *Webdav) Init() {
webdavConfig := config.Cfg.Storage.Webdav webdavConfig := config.Cfg.Storage.Webdav
basePath = strings.TrimSuffix(webdavConfig.BasePath, "/")
Client = gowebdav.NewClient(webdavConfig.URL, webdavConfig.Username, webdavConfig.Password) Client = gowebdav.NewClient(webdavConfig.URL, webdavConfig.Username, webdavConfig.Password)
if err := Client.Connect(); err != nil { if err := Client.Connect(); err != nil {
logger.L.Fatalf("Failed to connect to webdav server: %v", err) logger.L.Fatalf("Failed to connect to webdav server: %v", err)
@@ -32,9 +28,8 @@ func (w *Webdav) Init() {
} }
func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error { func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
storagePath = path.Join(basePath, storagePath) if err := Client.MkdirAll(path.Dir(storagePath), os.ModePerm); err != nil {
if err := Client.MkdirAll(filepath.Dir(storagePath), os.ModePerm); err != nil { logger.L.Errorf("Failed to create directory %s: %v", path.Dir(storagePath), err)
logger.L.Errorf("Failed to create directory %s: %v", filepath.Dir(storagePath), err)
return ErrFailedToCreateDirectory return ErrFailedToCreateDirectory
} }
file, err := os.Open(filePath) file, err := os.Open(filePath)

View File

@@ -7,8 +7,8 @@ import (
type ReceivedFile struct { type ReceivedFile struct {
gorm.Model gorm.Model
Processing bool Processing bool
ChatID int64 ChatID int64 `gorm:"uniqueIndex:idx_chat_id_message_id;not null"`
MessageID int MessageID int `gorm:"uniqueIndex:idx_chat_id_message_id;not null"`
ReplyMessageID int ReplyMessageID int
FileName string FileName string
} }

View File

@@ -3,6 +3,7 @@ package types
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/gotd/td/tg" "github.com/gotd/td/tg"
) )
@@ -34,6 +35,7 @@ type Task struct {
File *File File *File
Storage StorageType Storage StorageType
StoragePath string StoragePath string
StartTime time.Time
MessageID int MessageID int
ChatID int64 ChatID int64