mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-05-10 17:52:44 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc892d9370 | ||
|
|
29d523bd4f | ||
|
|
3f9c3f2a73 | ||
|
|
b796b045a8 | ||
|
|
5f7b936270 | ||
|
|
df64ec3069 | ||
|
|
d3cc56c8e6 | ||
|
|
018ed47949 | ||
|
|
09f4dd4ce7 | ||
|
|
adc64ad510 | ||
|
|
da6cf42355 |
107
.github/workflows/build-docker.yml
vendored
107
.github/workflows/build-docker.yml
vendored
@@ -9,37 +9,51 @@ env:
|
|||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: krau/saveany-bot
|
IMAGE_NAME: krau/saveany-bot
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docker-build-${{ github.repository }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.vars.outputs.version }}
|
version: ${{ steps.vars.outputs.version }}
|
||||||
git_commit: ${{ steps.vars.outputs.git_commit }}
|
major_minor: ${{ steps.vars.outputs.major_minor }}
|
||||||
|
short_sha: ${{ steps.vars.outputs.short_sha }}
|
||||||
build_time: ${{ steps.vars.outputs.build_time }}
|
build_time: ${{ steps.vars.outputs.build_time }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- id: vars
|
|
||||||
|
- name: Extract Version Components
|
||||||
|
id: vars
|
||||||
run: |
|
run: |
|
||||||
VERSION=${GITHUB_REF#refs/tags/v}
|
VERSION=${GITHUB_REF#refs/tags/v}
|
||||||
|
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
|
||||||
|
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
echo "git_commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
echo "major_minor=$MAJOR_MINOR" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
||||||
echo "build_time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
|
echo "build_time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: prepare
|
needs: prepare
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: [amd64, arm64]
|
arch: [amd64, arm64]
|
||||||
type: [default, micro, pico]
|
type: [default, micro, pico]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: ${{ matrix.arch == 'amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }}
|
runs-on: ${{ matrix.arch == 'amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }}
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -51,24 +65,40 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push by arch
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ${{ matrix.type == 'default' && './Dockerfile' || format('./Dockerfile.{0}', matrix.type) }}
|
file: ${{ matrix.type == 'default' && './Dockerfile' || format('./Dockerfile.{0}', matrix.type) }}
|
||||||
platforms: ${{ matrix.arch == 'amd64' && 'linux/amd64' || 'linux/arm64' }}
|
platforms: ${{ matrix.arch == 'amd64' && 'linux/amd64' || 'linux/arm64' }}
|
||||||
push: true
|
# 关键修改:不再使用 tags,而是通过 image output 按摘要推送
|
||||||
tags: |
|
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.type }}-${{ matrix.arch }}
|
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.type }}-${{ needs.prepare.outputs.version }}-${{ matrix.arch }}
|
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ needs.prepare.outputs.version }}
|
VERSION=${{ needs.prepare.outputs.version }}
|
||||||
GitCommit=${{ needs.prepare.outputs.git_commit }}
|
GitCommit=${{ needs.prepare.outputs.short_sha }}
|
||||||
BuildTime=${{ needs.prepare.outputs.build_time }}
|
BuildTime=${{ needs.prepare.outputs.build_time }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name:
|
||||||
|
Export digest
|
||||||
|
# 将 digest 写入文件,供后续步骤读取
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
echo "$digest" > /tmp/digests/digest
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digest-${{ matrix.type }}-${{ matrix.arch }}
|
||||||
|
path: /tmp/digests/digest
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
create-manifest:
|
create-manifest:
|
||||||
needs: [prepare, build]
|
needs: [prepare, build]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -85,24 +115,49 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create and push manifest group
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/digests
|
||||||
|
pattern: digest-${{ matrix.type }}-*
|
||||||
|
merge-multiple: false
|
||||||
|
|
||||||
|
- name: Create and push manifest lists
|
||||||
run: |
|
run: |
|
||||||
TAG_PREFIX="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
REPO="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
||||||
VERSION="${{ needs.prepare.outputs.version }}"
|
VERSION="${{ needs.prepare.outputs.version }}"
|
||||||
|
MAJOR_MINOR="${{ needs.prepare.outputs.major_minor }}"
|
||||||
|
SHA="${{ needs.prepare.outputs.short_sha }}"
|
||||||
TYPE="${{ matrix.type }}"
|
TYPE="${{ matrix.type }}"
|
||||||
|
|
||||||
|
DIGEST_AMD64=$(cat /tmp/digests/digest-${TYPE}-amd64/digest)
|
||||||
|
DIGEST_ARM64=$(cat /tmp/digests/digest-${TYPE}-arm64/digest)
|
||||||
|
|
||||||
|
echo "Found digests for $TYPE:"
|
||||||
|
echo "AMD64: $DIGEST_AMD64"
|
||||||
|
echo "ARM64: $DIGEST_ARM64"
|
||||||
|
|
||||||
|
TAGS=()
|
||||||
|
|
||||||
if [ "$TYPE" == "default" ]; then
|
if [ "$TYPE" == "default" ]; then
|
||||||
TARGET_TAGS=("$TAG_PREFIX:latest" "$TAG_PREFIX:$VERSION")
|
TAGS+=("$REPO:latest")
|
||||||
|
TAGS+=("$REPO:$VERSION")
|
||||||
|
TAGS+=("$REPO:$MAJOR_MINOR")
|
||||||
|
TAGS+=("$REPO:sha-$SHA")
|
||||||
else
|
else
|
||||||
TARGET_TAGS=("$TAG_PREFIX:$TYPE" "$TAG_PREFIX:$TYPE-latest" "$TAG_PREFIX:$TYPE-$VERSION")
|
TAGS+=("$REPO:$TYPE")
|
||||||
|
TAGS+=("$REPO:$TYPE-latest")
|
||||||
|
TAGS+=("$REPO:$TYPE-$VERSION")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for final_tag in "${TARGET_TAGS[@]}"; do
|
SRC_AMD64="${REPO}@${DIGEST_AMD64}"
|
||||||
docker buildx imagetools create -t "$final_tag" \
|
SRC_ARM64="${REPO}@${DIGEST_ARM64}"
|
||||||
"$TAG_PREFIX:$TYPE-amd64" \
|
|
||||||
"$TAG_PREFIX:$TYPE-arm64"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Inspect manifest
|
echo "Creating manifest list with sources:"
|
||||||
run: |
|
echo " $SRC_AMD64"
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.type }}
|
echo " $SRC_ARM64"
|
||||||
|
|
||||||
|
for TAG in "${TAGS[@]}"; do
|
||||||
|
echo "Pushing tag: $TAG"
|
||||||
|
docker buildx imagetools create -t "$TAG" "$SRC_AMD64" "$SRC_ARM64"
|
||||||
|
done
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
--mount=type=cache,target=/go/pkg \
|
--mount=type=cache,target=/go/pkg \
|
||||||
CGO_ENABLED=0 \
|
CGO_ENABLED=0 \
|
||||||
go build -trimpath \
|
go build -trimpath \
|
||||||
-tags=no_jsparser,no_minio \
|
-tags=no_jsparser,no_minio,no_bubbletea \
|
||||||
-ldflags=" \
|
-ldflags=" \
|
||||||
-s -w \
|
-s -w \
|
||||||
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
--mount=type=cache,target=/go/pkg \
|
--mount=type=cache,target=/go/pkg \
|
||||||
CGO_ENABLED=0 \
|
CGO_ENABLED=0 \
|
||||||
go build -trimpath \
|
go build -trimpath \
|
||||||
-tags=no_jsparser,no_minio,sqlite_glebarez \
|
-tags=no_jsparser,no_minio,sqlite_glebarez,no_bubbletea \
|
||||||
-ldflags=" \
|
-ldflags=" \
|
||||||
-s -w \
|
-s -w \
|
||||||
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ectx *ext.Context
|
||||||
|
|
||||||
|
func ExtContext() *ext.Context {
|
||||||
|
return ectx
|
||||||
|
}
|
||||||
|
|
||||||
func Init(ctx context.Context) <-chan struct{} {
|
func Init(ctx context.Context) <-chan struct{} {
|
||||||
log.FromContext(ctx).Info("初始化 Bot...")
|
log.FromContext(ctx).Info("初始化 Bot...")
|
||||||
resultChan := make(chan struct {
|
resultChan := make(chan struct {
|
||||||
@@ -88,6 +94,7 @@ func Init(ctx context.Context) <-chan struct{} {
|
|||||||
log.FromContext(ctx).Fatalf("初始化 Bot 失败: %s", result.err)
|
log.FromContext(ctx).Fatalf("初始化 Bot 失败: %s", result.err)
|
||||||
}
|
}
|
||||||
handlers.Register(result.client.Dispatcher)
|
handlers.Register(result.client.Dispatcher)
|
||||||
|
ectx = result.client.CreateContext()
|
||||||
log.FromContext(ctx).Info("Bot 初始化完成")
|
log.FromContext(ctx).Info("Bot 初始化完成")
|
||||||
}
|
}
|
||||||
return shouldRestart
|
return shouldRestart
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func Register(disp dispatcher.Dispatcher) {
|
|||||||
disp.AddHandler(handlers.NewMessage(filters.Message.Media, handleSilentMode(handleMediaMessage, handleSilentSaveMedia)))
|
disp.AddHandler(handlers.NewMessage(filters.Message.Media, handleSilentMode(handleMediaMessage, handleSilentSaveMedia)))
|
||||||
disp.AddHandler(handlers.NewMessage(filters.Message.Text, handleSilentMode(handleTextMessage, handleSilentSaveText)))
|
disp.AddHandler(handlers.NewMessage(filters.Message.Text, handleSilentMode(handleTextMessage, handleSilentSaveText)))
|
||||||
|
|
||||||
if config.C().Telegram.Userbot.Enable {
|
if config.C().Telegram.Userbot.Enable {
|
||||||
go listenMediaMessageEvent(userclient.GetMediaMessageCh())
|
go listenMediaMessageEvent(userclient.GetMediaMessageCh())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
|||||||
|
|
||||||
tctx := ctx
|
tctx := ctx
|
||||||
if config.C().Telegram.Userbot.Enable {
|
if config.C().Telegram.Userbot.Enable {
|
||||||
tctx = uc.GetCtx()
|
if uc.GetCtx() != nil {
|
||||||
|
tctx = uc.GetCtx()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, link := range msgLinks {
|
for _, link := range msgLinks {
|
||||||
|
|||||||
@@ -152,6 +152,9 @@ func handleUnwatchCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
|
func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
|
||||||
|
if userclient.GetCtx() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
logger := log.FromContext(userclient.GetCtx())
|
logger := log.FromContext(userclient.GetCtx())
|
||||||
for event := range ch {
|
for event := range ch {
|
||||||
logger.Debug("Received media message event", "chat_id", event.ChatID, "file_name", event.File.Name())
|
logger.Debug("Received media message event", "chat_id", event.ChatID, "file_name", event.File.Name())
|
||||||
|
|||||||
@@ -23,23 +23,16 @@ var uc *gotgproto.Client
|
|||||||
var ectx *ext.Context
|
var ectx *ext.Context
|
||||||
|
|
||||||
func GetCtx() *ext.Context {
|
func GetCtx() *ext.Context {
|
||||||
if uc == nil {
|
|
||||||
panic("User client is not initialized, please call Login first")
|
|
||||||
}
|
|
||||||
if ectx != nil {
|
if ectx != nil {
|
||||||
return ectx
|
return ectx
|
||||||
}
|
}
|
||||||
|
if uc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
ectx = uc.CreateContext()
|
ectx = uc.CreateContext()
|
||||||
return ectx
|
return ectx
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetClient() *gotgproto.Client {
|
|
||||||
if uc == nil {
|
|
||||||
panic("User client is not initialized, please call Login first")
|
|
||||||
}
|
|
||||||
return uc
|
|
||||||
}
|
|
||||||
|
|
||||||
func Login(ctx context.Context) (*gotgproto.Client, error) {
|
func Login(ctx context.Context) (*gotgproto.Client, error) {
|
||||||
log.FromContext(ctx).Debug("Logging in user client")
|
log.FromContext(ctx).Debug("Logging in user client")
|
||||||
if uc != nil {
|
if uc != nil {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/cmd/upload"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +15,11 @@ var rootCmd = &cobra.Command{
|
|||||||
Run: Run,
|
Run: Run,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
config.RegisterFlags(rootCmd)
|
||||||
|
upload.Register(rootCmd)
|
||||||
|
}
|
||||||
|
|
||||||
func Execute(ctx context.Context) {
|
func Execute(ctx context.Context) {
|
||||||
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func Run(cmd *cobra.Command, _ []string) {
|
|||||||
})
|
})
|
||||||
ctx = log.WithContext(ctx, logger)
|
ctx = log.WithContext(ctx, logger)
|
||||||
|
|
||||||
exitChan, err := initAll(ctx)
|
exitChan, err := initAll(ctx, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Init failed", "error", err)
|
logger.Fatal("Init failed", "error", err)
|
||||||
}
|
}
|
||||||
@@ -51,8 +51,9 @@ func Run(cmd *cobra.Command, _ []string) {
|
|||||||
cleanCache()
|
cleanCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initAll(ctx context.Context) (<-chan struct{}, error) {
|
func initAll(ctx context.Context, cmd *cobra.Command) (<-chan struct{}, error) {
|
||||||
if err := config.Init(ctx); err != nil {
|
configFile := config.GetConfigFile(cmd)
|
||||||
|
if err := config.Init(ctx, configFile); err != nil {
|
||||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||||
}
|
}
|
||||||
cache.Init()
|
cache.Init()
|
||||||
|
|||||||
128
cmd/upload/cmd.go
Normal file
128
cmd/upload/cmd.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package upload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/cache"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/ioutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
|
stortype "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var uploadCmd = &cobra.Command{
|
||||||
|
Use: "upload",
|
||||||
|
Short: "upload local files to storage",
|
||||||
|
RunE: Upload,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(root *cobra.Command) {
|
||||||
|
uploadCmd.Flags().StringP("file", "f", "", "file path to upload")
|
||||||
|
uploadCmd.MarkFlagRequired("file")
|
||||||
|
uploadCmd.Flags().StringP("storage", "s", "", "storage name to upload to")
|
||||||
|
uploadCmd.MarkFlagRequired("storage")
|
||||||
|
uploadCmd.Flags().StringP("dir", "d", "", "storage dir to upload to, default is the base_path of the storage")
|
||||||
|
uploadCmd.Flags().Bool("no-progress", false, "disable progress bar")
|
||||||
|
root.AddCommand(uploadCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Upload(cmd *cobra.Command, args []string) error {
|
||||||
|
storname, err := cmd.Flags().GetString("storage")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fp, err := cmd.Flags().GetString("file")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dirPath, err := cmd.Flags().GetString("dir")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
noProgress, err := cmd.Flags().GetBool("no-progress")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := cmd.Context()
|
||||||
|
log := log.FromContext(ctx)
|
||||||
|
configFile := config.GetConfigFile(cmd)
|
||||||
|
if err := config.Init(ctx, configFile); err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
cache.Init()
|
||||||
|
database.Init(ctx)
|
||||||
|
|
||||||
|
stor, err := storage.GetStorageByName(ctx, storname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to get storage", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch stor.Type() {
|
||||||
|
case stortype.Telegram:
|
||||||
|
bot.Init(ctx)
|
||||||
|
default:
|
||||||
|
// placeholder for other storage types that may need special initialization
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filepath.Clean(fp))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to open file", "error", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
fileInfo, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to get file info", "error", err)
|
||||||
|
}
|
||||||
|
fileName := fileInfo.Name()
|
||||||
|
fileSize := fileInfo.Size()
|
||||||
|
|
||||||
|
uploadPath := stor.JoinStoragePath(path.Join(dirPath, fileName))
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, ctxkey.ContentLength, fileSize)
|
||||||
|
ctx = tgutil.ExtWithContext(ctx, bot.ExtContext())
|
||||||
|
|
||||||
|
// Create progress reader and UI
|
||||||
|
var reader io.Reader
|
||||||
|
var progressUI *UploadProgress
|
||||||
|
log.Info("Uploading file...", "file", fp, "to", storname, "as", uploadPath)
|
||||||
|
|
||||||
|
if !noProgress && fileSize > 0 {
|
||||||
|
progressUI = NewUploadProgress(ctx, fileName, fileSize)
|
||||||
|
progressUI.Start()
|
||||||
|
|
||||||
|
reader = ioutil.NewProgressReader(file, fileSize, func(read int64, total int64) {
|
||||||
|
if total > 0 {
|
||||||
|
progressUI.UpdateProgress(float64(read) / float64(total))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
reader = file
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stor.Save(ctx, reader, uploadPath); err != nil {
|
||||||
|
if progressUI != nil {
|
||||||
|
progressUI.SetError(err)
|
||||||
|
progressUI.Wait()
|
||||||
|
}
|
||||||
|
log.Fatal("Failed to upload file", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if progressUI != nil {
|
||||||
|
progressUI.Done()
|
||||||
|
progressUI.Wait()
|
||||||
|
}
|
||||||
|
log.Info("File uploaded successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
35
cmd/upload/progress_stub.go
Normal file
35
cmd/upload/progress_stub.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//go:build no_bubbletea
|
||||||
|
|
||||||
|
package upload
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type uploadModel struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadProgress manages the progress UI for uploads
|
||||||
|
type UploadProgress struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUploadProgress creates a new upload progress tracker
|
||||||
|
func NewUploadProgress(ctx context.Context, fileName string, fileSize int64) *UploadProgress {
|
||||||
|
return &UploadProgress{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the progress UI in a goroutine and returns immediately
|
||||||
|
func (up *UploadProgress) Start() {}
|
||||||
|
|
||||||
|
// UpdateProgress updates the progress bar with a new percentage (0.0 - 1.0)
|
||||||
|
func (up *UploadProgress) UpdateProgress(percent float64) {}
|
||||||
|
|
||||||
|
// SetError sets an error and quits the progress UI
|
||||||
|
func (up *UploadProgress) SetError(err error) {}
|
||||||
|
|
||||||
|
// Done signals that the upload is complete
|
||||||
|
func (up *UploadProgress) Done() {}
|
||||||
|
|
||||||
|
// Wait waits for the progress UI to finish
|
||||||
|
func (up *UploadProgress) Wait() {}
|
||||||
|
|
||||||
|
// Quit quits the progress UI
|
||||||
|
func (up *UploadProgress) Quit() {}
|
||||||
178
cmd/upload/progress_tea.go
Normal file
178
cmd/upload/progress_tea.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
//go:build !no_bubbletea
|
||||||
|
|
||||||
|
package upload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/progress"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262"))
|
||||||
|
)
|
||||||
|
|
||||||
|
// progressMsg is sent to update the progress bar
|
||||||
|
type progressMsg float64
|
||||||
|
|
||||||
|
// progressErrMsg is sent when an error occurs
|
||||||
|
type progressErrMsg struct{ err error }
|
||||||
|
|
||||||
|
// progressDoneMsg is sent when the upload is complete
|
||||||
|
type progressDoneMsg struct{}
|
||||||
|
|
||||||
|
// uploadModel is the bubbletea model for the upload progress UI
|
||||||
|
type uploadModel struct {
|
||||||
|
progress progress.Model
|
||||||
|
fileName string
|
||||||
|
fileSize int64
|
||||||
|
bytesRead int64
|
||||||
|
err error
|
||||||
|
done bool
|
||||||
|
quitting bool
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUploadModel(fileName string, fileSize int64) uploadModel {
|
||||||
|
p := progress.New(
|
||||||
|
progress.WithDefaultGradient(),
|
||||||
|
progress.WithWidth(50),
|
||||||
|
)
|
||||||
|
return uploadModel{
|
||||||
|
progress: p,
|
||||||
|
fileName: fileName,
|
||||||
|
fileSize: fileSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uploadModel) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uploadModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.progress.Width = min(msg.Width-10, 80)
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case progressMsg:
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
percent := float64(msg)
|
||||||
|
m.bytesRead = int64(percent * float64(m.fileSize))
|
||||||
|
|
||||||
|
cmds = append(cmds, m.progress.SetPercent(percent))
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
|
||||||
|
case progressErrMsg:
|
||||||
|
m.err = msg.err
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case progressDoneMsg:
|
||||||
|
m.done = true
|
||||||
|
m.progress.SetPercent(1.0)
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case progress.FrameMsg:
|
||||||
|
// Don't process frame messages if we're done or quitting
|
||||||
|
if m.done || m.quitting {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
progressModel, cmd := m.progress.Update(msg)
|
||||||
|
m.progress = progressModel.(progress.Model)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uploadModel) View() string {
|
||||||
|
if m.err != nil {
|
||||||
|
return fmt.Sprintf("\n ❌ Error: %s\n\n", m.err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("\n")
|
||||||
|
|
||||||
|
// File info
|
||||||
|
sb.WriteString(fmt.Sprintf(" 📁 %s\n", m.fileName))
|
||||||
|
sb.WriteString(fmt.Sprintf(" 📊 %s / %s\n\n",
|
||||||
|
humanize.Bytes(uint64(m.bytesRead)),
|
||||||
|
humanize.Bytes(uint64(m.fileSize)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(m.progress.View())
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
|
||||||
|
if m.done {
|
||||||
|
sb.WriteString(" √ Upload complete!\n\n")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(helpStyle.Render(" Press Ctrl+C to cancel"))
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadProgress manages the progress UI for uploads
|
||||||
|
type UploadProgress struct {
|
||||||
|
program *tea.Program
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUploadProgress creates a new upload progress tracker
|
||||||
|
func NewUploadProgress(ctx context.Context, fileName string, fileSize int64) *UploadProgress {
|
||||||
|
model := newUploadModel(fileName, fileSize)
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
p := tea.NewProgram(
|
||||||
|
model,
|
||||||
|
tea.WithoutSignalHandler(),
|
||||||
|
tea.WithContext(ctx),
|
||||||
|
tea.WithInput(nil), // Disable keyboard input, rely on context cancellation
|
||||||
|
)
|
||||||
|
return &UploadProgress{
|
||||||
|
program: p,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the progress UI in a goroutine and returns immediately
|
||||||
|
func (up *UploadProgress) Start() {
|
||||||
|
go func() {
|
||||||
|
up.program.Run()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProgress updates the progress bar with a new percentage (0.0 - 1.0)
|
||||||
|
func (up *UploadProgress) UpdateProgress(percent float64) {
|
||||||
|
up.program.Send(progressMsg(percent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetError sets an error and quits the progress UI
|
||||||
|
func (up *UploadProgress) SetError(err error) {
|
||||||
|
up.program.Send(progressErrMsg{err: err})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done signals that the upload is complete
|
||||||
|
func (up *UploadProgress) Done() {
|
||||||
|
up.program.Send(progressDoneMsg{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the progress UI to finish
|
||||||
|
func (up *UploadProgress) Wait() {
|
||||||
|
up.program.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit quits the progress UI
|
||||||
|
func (up *UploadProgress) Quit() {
|
||||||
|
up.program.Quit()
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ lifetime:
|
|||||||
cleaning_cache: "正在清理缓存 {{.Path}}"
|
cleaning_cache: "正在清理缓存 {{.Path}}"
|
||||||
bye: 已退出
|
bye: 已退出
|
||||||
config:
|
config:
|
||||||
loaded_storages: "已加载 {{.Count}} 个存储后端"
|
|
||||||
err:
|
err:
|
||||||
invalid_cache_dir: "无效的缓存目录: {{.Path}},请检查配置文件"
|
invalid_cache_dir: "无效的缓存目录: {{.Path}},请检查配置文件"
|
||||||
duplicate_storage_name: "存储名称 '{{.Name}}' 重复,请检查配置文件"
|
duplicate_storage_name: "存储名称 '{{.Name}}' 重复,请检查配置文件"
|
||||||
@@ -35,11 +34,13 @@ bot:
|
|||||||
/config - 修改配置
|
/config - 修改配置
|
||||||
/fnametmpl - 设置文件自定义命名模板
|
/fnametmpl - 设置文件自定义命名模板
|
||||||
/parser - 管理解析器插件
|
/parser - 管理解析器插件
|
||||||
|
/task - 管理任务队列
|
||||||
/watch - 监听聊天并自动保存 (UserBot)
|
/watch - 监听聊天并自动保存 (UserBot)
|
||||||
|
/unwatch - 取消监听聊天 (UserBot)
|
||||||
|
/lswatch - 列出正在监听的聊天 (UserBot)
|
||||||
/update - 检查更新并升级
|
/update - 检查更新并升级
|
||||||
|
|
||||||
使用帮助: https://sabot.unv.app/usage
|
使用帮助: https://sabot.unv.app/usage
|
||||||
反馈群组: https://t.me/ProjectSaveAny
|
|
||||||
save_help_text: |
|
save_help_text: |
|
||||||
使用方法:
|
使用方法:
|
||||||
|
|
||||||
|
|||||||
65
common/utils/ioutil/progress_reader.go
Normal file
65
common/utils/ioutil/progress_reader.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package ioutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ io.ReadSeeker = (*ProgressReadSeeker)(nil)
|
||||||
|
|
||||||
|
// ProgressReadSeeker wraps an io.ReadSeeker and tracks read progress
|
||||||
|
type ProgressReadSeeker struct {
|
||||||
|
reader io.ReadSeeker
|
||||||
|
total atomic.Int64
|
||||||
|
read atomic.Int64
|
||||||
|
onProgress func(read int64, total int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek implements io.ReadSeeker.
|
||||||
|
func (pr *ProgressReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
return pr.reader.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProgressReader creates a new ProgressReader
|
||||||
|
func NewProgressReader(rs io.ReadSeeker, total int64, onProgress func(read int64, total int64)) *ProgressReadSeeker {
|
||||||
|
prs := &ProgressReadSeeker{
|
||||||
|
reader: rs,
|
||||||
|
total: atomic.Int64{},
|
||||||
|
read: atomic.Int64{},
|
||||||
|
onProgress: onProgress,
|
||||||
|
}
|
||||||
|
prs.total.Store(total)
|
||||||
|
return prs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements io.Reader
|
||||||
|
func (pr *ProgressReadSeeker) Read(p []byte) (int, error) {
|
||||||
|
n, err := pr.reader.Read(p)
|
||||||
|
if n > 0 {
|
||||||
|
pr.read.Add(int64(n))
|
||||||
|
read := pr.read.Load()
|
||||||
|
|
||||||
|
if pr.onProgress != nil {
|
||||||
|
pr.onProgress(read, pr.total.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress returns the current progress as a float64 between 0 and 1
|
||||||
|
func (pr *ProgressReadSeeker) Progress() float64 {
|
||||||
|
if pr.total.Load() <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(pr.read.Load()) / float64(pr.total.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read returns the number of bytes read so far
|
||||||
|
func (pr *ProgressReadSeeker) BytesRead() int64 {
|
||||||
|
return pr.read.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total returns the total number of bytes
|
||||||
|
func (pr *ProgressReadSeeker) Total() int64 {
|
||||||
|
return pr.total.Load()
|
||||||
|
}
|
||||||
83
config/flags.go
Normal file
83
config/flags.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterFlags(cmd *cobra.Command) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
// 基础配置
|
||||||
|
flags.StringP("config", "c", "", "config file path")
|
||||||
|
flags.StringP("lang", "l", "", "language (e.g., zh-Hans, en)")
|
||||||
|
flags.IntP("workers", "w", 0, "number of workers")
|
||||||
|
flags.Int("retry", 0, "retry times")
|
||||||
|
flags.Int("threads", 0, "number of threads")
|
||||||
|
flags.Bool("stream", false, "enable stream mode")
|
||||||
|
flags.Bool("no-clean-cache", false, "do not clean cache on exit")
|
||||||
|
flags.String("proxy", "", "proxy URL (http, https, socks5, socks5h)")
|
||||||
|
|
||||||
|
// Telegram 配置
|
||||||
|
flags.String("telegram-token", "", "telegram bot token")
|
||||||
|
flags.Int("telegram-app-id", 0, "telegram app id")
|
||||||
|
flags.String("telegram-app-hash", "", "telegram app hash")
|
||||||
|
flags.Int("telegram-rpc-retry", 0, "telegram rpc retry times")
|
||||||
|
flags.Bool("telegram-userbot-enable", false, "enable userbot")
|
||||||
|
flags.String("telegram-userbot-session", "", "userbot session path")
|
||||||
|
flags.Bool("telegram-proxy-enable", false, "enable telegram proxy")
|
||||||
|
flags.String("telegram-proxy-url", "", "telegram proxy URL")
|
||||||
|
|
||||||
|
// 数据库配置
|
||||||
|
flags.String("db-path", "", "database path")
|
||||||
|
flags.String("db-session", "", "session database path")
|
||||||
|
|
||||||
|
// 临时目录配置
|
||||||
|
flags.String("temp-base-path", "", "temp directory base path")
|
||||||
|
|
||||||
|
// Parser 配置
|
||||||
|
flags.Bool("parser-plugin-enable", false, "enable parser plugins")
|
||||||
|
flags.StringSlice("parser-plugin-dirs", nil, "parser plugin directories")
|
||||||
|
flags.String("parser-proxy", "", "parser proxy URL")
|
||||||
|
|
||||||
|
// 绑定到 viper
|
||||||
|
bindFlags(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindFlags(cmd *cobra.Command) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
viper.BindPFlag("lang", flags.Lookup("lang"))
|
||||||
|
viper.BindPFlag("workers", flags.Lookup("workers"))
|
||||||
|
viper.BindPFlag("retry", flags.Lookup("retry"))
|
||||||
|
viper.BindPFlag("threads", flags.Lookup("threads"))
|
||||||
|
viper.BindPFlag("stream", flags.Lookup("stream"))
|
||||||
|
viper.BindPFlag("no_clean_cache", flags.Lookup("no-clean-cache"))
|
||||||
|
viper.BindPFlag("proxy", flags.Lookup("proxy"))
|
||||||
|
|
||||||
|
// Telegram
|
||||||
|
viper.BindPFlag("telegram.token", flags.Lookup("telegram-token"))
|
||||||
|
viper.BindPFlag("telegram.app_id", flags.Lookup("telegram-app-id"))
|
||||||
|
viper.BindPFlag("telegram.app_hash", flags.Lookup("telegram-app-hash"))
|
||||||
|
viper.BindPFlag("telegram.rpc_retry", flags.Lookup("telegram-rpc-retry"))
|
||||||
|
viper.BindPFlag("telegram.userbot.enable", flags.Lookup("telegram-userbot-enable"))
|
||||||
|
viper.BindPFlag("telegram.userbot.session", flags.Lookup("telegram-userbot-session"))
|
||||||
|
viper.BindPFlag("telegram.proxy.enable", flags.Lookup("telegram-proxy-enable"))
|
||||||
|
viper.BindPFlag("telegram.proxy.url", flags.Lookup("telegram-proxy-url"))
|
||||||
|
|
||||||
|
// database
|
||||||
|
viper.BindPFlag("db.path", flags.Lookup("db-path"))
|
||||||
|
viper.BindPFlag("db.session", flags.Lookup("db-session"))
|
||||||
|
// 临时目录
|
||||||
|
viper.BindPFlag("temp.base_path", flags.Lookup("temp-base-path"))
|
||||||
|
|
||||||
|
// Parser
|
||||||
|
viper.BindPFlag("parser.plugin_enable", flags.Lookup("parser-plugin-enable"))
|
||||||
|
viper.BindPFlag("parser.plugin_dirs", flags.Lookup("parser-plugin-dirs"))
|
||||||
|
viper.BindPFlag("parser.proxy", flags.Lookup("parser-proxy"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigFile(cmd *cobra.Command) string {
|
||||||
|
configFile, _ := cmd.Flags().GetString("config")
|
||||||
|
return configFile
|
||||||
|
}
|
||||||
@@ -52,16 +52,39 @@ func (c Config) GetStorageByName(name string) storage.StorageConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Init(ctx context.Context) error {
|
func Init(ctx context.Context, configFile ...string) error {
|
||||||
viper.SetConfigName("config")
|
|
||||||
viper.AddConfigPath(".")
|
|
||||||
viper.AddConfigPath("/etc/saveany/")
|
|
||||||
viper.SetConfigType("toml")
|
viper.SetConfigType("toml")
|
||||||
viper.SetEnvPrefix("SAVEANY")
|
viper.SetEnvPrefix("SAVEANY")
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
replacer := strings.NewReplacer(".", "_")
|
replacer := strings.NewReplacer(".", "_")
|
||||||
viper.SetEnvKeyReplacer(replacer)
|
viper.SetEnvKeyReplacer(replacer)
|
||||||
|
|
||||||
|
// 如果指定了配置文件路径,则使用指定的配置文件
|
||||||
|
// 配置文件支持传入一个 http(s) URL 地址
|
||||||
|
if len(configFile) > 0 && configFile[0] != "" {
|
||||||
|
cfg := configFile[0]
|
||||||
|
if strings.HasPrefix(cfg, "http://") || strings.HasPrefix(cfg, "https://") {
|
||||||
|
// 使用远程配置文件
|
||||||
|
resp, err := http.Get(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch remote config file: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("failed to fetch remote config file: status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if err := viper.ReadConfig(resp.Body); err != nil {
|
||||||
|
return fmt.Errorf("failed to read remote config file: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viper.SetConfigFile(cfg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
viper.AddConfigPath("/etc/saveany/")
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfigs := map[string]any{
|
defaultConfigs := map[string]any{
|
||||||
// 基础配置
|
// 基础配置
|
||||||
"lang": "zh-Hans",
|
"lang": "zh-Hans",
|
||||||
@@ -125,13 +148,6 @@ func Init(ctx context.Context) error {
|
|||||||
storageNames[storage.GetName()] = struct{}{}
|
storageNames[storage.GetName()] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(i18n.TWithoutInit(cfg.Lang, i18nk.ConfigLoadedStorages, map[string]any{
|
|
||||||
"Count": len(cfg.Storages),
|
|
||||||
}))
|
|
||||||
for _, storage := range cfg.Storages {
|
|
||||||
fmt.Printf(" - %s (%s)\n", storage.GetName(), storage.GetType())
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Workers < 1 {
|
if cfg.Workers < 1 {
|
||||||
cfg.Workers = 1
|
cfg.Workers = 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ weight: 20
|
|||||||
|
|
||||||
# 参与开发
|
# 参与开发
|
||||||
|
|
||||||
在开始之前, 请 Fork 本项目, 并克隆到本地, 并确保 Go 版本 >= 1.23.
|
在开始之前, 请 Fork 本项目, 并克隆到本地, 安装好 Go 开发环境.
|
||||||
|
|
||||||
以下是一些贡献代码的指南或建议, 你不必完全遵守, 但将有助于快速 review 并合并你的提交:
|
以下是一些贡献代码的指南或建议, 你不必完全遵守, 但将有助于快速 review 并合并你的提交:
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,14 @@ https://s3.example.com/your_bucket_name/path/to/s3/your_file
|
|||||||
不支持 Stream 模式.
|
不支持 Stream 模式.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
chat_id = "123456789" # Telegram 聊天 ID, Bot 将把文件发送到这个聊天
|
# Telegram 聊天 ID, Bot 将把文件发送到这个聊天
|
||||||
force_file = false # 是否强制使用文件方式发送, 默认为 false.
|
chat_id = "123456789"
|
||||||
skip_large = true # 是否跳过大文件, 默认为 true. 如果启用, 超过 Telegram 限制的文件将不会上传.
|
# 是否强制使用文件方式发送, 默认为 false
|
||||||
spilt_size_mb = 2000 # 分卷大小, 单位 MB, 默认为 2000 MB (2 GB). 超过该大小的文件将被分割成多个部分上传.(使用 zip 格式)
|
force_file = false
|
||||||
|
# 是否跳过大文件, 默认为 false. 如果启用, 超过 Telegram 限制的文件将不会上传.
|
||||||
|
skip_large = false
|
||||||
|
# 分卷大小, 单位 MB, 默认为 2000 MB (2 GB).
|
||||||
|
# 超过该大小的文件将被分割成多个部分上传.(使用 zip 格式)
|
||||||
|
# 当 skip_large 启用时, 该选项无效.
|
||||||
|
spilt_size_mb = 2000
|
||||||
```
|
```
|
||||||
11
go.mod
11
go.mod
@@ -6,7 +6,11 @@ require (
|
|||||||
github.com/blang/semver v3.5.1+incompatible
|
github.com/blang/semver v3.5.1+incompatible
|
||||||
github.com/celestix/gotgproto v1.0.0-beta22
|
github.com/celestix/gotgproto v1.0.0-beta22
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10
|
github.com/gabriel-vasile/mimetype v1.4.10
|
||||||
github.com/goccy/go-yaml v1.18.0
|
github.com/goccy/go-yaml v1.18.0
|
||||||
github.com/gotd/contrib v0.21.1
|
github.com/gotd/contrib v0.21.1
|
||||||
@@ -31,7 +35,7 @@ require (
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
@@ -39,7 +43,7 @@ require (
|
|||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
github.com/deckarep/golang-set/v2 v2.7.0 // indirect
|
github.com/deckarep/golang-set/v2 v2.7.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
@@ -67,9 +71,12 @@ require (
|
|||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // 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/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/ncruces/julianday v1.0.0 // indirect
|
github.com/ncruces/julianday v1.0.0 // indirect
|
||||||
|
|||||||
15
go.sum
15
go.sum
@@ -42,8 +42,14 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
|||||||
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/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
|
github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
|
||||||
github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts=
|
github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
||||||
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
|
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
@@ -76,6 +82,8 @@ github.com/duke-git/lancet/v2 v2.3.7 h1:nnNBA9KyoqwbPm4nFmEFVIbXeAmpqf6IDCH45+HH
|
|||||||
github.com/duke-git/lancet/v2 v2.3.7/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
github.com/duke-git/lancet/v2 v2.3.7/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=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
@@ -168,6 +176,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
|||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||||
@@ -180,6 +190,10 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
|
|||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
|
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
|
||||||
@@ -303,6 +317,7 @@ golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|||||||
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import (
|
|||||||
var UserStorages = make(map[int64][]Storage)
|
var UserStorages = make(map[int64][]Storage)
|
||||||
|
|
||||||
// GetStorageByName returns storage by name from cache or creates new one
|
// GetStorageByName returns storage by name from cache or creates new one
|
||||||
func getStorageByName(ctx context.Context, name string) (Storage, error) {
|
// It should NOT be used to get storage for user, use GetStorageByUserIDAndName instead
|
||||||
|
func GetStorageByName(ctx context.Context, name string) (Storage, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return nil, ErrStorageNameEmpty
|
return nil, ErrStorageNameEmpty
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ func GetStorageByUserIDAndName(ctx context.Context, chatID int64, name string) (
|
|||||||
return nil, fmt.Errorf("no storage %s for user %d", name, chatID)
|
return nil, fmt.Errorf("no storage %s for user %d", name, chatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getStorageByName(ctx, name)
|
return GetStorageByName(ctx, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserStorages(ctx context.Context, chatID int64) []Storage {
|
func GetUserStorages(ctx context.Context, chatID int64) []Storage {
|
||||||
@@ -55,7 +56,7 @@ func GetUserStorages(ctx context.Context, chatID int64) []Storage {
|
|||||||
}
|
}
|
||||||
var storages []Storage
|
var storages []Storage
|
||||||
for _, name := range config.C().GetStorageNamesByUserID(chatID) {
|
for _, name := range config.C().GetStorageNamesByUserID(chatID) {
|
||||||
storage, err := getStorageByName(ctx, name)
|
storage, err := GetStorageByName(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,7 @@ func LoadStorages(ctx context.Context) {
|
|||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
logger.Info("加载存储...")
|
logger.Info("加载存储...")
|
||||||
for _, storage := range config.C().Storages {
|
for _, storage := range config.C().Storages {
|
||||||
_, err := getStorageByName(ctx, storage.GetName())
|
_, err := GetStorageByName(ctx, storage.GetName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("加载存储 %s 失败: %v", storage.GetName(), err)
|
logger.Errorf("加载存储 %s 失败: %v", storage.GetName(), err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
|
// Init 只应该在创建存储时调用一次
|
||||||
Init(ctx context.Context, cfg storcfg.StorageConfig) error
|
Init(ctx context.Context, cfg storcfg.StorageConfig) error
|
||||||
Type() storenum.StorageType
|
Type() storenum.StorageType
|
||||||
Name() string
|
Name() string
|
||||||
@@ -42,6 +43,7 @@ var storageConstructors = map[storenum.StorageType]StorageConstructor{
|
|||||||
storenum.Telegram: func() Storage { return new(telegram.Telegram) },
|
storenum.Telegram: func() Storage { return new(telegram.Telegram) },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewStorage creates a new storage instance based on the provided config and initializes it
|
||||||
func NewStorage(ctx context.Context, cfg storcfg.StorageConfig) (Storage, error) {
|
func NewStorage(ctx context.Context, cfg storcfg.StorageConfig) (Storage, error) {
|
||||||
constructor, ok := storageConstructors[cfg.GetType()]
|
constructor, ok := storageConstructors[cfg.GetType()]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
Reference in New Issue
Block a user