mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-08-14 00:44:01 +08:00
* feat: show upload progress for file tasks * feat: show upload progress for batch file tasks * style: distinguish download and upload phases * style: mark successful task completion * fix: preserve storage save error context * fix: serialize single-file progress updates * fix: stabilize batch upload progress reporting Correct batch upload totals, completion state, actual file sizes, and synchronized progress snapshots. Add regression coverage for concurrent updates and phase transitions. * feat: show per-file transfer progress Format single-file and batch download and upload states with Telegram entities, blockquotes, speeds, transferred sizes, progress bars, concise counters, and accurate confirmation handling. Cover upload retries plus final, error, and cancellation messages with regression tests. * chore: remove transfer progress tests * test: restore critical transfer progress coverage * test: cover interleaved batch transfers * fix: declare progress styles in locale templates
109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
storcfg "github.com/krau/SaveAny-Bot/config/storage"
|
|
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
|
"github.com/krau/SaveAny-Bot/pkg/storagetypes"
|
|
"github.com/krau/SaveAny-Bot/storage/alist"
|
|
"github.com/krau/SaveAny-Bot/storage/local"
|
|
"github.com/krau/SaveAny-Bot/storage/minio"
|
|
"github.com/krau/SaveAny-Bot/storage/rclone"
|
|
"github.com/krau/SaveAny-Bot/storage/s3"
|
|
"github.com/krau/SaveAny-Bot/storage/telegram"
|
|
"github.com/krau/SaveAny-Bot/storage/webdav"
|
|
)
|
|
|
|
type Storage interface {
|
|
// Init 只应该在创建存储时调用一次
|
|
Init(ctx context.Context, cfg storcfg.StorageConfig) error
|
|
Type() storenum.StorageType
|
|
Name() string
|
|
Save(ctx context.Context, reader io.Reader, storagePath string) error
|
|
Exists(ctx context.Context, storagePath string) bool
|
|
}
|
|
|
|
type StorageCannotStream interface {
|
|
Storage
|
|
CannotStream() string
|
|
}
|
|
|
|
// StorageBatchSaver can preserve relationships between files when saving a
|
|
// logical batch, such as a Telegram media album.
|
|
type StorageBatchSaver interface {
|
|
Storage
|
|
SaveBatch(ctx context.Context, items []storagetypes.BatchItem) error
|
|
}
|
|
|
|
// StorageBatchProgressSaver reports confirmed upload progress for each item in
|
|
// a logical batch. The item index matches the items slice passed to
|
|
// SaveBatchWithProgress.
|
|
type StorageBatchProgressSaver interface {
|
|
StorageBatchSaver
|
|
SaveBatchWithProgress(
|
|
ctx context.Context,
|
|
items []storagetypes.BatchItem,
|
|
onProgress func(index int, uploaded, total int64),
|
|
) error
|
|
}
|
|
|
|
// StorageProgressSaver reports bytes after the backend has accepted them for
|
|
// upload. Backends with native progress support should implement this instead
|
|
// of relying on progress inferred from reads of the input stream.
|
|
type StorageProgressSaver interface {
|
|
Storage
|
|
SaveWithProgress(
|
|
ctx context.Context,
|
|
reader io.Reader,
|
|
storagePath string,
|
|
onProgress func(uploaded, total int64),
|
|
) error
|
|
}
|
|
|
|
// StorageListable 表示支持列举目录内容的存储
|
|
type StorageListable interface {
|
|
Storage
|
|
ListFiles(ctx context.Context, dirPath string) ([]storagetypes.FileInfo, error)
|
|
}
|
|
|
|
// StorageReadable 表示支持读取文件内容的存储
|
|
type StorageReadable interface {
|
|
Storage
|
|
OpenFile(ctx context.Context, filePath string) (io.ReadCloser, int64, error)
|
|
}
|
|
|
|
var Storages = make(map[string]Storage)
|
|
|
|
var _ StorageProgressSaver = (*telegram.Telegram)(nil)
|
|
var _ StorageBatchProgressSaver = (*telegram.Telegram)(nil)
|
|
|
|
type StorageConstructor func() Storage
|
|
|
|
var storageConstructors = map[storenum.StorageType]StorageConstructor{
|
|
storenum.Alist: func() Storage { return new(alist.Alist) },
|
|
storenum.Local: func() Storage { return new(local.Local) },
|
|
storenum.Webdav: func() Storage { return new(webdav.Webdav) },
|
|
storenum.Minio: func() Storage { return new(minio.Minio) },
|
|
storenum.S3: func() Storage { return new(s3.S3) },
|
|
storenum.Telegram: func() Storage { return new(telegram.Telegram) },
|
|
storenum.Rclone: func() Storage { return new(rclone.Rclone) },
|
|
}
|
|
|
|
// NewStorage creates a new storage instance based on the provided config and initializes it
|
|
func NewStorage(ctx context.Context, cfg storcfg.StorageConfig) (Storage, error) {
|
|
constructor, ok := storageConstructors[cfg.GetType()]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported storage type: %s", cfg.GetType())
|
|
}
|
|
|
|
storage := constructor()
|
|
if err := storage.Init(ctx, cfg); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize storage %s: %w", cfg.GetName(), err)
|
|
}
|
|
|
|
return storage, nil
|
|
}
|