Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50fba3f910 | ||
|
|
87d3f14392 | ||
|
|
30452c8d46 | ||
|
|
300f7723af |
15
README.md
15
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
# <img src="docs/logo.jpg" width="45" align="center"> Save Any Bot
|
||||
|
||||
**简体中文** | [English](README_EN.md)
|
||||
**简体中文** | [English](README_EN.md)
|
||||
|
||||
把 Telegram 的文件保存到各类存储端.
|
||||
|
||||
@@ -50,7 +50,7 @@ WantedBy=multi-user.target
|
||||
systemctl enable --now saveany-bot
|
||||
```
|
||||
|
||||
#### 为OpenWrt及衍生系统添加开机自启动服务
|
||||
#### 为 OpenWrt 及衍生系统添加开机自启动服务
|
||||
|
||||
创建文件 ` /etc/init.d/saveanybot` ,参考[saveanybot](./docs/saveanybot)自行修改.
|
||||
|
||||
@@ -60,7 +60,7 @@ systemctl enable --now saveany-bot
|
||||
|
||||
`chmod +x /etc/rc.d/S99saveanybot`
|
||||
|
||||
#### 为OpenWrt及衍生系统添加快捷指令
|
||||
#### 为 OpenWrt 及衍生系统添加快捷指令
|
||||
|
||||
创建文件` /usr/bin/sabot` ,参考[sabot](./docs/sabot)自行配置修改,注意此处文件编码仅支持 ANSI 936 .
|
||||
|
||||
@@ -68,7 +68,6 @@ systemctl enable --now saveany-bot
|
||||
|
||||
之后,终端输入`sabot start|stop|restart|status|enable|disable`即可.
|
||||
|
||||
|
||||
### 使用 Docker 部署
|
||||
|
||||
#### Docker Compose
|
||||
@@ -111,6 +110,14 @@ docker restart saveany-bot
|
||||
|
||||
---
|
||||
|
||||
## 赞助
|
||||
|
||||
本项目受到 [YxVM](https://yxvm.com/) 与 [NodeSupport](https://github.com/NodeSeekDev/NodeSupport) 的支持.
|
||||
|
||||
如果这个项目对你有帮助, 你可以考虑通过以下方式赞助我:
|
||||
|
||||
- [爱发电](https://afdian.com/a/acherkrau)
|
||||
|
||||
## Thanks
|
||||
|
||||
- [gotd](https://github.com/gotd/td)
|
||||
|
||||
@@ -92,6 +92,14 @@ Send (forward) files to the Bot and follow the prompts.
|
||||
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
|
||||
This project is supported by [YxVM](https://yxvm.com/) and [NodeSupport](https://github.com/NodeSeekDev/NodeSupport).
|
||||
|
||||
You can consider sponsoring me if this project helps you:
|
||||
|
||||
- [Afdian](https://afdian.com/a/acherkrau)
|
||||
|
||||
## Thanks
|
||||
|
||||
- [gotd](https://github.com/gotd/td)
|
||||
|
||||
@@ -27,10 +27,10 @@ func newProxyDialer(proxyUrl string) (proxy.Dialer, error) {
|
||||
}
|
||||
|
||||
func Init() {
|
||||
InitTelegraphClient()
|
||||
common.Log.Info("初始化 Telegram 客户端...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Cfg.Telegram.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
go InitTelegraphClient()
|
||||
resultChan := make(chan struct {
|
||||
client *gotgproto.Client
|
||||
err error
|
||||
|
||||
@@ -13,6 +13,9 @@ token = ""
|
||||
# app_id = 123456
|
||||
# app_hash = "0123456789abcdef0123456789abcdef"
|
||||
|
||||
# 初始化超时时间, 单位: 秒
|
||||
timeout = 60
|
||||
|
||||
[telegram.proxy]
|
||||
# 启用代理连接 telegram, 只支持 socks5
|
||||
enable = false
|
||||
@@ -30,12 +33,6 @@ enable = true
|
||||
# 文件保存根路径
|
||||
base_path = "./downloads"
|
||||
|
||||
[[storages]]
|
||||
name = "本机2"
|
||||
type = "local"
|
||||
enable = true
|
||||
base_path = "./downloads/2"
|
||||
|
||||
[[storages]]
|
||||
name = "MyAlist"
|
||||
type = "alist"
|
||||
@@ -49,7 +46,6 @@ token_exp = 86400 # 86400--1天 604800--7天 1296000--15天 2592000--30
|
||||
# 请自行在 alist 侧配置合理的 token 过期时间
|
||||
# token = ""
|
||||
|
||||
|
||||
[[storages]]
|
||||
name = "MyWebdav"
|
||||
type = "webdav"
|
||||
|
||||
@@ -44,6 +44,7 @@ type telegramConfig struct {
|
||||
Token string `toml:"token" mapstructure:"token"`
|
||||
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
||||
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
||||
Timeout int `toml:"timeout" mapstructure:"timeout" json:"timeout"`
|
||||
Proxy proxyConfig `toml:"proxy" mapstructure:"proxy"`
|
||||
|
||||
// Deprecated
|
||||
@@ -82,6 +83,7 @@ func Init() error {
|
||||
|
||||
viper.SetDefault("telegram.app_id", 1025907)
|
||||
viper.SetDefault("telegram.app_hash", "452b0359b988148995f22ff0f4229750")
|
||||
viper.SetDefault("telegram.timeout", 60)
|
||||
|
||||
viper.SetDefault("temp.base_path", "cache/")
|
||||
viper.SetDefault("temp.cache_ttl", 3600)
|
||||
|
||||
@@ -6,10 +6,7 @@ Bot 接受两种消息: 文件和链接.
|
||||
|
||||
支持以下链接:
|
||||
|
||||
1. 公开频道 (具有用户名) 的消息链接, 例如: `https://t.me/acherkrau/1097`.
|
||||
|
||||
**即使频道禁止了转发和保存, Bot 依然可以下载其文件.**
|
||||
|
||||
1. 公开频道 (具有用户名) 的消息链接, 例如: `https://t.me/acherkrau/1097`. **即使频道禁止了转发和保存, Bot 依然可以下载其文件.**
|
||||
2. Telegra.ph 的文章链接, Bot 将下载其中的所有图片
|
||||
|
||||
## 静默模式 (silent)
|
||||
|
||||
130
storage/webdav/client._test.go
Normal file
130
storage/webdav/client._test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
func setupWebDAVServer(t *testing.T) (*httptest.Server, string) {
|
||||
t.Helper()
|
||||
tempDir, err := os.MkdirTemp("", "webdav_test")
|
||||
if err != nil {
|
||||
t.Fatalf("mk temp dir failed: %v", err)
|
||||
}
|
||||
|
||||
handler := &webdav.Handler{
|
||||
Prefix: "/",
|
||||
FileSystem: webdav.Dir(tempDir),
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
}
|
||||
|
||||
server := httptest.NewServer(handler)
|
||||
return server, tempDir
|
||||
}
|
||||
|
||||
func TestMkDirAndExists(t *testing.T) {
|
||||
server, tempDir := setupWebDAVServer(t)
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "", "", nil)
|
||||
ctx := context.Background()
|
||||
|
||||
testpaths := []string{"testdir", "testdir/subdir", "testdir/子目录", "/testdir/测试路径/测试路径2"}
|
||||
for _, p := range testpaths {
|
||||
exists, err := client.Exists(ctx, p)
|
||||
if err != nil {
|
||||
t.Fatalf("Call Exists Err: %v", err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("Dir should not exist")
|
||||
}
|
||||
|
||||
if err := client.MkDir(ctx, p); err != nil {
|
||||
t.Fatalf("Call MkDir Err: %v", err)
|
||||
}
|
||||
|
||||
exists, err = client.Exists(ctx, p)
|
||||
if err != nil {
|
||||
t.Fatalf("Call Exists Err: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Dir should exist")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
server, tempDir := setupWebDAVServer(t)
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "", "", nil)
|
||||
ctx := context.Background()
|
||||
|
||||
testCases := []struct {
|
||||
remotePath string
|
||||
content string
|
||||
}{
|
||||
{
|
||||
remotePath: "hello.txt",
|
||||
content: "Hello webdav",
|
||||
},
|
||||
{
|
||||
remotePath: "nested/dir/test.txt",
|
||||
content: "Nested file",
|
||||
},
|
||||
{
|
||||
remotePath: "empty.txt",
|
||||
content: "",
|
||||
},
|
||||
{
|
||||
remotePath: "unicode.txt",
|
||||
content: "测试",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.remotePath, func(t *testing.T) {
|
||||
dir := path.Dir(tc.remotePath)
|
||||
if dir != "." {
|
||||
if err := client.MkDir(ctx, dir); err != nil {
|
||||
t.Fatalf("创建目录 %s 失败: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.WriteFile(ctx, tc.remotePath, strings.NewReader(tc.content)); err != nil {
|
||||
t.Fatalf("写入文件 %s 失败: %v", tc.remotePath, err)
|
||||
}
|
||||
|
||||
localPath := filepath.Join(tempDir, tc.remotePath)
|
||||
data, err := os.ReadFile(localPath)
|
||||
if err != nil {
|
||||
t.Fatalf("读取文件 %s 失败: %v", localPath, err)
|
||||
}
|
||||
if string(data) != tc.content {
|
||||
t.Fatalf("文件内容不匹配: got %s, want %s", string(data), tc.content)
|
||||
}
|
||||
|
||||
appended := tc.content + " Overwritten."
|
||||
if err := client.WriteFile(ctx, tc.remotePath, strings.NewReader(appended)); err != nil {
|
||||
t.Fatalf("覆盖写入文件 %s 失败: %v", tc.remotePath, err)
|
||||
}
|
||||
data, err = os.ReadFile(localPath)
|
||||
if err != nil {
|
||||
t.Fatalf("读取覆盖后的文件 %s 失败: %v", localPath, err)
|
||||
}
|
||||
if string(data) != appended {
|
||||
t.Fatalf("文件覆盖后的内容不匹配: got %s, want %s", string(data), appended)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -48,18 +48,55 @@ func (c *Client) doRequest(ctx context.Context, method, url string, body io.Read
|
||||
return c.httpClient.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) MkDir(ctx context.Context, dirPath string) error {
|
||||
url := c.BaseURL + dirPath
|
||||
resp, err := c.doRequest(ctx, "MKCOL", url, nil)
|
||||
func (c *Client) Exists(ctx context.Context, remotePath string) (bool, error) {
|
||||
url := c.BaseURL + remotePath
|
||||
resp, err := c.doRequest(ctx, "PROPFIND", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return true, nil
|
||||
}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("PROPFIND: %s", resp.Status)
|
||||
}
|
||||
|
||||
func (c *Client) MkDir(ctx context.Context, dirPath string) error {
|
||||
dirPath = strings.Trim(dirPath, "/")
|
||||
if dirPath == "" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("MKCOL: %s", resp.Status)
|
||||
parts := strings.Split(dirPath, "/")
|
||||
currentPath := ""
|
||||
for i, part := range parts {
|
||||
if i > 0 {
|
||||
currentPath += "/"
|
||||
}
|
||||
currentPath += part
|
||||
|
||||
exists, err := c.Exists(ctx, currentPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
url := c.BaseURL + currentPath
|
||||
resp, err := c.doRequest(ctx, "MKCOL", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("MKCOL %s: %s", currentPath, resp.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) WriteFile(ctx context.Context, remotePath string, content io.Reader) error {
|
||||
|
||||
Reference in New Issue
Block a user