Compare commits

...

2 Commits

Author SHA1 Message Date
krau
056e2fd546 fix: add ca-certificates installation and environment variables for SSL support in Dockerfile, close #222 2026-07-06 11:34:28 +08:00
krau
c9bb6c9e3c feat(docs): add CLI subcommands documentation and file naming strategies
- Introduced new documentation for CLI subcommands: `upload` and `watch`, detailing their usage, flags, and examples.
- Added a section on file naming and conflict strategies, explaining how users can customize file names and handle duplicates via `/config` and `/fnametmpl` commands.
- Translated the new documentation into Chinese for broader accessibility.
2026-06-28 18:00:02 +08:00
11 changed files with 508 additions and 2 deletions

View File

@@ -7,6 +7,8 @@ ARG BuildTime="Unknown"
WORKDIR /app
RUN apk add --no-cache ca-certificates
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
@@ -31,5 +33,9 @@ FROM scratch
WORKDIR /app
COPY --from=builder /app/saveany-bot .
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
ENV SSL_CERT_DIR=/etc/ssl/certs
ENTRYPOINT ["/app/saveany-bot"]

View File

@@ -113,6 +113,51 @@ secret = "your-rpc-secret"
remove_after_transfer = true
```
### yt-dlp Configuration
Configures the behavior of the `/ytdlp` command and the `ytdlp` HTTP-API task type when no custom flags are passed.
- `max_height`: Default maximum video resolution by height in pixels (e.g. `1080`, `720`). `0` means no limit (best available). Ignored when `format` is set.
- `format`: A raw yt-dlp format selector (`-f`). When set, it takes precedence over `max_height` and gives you full control, e.g. `bv*[height<=720]+ba/b`.
- `recode`: The target video container yt-dlp recodes into after download (e.g. `mp4`). Leave empty to disable recoding.
{{< hint info >}}
These defaults only apply when using the `/ytdlp` command without passing any custom flags. Passing custom flags on the command (or `flags` in the API) overrides them.
{{< /hint >}}
```toml
[ytdlp]
max_height = 1080
format = "" # e.g. "bv*[height<=720]+ba/b"
recode = "mp4" # empty disables recoding
```
### HTTP API Configuration
When enabled, SaveAny-Bot exposes an HTTP API for creating/querying/canceling tasks programmatically. See [HTTP API](../../usage/api) for the full endpoint reference.
- `enable`: Whether to enable the HTTP API server, default is `false`.
- `host`: Bind address, default `0.0.0.0`.
- `port`: Listen port, default `8080`.
- `token`: Authentication token. **Strongly recommended** — if empty, the API is exposed without any authentication.
```toml
[api]
enable = false
host = "0.0.0.0"
port = 8080
token = "your-token"
```
### Log Configuration
- `level`: Log level. One of `debug`, `info`, `warn`, `error`, `fatal`. Default is `info`.
```toml
[log]
level = "info"
```
### Storage Endpoints List
The storage endpoints list is used to define the storage locations supported by the Bot. Each storage endpoint needs to specify a name, type, and related configuration, using the double bracket syntax `[[storages]]`.

View File

@@ -0,0 +1,90 @@
---
title: "CLI Subcommands"
weight: 21
---
# CLI Subcommands
Besides running the Telegram bot with `./saveany-bot` (no subcommand), the binary exposes two helper subcommands for moving local files into a storage backend: `upload` (one-shot) and `watch` (continuous).
These subcommands load the same `config.toml` as the bot, initialize the database and caches, then perform their task. They do **not** start the Telegram bot itself, although storages of type `telegram` will spin up the bot client just for the upload.
## `upload` — Upload a Single File
```
saveany-bot upload -f <file> -s <storage> [-d <dir>] [--no-progress]
```
Flags:
| Flag | Required | Description |
|---|---|---|
| `-f, --file` | Yes | Path to the local file to upload |
| `-s, --storage` | Yes | Target storage name (must exist in `config.toml`) |
| `-d, --dir` | No | Destination directory within the storage. Defaults to the storage's `base_path` |
| `--no-progress` | No | Disable the terminal progress bar |
Examples:
```bash
# Upload a file to the default dir of storage "MyAlist"
./saveany-bot upload -f ./movie.mp4 -s MyAlist
# Upload into a specific subdirectory
./saveany-bot upload -f ./movie.mp4 -s MyAlist -d movies/2026
# Upload via Telegram storage without a progress bar
./saveany-bot upload -f ./photo.jpg -s MyChannel --no-progress
```
## `watch` — Watch a Directory and Auto-Upload
The `watch` subcommand continuously monitors a local directory and uploads created or modified files to a storage backend, preserving the relative directory structure from the watch root.
```
saveany-bot watch -p <path> -s <storage> [-d <dir>] [options]
```
Flags:
| Flag | Default | Description |
|---|---|---|
| `-p, --path` | *(required)* | Local directory to watch |
| `-s, --storage` | *(required)* | Target storage name |
| `-d, --dir` | storage's `base_path` | Destination directory within the storage |
| `-r, --recursive` | `false` | Watch subdirectories recursively |
| `--overwrite` | `false` | Overwrite existing files on the storage instead of skipping them |
| `--initial-scan` | `false` | Upload files already present in the directory on startup |
| `--debounce` | `2s` | How long to wait after the last write before uploading a file |
| `--upload-workers` | `config.workers` | Number of concurrent uploads |
| `--retry-delay` | `3s` | Delay between upload retries |
{{< hint info >}}
Write-completion detection: the watcher debounces per file and only uploads once the file size stays unchanged across the debounce window, so partial/write-in-progress files are not uploaded.
<br />
If a file changes while being uploaded, it is re-uploaded once after the current upload finishes (instead of being queued multiple times).
{{< /hint >}}
Examples:
```bash
# Watch ./inbox and upload new files to "MyAlist" recursively
./saveany-bot watch -p ./inbox -s MyAlist -r
# Watch with a custom destination dir and overwrite
./saveany-bot watch -p ./inbox -s MyAlist -d backup --overwrite
# On startup, also upload everything already in ./inbox
./saveany-bot watch -p ./inbox -s MyAlist --initial-scan
```
### Behavior notes
- Relative directory structure is preserved under the destination directory. A file written to `./inbox/sub/file.txt` with `--path ./inbox` is uploaded to `<dest_dir>/sub/file.txt`.
- `watch` runs until interrupted (e.g. `Ctrl-C` / `SIGINT`); in-flight uploads are drained before exit.
- Retries follow the global `retry` value from `config.toml`, with `--retry-delay` between attempts.
- Telegram-type storages will start the bot client automatically to perform uploads.
{{< hint warning >}}
`watch` is unrelated to the in-bot `/watch` command (which watches Telegram chats). This subcommand watches a **local filesystem directory** and uploads to a storage backend, independent of Telegram.
{{< /hint >}}

View File

@@ -0,0 +1,75 @@
---
title: "File Naming & Conflict Strategies"
weight: 11
---
# File Naming & Conflict Strategies
SaveAny-Bot lets you customize how saved files are named and how collisions with existing files are resolved, directly in Telegram via the `/config` and `/fnametmpl` commands.
## `/config` — User Configuration
The `/config` command opens an inline menu where you can change two per-user settings:
- **Filename strategy** — how the saved file is named
- **Duplicate file strategy** — what happens when a file with the same name already exists in the target storage
Settings are stored per user and apply to all of that user's subsequent save/transfer tasks.
### Filename strategy
| Option | Behavior |
|---|---|
| `Default` | Use the original media filename, or a generated name when no original filename is available |
| `Gen From Msg First` | Generate the filename from the message content (e.g. caption, text) and prefer that over the original filename |
| `Template` | Render the filename from a custom template you define with `/fnametmpl` |
### Duplicate file strategy
| Option | Behavior |
|---|---|
| `Always rename` (default) | Keep the existing file and save the new one with an alternate name |
| `Ask every time` | Prompt you with inline buttons each time a collision occurs |
| `Always overwrite` | Replace the existing file with the new one |
| `Always skip` | Do nothing for conflicting files |
{{< hint info >}}
The conflict strategy only kicks in for storage backends that can detect the existence of a file. Backends that do not support existence checks will fall back to overwriting.
{{< /hint >}}
## `/fnametmpl` — Custom Filename Template
When the filename strategy is set to `Template`, SaveAny-Bot renders each saved file's name using the template configured via `/fnametmpl`.
```
/fnametmpl [template]
```
- Running `/fnametmpl` without arguments shows your current template and the help text.
- Running it with a template string sets that template as your filename template.
The template uses Go [`text/template`](https://pkg.go.dev/text/template) syntax. The available variables are:
| Variable | Description |
|---|---|
| `{{.msgid}}` | Telegram message ID |
| `{{.msgtags}}` | Hashtags found in the message, joined with `_` |
| `{{.msggen}}` | Filename generated from the message |
| `{{.msgdate}}` | Message date, formatted `YYYY-MM-DD_HH-MM-SS` |
| `{{.msgraw}}` | Raw, unprocessed message text |
| `{{.origname}}` | The media's original filename (if any) |
| `{{.chatid}}` | Chat ID of the message |
Examples:
```
# Fixed prefix + message id + date
/fnametmpl Image_{{.msgid}}_{{.msgdate}}.jpg
# Use original name if available, otherwise a generated name
/fnametmpl {{.origname}}
```
{{< hint warning >}}
The template only takes effect when the filename strategy is set to `Template`. If template parsing fails, SaveAny-Bot falls back to the default filename naming logic.
{{< /hint >}}

View File

@@ -27,6 +27,45 @@ Pay attention to spaces; the bot can only parse correctly formatted syntax. Belo
In addition, if `CHOSEN` is used as the storage name in the rule, it means files will be stored under the path of the storage you selected by clicking the inline button.
You can also toggle whether rules are applied with `/rule switch`. When rule mode is off, all files go to the default storage.
## Preset Rules
Manually writing regex rules for common file types is tedious, so the bot ships a built-in set of preset categories (video, image, audio, document, archive) that you can import in one command:
```
/rule preset <storage> [base_path]
```
Parameters:
- `storage`: Target storage name (must exist and be accessible to you)
- `base_path`: Optional. Each preset category's subdirectory is created under this path. If omitted, the default category directory names are used directly.
Examples:
```
# Import preset rules into "MyAlist" with the default directory layout
/rule preset MyAlist
# Import preset rules with a custom base path "downloads/sorted"
/rule preset MyAlist downloads/sorted
```
This will create `FILENAME-REGEX` rules for each category, routing matched files to the corresponding subdirectory under `base_path`:
| Category | Matched extensions | Default directory |
|---|---|---|
| video | mp4, mkv, ts, avi, flv, mov, webm, wmv, rmvb, m2ts | `视频` |
| image | jpg, jpeg, png, gif, webp, bmp | `图片` |
| audio | mp3, flac, wav, aac, m4a, ogg | `音频` |
| document | pdf, doc, docx, xls, xlsx, ppt, pptx, txt, md, csv, epub, mobi, azw3, chm | `文档` |
| archive | zip, rar, 7z, tar, gz, bz2, xz, ... | `压缩包` |
{{< hint info >}}
Preset rules are regular `FILENAME-REGEX` rules once imported. You can view, edit, or delete them individually with `/rule` and `/rule del <id>` like any other rule.
{{< /hint >}}
Rule types:
## FILENAME-REGEX

View File

@@ -30,6 +30,7 @@ base_path = "./downloads"
### 全局配置
- `lang`: Bot 使用的语言, 默认为 `zh-CN` (简体中文), 设为 `en` 则使用英语.
- `stream`: 是否启用 Stream 模式, 默认为 `false`. 启用后 Bot 将直接将文件流式传输到存储端(若存储端支持), 不需要下载到本地
{{< hint warning >}}
Stream 模式对于磁盘空间有限的部署环境十分有用, 但也有一些弊端:
@@ -47,6 +48,7 @@ Stream 模式对于磁盘空间有限的部署环境十分有用, 但也有一
- `proxy`: 全局代理配置, 配置后程序内一切网络连接将会尝试使用该代理, 可选.
```toml
lang = "zh-CN"
stream = false
workers = 3
threads = 4
@@ -111,6 +113,51 @@ secret = "your-rpc-secret"
remove_after_transfer = true
```
### yt-dlp 配置
用于配置 `/ytdlp` 命令以及 HTTP API 中 `ytdlp` 任务类型在未传自定义参数时的默认行为.
- `max_height`: 默认下载的最高视频清晰度 (按高度限制), 如 `1080`, `720`, `480`; `0` 表示不限制 (下载最佳画质). 当设置了 `format` 时此项被忽略.
- `format`: 直接指定 yt-dlp format 选择表达式, 设置后优先级高于 `max_height`, 例如 `bv*[height<=720]+ba/b`.
- `recode`: 下载后转封装的视频容器格式 (如 `mp4`), 留空则不转封装.
{{< hint info >}}
这些默认值仅在使用 `/ytdlp` 命令且未传任何自定义参数时生效. 在命令上传递自定义参数 (或在 API 中传 `flags`) 会覆盖这些默认值.
{{< /hint >}}
```toml
[ytdlp]
max_height = 1080
format = "" # 例如 "bv*[height<=720]+ba/b"
recode = "mp4" # 留空则不转封装
```
### HTTP API 配置
启用后, SaveAny-Bot 会暴露一套 HTTP API, 用于以编程方式创建/查询/取消任务. 完整的接口说明见 [HTTP API](../../usage/api).
- `enable`: 是否启用 HTTP API 服务, 默认为 `false`.
- `host`: 监听地址, 默认 `0.0.0.0`.
- `port`: 监听端口, 默认 `8080`.
- `token`: 鉴权 Token, **强烈建议设置** — 若为空, API 将在无任何鉴权的情况下暴露.
```toml
[api]
enable = false
host = "0.0.0.0"
port = 8080
token = "your-token"
```
### 日志配置
- `level`: 日志级别, 可选 `debug`, `info`, `warn`, `error`, `fatal`. 默认为 `info`.
```toml
[log]
level = "info"
```
### 存储端列表
存储端列表用于定义 Bot 支持的存储位置, 每个存储端需要指定名称、类型和相关配置, 使用双中括号语法 `[[storages]]` 定义.

View File

@@ -0,0 +1,90 @@
---
title: "命令行子命令"
weight: 21
---
# 命令行子命令
除了直接运行 `./saveany-bot` (不带子命令) 启动 Telegram Bot 外, 这个二进制文件还提供两个把本地文件上传到存储后端的辅助子命令: `upload` (一次性) 和 `watch` (持续监听).
这些子命令会读取与 Bot 相同的 `config.toml`, 初始化数据库和缓存, 然后执行任务. 它们**不会**启动 Telegram Bot 本身, 但 `telegram` 类型的存储会在需要上传时临时启动 Bot 客户端来执行上传.
## `upload` — 上传单个文件
```
saveany-bot upload -f <文件> -s <存储名> [-d <目录>] [--no-progress]
```
参数:
| 参数 | 必填 | 说明 |
|---|---|---|
| `-f, --file` | 是 | 待上传的本地文件路径 |
| `-s, --storage` | 是 | 目标存储名 (必须存在于 `config.toml`) |
| `-d, --dir` | 否 | 存储中的目标目录, 默认使用存储的 `base_path` |
| `--no-progress` | 否 | 关闭终端进度条 |
示例:
```bash
# 上传文件到 "MyAlist" 的默认目录
./saveany-bot upload -f ./movie.mp4 -s MyAlist
# 上传到指定子目录
./saveany-bot upload -f ./movie.mp4 -s MyAlist -d movies/2026
# 通过 Telegram 存储上传并关闭进度条
./saveany-bot upload -f ./photo.jpg -s MyChannel --no-progress
```
## `watch` — 监听目录并自动上传
`watch` 子命令持续监听一个本地目录, 将新建或修改的文件上传到存储后端, 并保留相对监听根目录的子目录结构.
```
saveany-bot watch -p <路径> -s <存储名> [-d <目录>] [选项]
```
参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
| `-p, --path` | *(必填)* | 要监听的本地目录 |
| `-s, --storage` | *(必填)* | 目标存储名 |
| `-d, --dir` | 存储的 `base_path` | 存储中的目标目录 |
| `-r, --recursive` | `false` | 是否递归监听子目录 |
| `--overwrite` | `false` | 覆盖存储上已有的文件, 而非跳过 |
| `--initial-scan` | `false` | 启动时将目录中已存在的文件也上传 |
| `--debounce` | `2s` | 文件最后一次写入后, 等待多久再上传 |
| `--upload-workers` | `config.workers` | 并发上传数 |
| `--retry-delay` | `3s` | 上传重试之间的延迟 |
{{< hint info >}}
写入完成检测: 监听器会按文件做防抖处理, 仅当文件大小在一个 debounce 窗口内保持不变时才上传, 因此不会上传未写完的半成品文件.
<br />
若某文件在上传过程中又被修改, 它会在当前上传完成后再上传一次, 而不是被重复排队.
{{< /hint >}}
示例:
```bash
# 递归监听 ./inbox 并且把新文件上传到 "MyAlist"
./saveany-bot watch -p ./inbox -s MyAlist -r
# 自定义目标目录并覆盖已有文件
./saveany-bot watch -p ./inbox -s MyAlist -d backup --overwrite
# 启动时把 ./inbox 中已有的内容也一并上传
./saveany-bot watch -p ./inbox -s MyAlist --initial-scan
```
### 行为说明
- 相对子目录结构会被保留: 以 `--path ./inbox` 为例, 写入 `./inbox/sub/file.txt` 的文件会被上传到 `<目标目录>/sub/file.txt`.
- `watch` 会一直运行直到被中断 (如 `Ctrl-C` / `SIGINT`), 退出前会等待所有进行中的上传完成.
- 重试次数遵循 `config.toml` 中的全局 `retry` 值, 各次重试之间间隔 `--retry-delay`.
- `telegram` 类型的存储会自动启动 Bot 客户端来执行上传.
{{< hint warning >}}
`watch` 子命令与 Bot 内的 `/watch` 命令 (监听 Telegram 聊天) 无关. 本子命令监听的是**本地文件系统目录**, 不依赖 Telegram.
{{< /hint >}}

View File

@@ -0,0 +1,75 @@
---
title: "文件命名与重名策略"
weight: 11
---
# 文件命名与重名策略
SaveAny-Bot 支持在 Telegram 中通过 `/config``/fnametmpl` 命令自定义保存文件的命名方式, 以及处理与已存在文件重名时的冲突策略.
## `/config` — 用户配置
`/config` 命令会弹出一个内联菜单, 你可以在其中修改以下两项用户级设置:
- **文件名策略** — 保存文件的命名方式
- **重名文件保存策略** — 目标存储中已存在同名文件时的处理方式
设置按用户分别保存, 对该用户后续所有的保存/转存任务生效.
### 文件名策略
| 选项 | 行为 |
|---|---|
| `默认` | 使用媒体原始文件名, 没有原始文件名时使用生成名 |
| `优先从消息生成` | 优先根据消息内容 (如 caption、文本) 生成文件名, 而非原始文件名 |
| `自定义模板` | 使用 `/fnametmpl` 设置的自定义模板渲染文件名 |
### 重名文件保存策略
| 选项 | 行为 |
|---|---|
| `始终重命名` (默认) | 保留已有文件, 将新文件以另一个名字保存 |
| `每次询问` | 每次遇到重名时通过内联按钮提示你选择 |
| `始终覆盖` | 用新文件替换已有文件 |
| `始终跳过` | 对重名文件不做处理 |
{{< hint info >}}
重名策略仅在能够检测文件是否已存在的存储后端生效. 不支持检测文件是否存在的存储后端会退化为覆盖行为.
{{< /hint >}}
## `/fnametmpl` — 自定义文件名模板
当文件名策略设置为 `自定义模板` 时, SaveAny-Bot 会用 `/fnametmpl` 配置的模板来渲染所保存文件的文件名.
```
/fnametmpl [模板]
```
- 不带参数运行 `/fnametmpl` 会显示当前模板以及帮助说明
- 带模板字符串运行则会把它设为你的文件名模板
模板使用 Go [`text/template`](https://pkg.go.dev/text/template) 语法. 可用变量如下:
| 变量 | 说明 |
|---|---|
| `{{.msgid}}` | Telegram 消息 ID |
| `{{.msgtags}}` | 消息中的标签, 以 `_` 连接输出 |
| `{{.msggen}}` | 根据消息生成的文件名 |
| `{{.msgdate}}` | 消息日期, 格式 `YYYY-MM-DD_HH-MM-SS` |
| `{{.msgraw}}` | 消息的原始文本内容 (不做处理) |
| `{{.origname}}` | 媒体的原始文件名 (如有) |
| `{{.chatid}}` | 消息所在聊天的 ID |
示例:
```
# 固定前缀 + 消息 ID + 日期
/fnametmpl 图片_{{.msgid}}_{{.msgdate}}.jpg
# 优先使用原始文件名, 没有则用生成名
/fnametmpl {{.origname}}
```
{{< hint warning >}}
模板仅在文件名策略设置为 `自定义模板` 时生效. 如果模板解析失败, SaveAny-Bot 会回退到默认的文件名生成逻辑.
{{< /hint >}}

View File

@@ -27,6 +27,45 @@ weight: 3
此外, 规则中的存储名若使用 "CHOSEN" , 则表示存储到点击按钮选择的存储端的路径下
你也可以使用 `/rule switch` 来开关规则模式. 关闭规则模式时, 所有文件都将保存到默认存储.
## 预设规则
为常见文件类型手动编写正则规则比较繁琐, 因此 Bot 内置了一组预设分类 (视频、图片、音频、文档、压缩包), 可以通过一条命令批量导入:
```
/rule preset <存储名> [基础路径]
```
参数:
- `存储名`: 目标存储名 (必须存在且你有权访问)
- `基础路径`: 可选. 各预设分类的子目录会创建在此路径下; 若不填则直接使用默认分类目录名
示例:
```
# 导入预设规则到 "MyAlist", 使用默认目录布局
/rule preset MyAlist
# 在自定义基础路径 "downloads/sorted" 下导入预设规则
/rule preset MyAlist downloads/sorted
```
此命令会为每个分类创建 `FILENAME-REGEX` 规则, 将匹配的文件路由到 `基础路径` 下对应的子目录:
| 分类 | 匹配的扩展名 | 默认目录 |
|---|---|---|
| 视频 | mp4, mkv, ts, avi, flv, mov, webm, wmv, rmvb, m2ts | `视频` |
| 图片 | jpg, jpeg, png, gif, webp, bmp | `图片` |
| 音频 | mp3, flac, wav, aac, m4a, ogg | `音频` |
| 文档 | pdf, doc, docx, xls, xlsx, ppt, pptx, txt, md, csv, epub, mobi, azw3, chm | `文档` |
| 压缩包 | zip, rar, 7z, tar, gz, bz2, xz, ... | `压缩包` |
{{< hint info >}}
导入后的预设规则就是普通的 `FILENAME-REGEX` 规则. 你可以像其他规则一样通过 `/rule` 查看或用 `/rule del <id>` 单独删除/编辑它们.
{{< /hint >}}
规则类型:
## FILENAME-REGEX

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"Target":"book.min.a22f4c7d8c2bdc5e3d6e34ba11cb59ab50ea5772594e71305bfd5a595dc78b7e.css","MediaType":"text/css","Data":{"Integrity":"sha256-oi9MfYwr3F49bjS6EctZq1DqV3JZTnEwW/1aWV3Hi34="}}
{"Target":"book.min.a643d39733d3a0ac48d6369128a52703207c5e11a74c3a70cfebfe0c15838ab8.css","MediaType":"text/css","Data":{"Integrity":"sha256-pkPTlzPToKxI1jaRKKUnAyB8XhGnTDpwz+v+DBWDirg="}}